diff --git a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Details.razor b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Details.razor new file mode 100644 index 0000000..f82f25b --- /dev/null +++ b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Details.razor @@ -0,0 +1,305 @@ +@using SZUAbsolventenverein.Module.HallOfFame.Services +@using SZUAbsolventenverein.Module.HallOfFame.Models +@using Oqtane.Security +@using Oqtane.Shared + +@namespace SZUAbsolventenverein.Module.HallOfFame +@inherits ModuleBase +@inject IHallOfFameService HallOfFameService +@inject NavigationManager NavigationManager + +@if (_item == null) +{ +

Loading...

+} +else +{ +
+
+
+
+ @if (!string.IsNullOrEmpty(_item.Image)) + { +
+ @_item.Name + } + else + { +
+ } +
+
+
+
+ +

@_item.Name

+

Absolvent des Jahrgangs @_item.Year

+
+ +
+ +
+
Werdegang & Erfolge
+ @if (_item.IsReported && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) + { +
+
Meldungen:
+ @if (_reports != null && _reports.Any()) + { +
    + @foreach (var report in _reports) + { +
  • +
    + @report.CreatedBy (@report.CreatedOn.ToShortDateString()):
    + @report.Reason +
    + +
  • + } +
+ } + else + { +

Keine detaillierten Meldungen gefunden.

+ } +
+ } +
+ @foreach (var line in (_item.Description?.Replace("\t", " ").Split('\n') ?? Array.Empty())) + { +
@line
+ } +
+
+ +
+ + + @if (!string.IsNullOrEmpty(_item.Link)) + { + + Webseite besuchen + + } + + + Zurück + + +
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) + { + + } + +
+
+
+
+
+
+
+ @if (_showReportModal) + { + + } +} + + + +@code { + public override string Actions => "Details"; + + private HallOfFame _item; + private int _id; + private List _reports; + + private bool _showReportModal = false; + private string _reportReason = ""; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + try + { + _id = Int32.Parse(PageState.QueryString["id"]); + _item = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId); + + if (_item != null && _item.IsReported && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) + { + _reports = await HallOfFameService.GetHallOfFameReportsAsync(_id, ModuleState.ModuleId); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading HallOfFame {HallOfFameId} {Error}", _id, ex.Message); + AddModuleMessage("Fehler beim Laden der Details.", MessageType.Error); + } + } + + private async Task PrintPage() + { + await JSRuntime.InvokeVoidAsync("window.print"); + } + + private void ShowReportModal() + { + _reportReason = ""; + _showReportModal = true; + } + + private void CloseReportModal() + { + _showReportModal = false; + } + + private async Task ReportEntry() + { + if (!string.IsNullOrEmpty(_reportReason)) + { + try + { + await HallOfFameService.ReportAsync(_item.HallOfFameId, ModuleState.ModuleId, _reportReason); + AddModuleMessage("Eintrag wurde erfolgreich gemeldet.", MessageType.Success); + _showReportModal = false; + await LoadData(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Reporting HallOfFame {Error}", ex.Message); + AddModuleMessage("Fehler beim Melden des Eintrags.", MessageType.Error); + } + } + } + + private async Task DeleteEntry() + { + try + { + await HallOfFameService.DeleteHallOfFameAsync(_item.HallOfFameId, ModuleState.ModuleId); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting HallOfFame {Error}", ex.Message); + AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error); + } + } + + private async Task DeleteReport(int reportId) + { + try + { + await HallOfFameService.DeleteHallOfFameReportAsync(reportId, ModuleState.ModuleId); + AddModuleMessage("Meldung gelöscht.", MessageType.Success); + await LoadData(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting Report {Error}", ex.Message); + AddModuleMessage("Fehler beim Löschen der Meldung.", MessageType.Error); + } + } +} diff --git a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Edit.razor b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Edit.razor index e7f45c8..64ac3c3 100644 --- a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Edit.razor +++ b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Edit.razor @@ -1,6 +1,7 @@ @using Oqtane.Modules.Controls @using SZUAbsolventenverein.Module.HallOfFame.Services @using SZUAbsolventenverein.Module.HallOfFame.Models +@using Microsoft.AspNetCore.Components.Forms @namespace SZUAbsolventenverein.Module.HallOfFame @inherits ModuleBase @@ -20,21 +21,47 @@
- +
Bitte gib einen gültigen Jahrgang ein.
- + +
@(_description?.Length ?? 0) / 500 Zeichen
Bitte gib eine Beschreibung ein.
-
- +
+
- +
+ @if (!string.IsNullOrEmpty(_image)) + { +
+ Vorschau + +
+ } + else + { +
+ Kein Bild +
+ } +
+ +
Nur JPG oder PNG, max. 5 MB.
+ @if (_uploading) + { +
+ Wird hochgeladen... +
+ Wird hochgeladen... + }
@@ -53,8 +80,8 @@
- - + + Abbrechen
@@ -79,6 +106,7 @@ private ElementReference form; private bool validated = false; + private bool _uploading = false; private int _id; private string _name; @@ -102,7 +130,6 @@ _id = Int32.Parse(PageState.QueryString["id"]); HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId); - // Security check: only allow editing own entry if (HallOfFame != null) { if (HallOfFame.UserId != PageState.User.UserId) @@ -126,11 +153,9 @@ } else // Add Mode { - // Check if user already has an entry to prevent duplicates var existing = await HallOfFameService.GetHallOfFameByUserIdAsync(PageState.User.UserId, ModuleState.ModuleId); if (existing != null) { - // Use NavigateUrl with parameters properly (simplified here) NavigationManager.NavigateTo(EditUrl(existing.HallOfFameId.ToString())); } } @@ -142,6 +167,50 @@ } } + private async Task HandleFileSelected(InputFileChangeEventArgs e) + { + var file = e.File; + if (file == null) return; + + if (file.Size > 5 * 1024 * 1024) + { + AddModuleMessage("Die Datei ist zu groß (max. 5 MB allowed).", MessageType.Warning); + return; + } + + try + { + _uploading = true; + using var stream = file.OpenReadStream(5 * 1024 * 1024); + var url = await HallOfFameService.UploadFileAsync(stream, file.Name, ModuleState.ModuleId); + if (!string.IsNullOrEmpty(url)) + { + _image = url; + AddModuleMessage("Foto erfolgreich hochgeladen.", MessageType.Success); + } + else + { + AddModuleMessage("Fehler beim Hochladen des Fotos.", MessageType.Error); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Uploading File {Error}", ex.Message); + AddModuleMessage("Ein technischer Fehler ist beim Upload aufgetreten.", MessageType.Error); + } + finally + { + _uploading = false; + StateHasChanged(); + } + } + + private void RemoveImage() + { + _image = string.Empty; + StateHasChanged(); + } + private async Task Save(string status) { try @@ -156,7 +225,7 @@ { HallOfFame HallOfFame = new HallOfFame(); HallOfFame.ModuleId = ModuleState.ModuleId; - HallOfFame.UserId = PageState.User.UserId; // Set Owner + HallOfFame.UserId = PageState.User.UserId; HallOfFame.Name = _name; HallOfFame.Year = _year; HallOfFame.Description = _description; @@ -170,7 +239,6 @@ else { HallOfFame HallOfFame = await HallOfFameService.GetHallOfFameAsync(_id, ModuleState.ModuleId); - // Ensure we don't overwrite with invalid user logic, though server checks too if (HallOfFame.UserId == PageState.User.UserId) { HallOfFame.Name = _name; diff --git a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Index.razor b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Index.razor index c10e17b..30262cc 100644 --- a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Index.razor +++ b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Index.razor @@ -1,5 +1,7 @@ @using SZUAbsolventenverein.Module.HallOfFame.Services @using SZUAbsolventenverein.Module.HallOfFame.Models +@using Oqtane.Security +@using Oqtane.Shared @namespace SZUAbsolventenverein.Module.HallOfFame @inherits ModuleBase @@ -13,30 +15,44 @@ } else { -
-
+
+
+
+ + +
+
+
+
+ + +
+
+
@if (PageState.User != null) { if (_myEntry != null) { - + } else { - + } } - else - { -

Einloggen, um einen Eintrag zu erstellen.

- }
@if (@_HallOfFames.Count != 0) {
- @foreach (var item in _HallOfFames) + @foreach (var item in FilteredHallOfFames) {
@@ -44,13 +60,35 @@ else { @item.Name } -
+
@item.Name (@item.Year)
-

@item.Description

- @if (!string.IsNullOrEmpty(item.Link)) + @if (item.IsReported && UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) { - Mehr Infos +
+ Dieser Eintrag wurde gemeldet! +
} +
+ @foreach (var line in (item.Description?.Replace("\t", " ").Split('\n') ?? Array.Empty())) + { +
@line
+ } +
+
+ +
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin + ";" + RoleNames.Host)) + { + + } +
+
@@ -66,7 +104,6 @@ else } @code { - public override string RenderMode => RenderModes.Static; public override List Resources => new List() { @@ -76,8 +113,53 @@ else List _HallOfFames; HallOfFame _myEntry; + string _searchText = ""; + string _sortOption = "CreatedOn"; + bool _sortAscending = false; + + IEnumerable FilteredHallOfFames + { + get + { + var items = _HallOfFames.AsEnumerable(); + if (!string.IsNullOrEmpty(_searchText)) + { + items = items.Where(i => + (i.Name?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) || + (i.Description?.Contains(_searchText, StringComparison.OrdinalIgnoreCase) ?? false) + ); + } + + items = _sortOption switch + { + "Name" => _sortAscending ? items.OrderBy(i => i.Name) : items.OrderByDescending(i => i.Name), + "Year" => _sortAscending ? items.OrderBy(i => i.Year) : items.OrderByDescending(i => i.Year), + "CreatedOn" or _ => _sortAscending ? items.OrderBy(i => i.CreatedOn) : items.OrderByDescending(i => i.CreatedOn) + }; + + return items; + } + } + + private string TruncateDescription(string description) + { + if (string.IsNullOrEmpty(description)) return ""; + const int maxLength = 150; + if (description.Length <= maxLength) return description; + return description.Substring(0, maxLength).TrimEnd() + "..."; + } + + private void ToggleSortDirection() + { + _sortAscending = !_sortAscending; + } protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() { try { @@ -94,4 +176,19 @@ else AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error); } } + + private async Task Delete(int hallOfFameId) + { + try + { + await HallOfFameService.DeleteHallOfFameAsync(hallOfFameId, ModuleState.ModuleId); + AddModuleMessage("Eintrag wurde gelöscht.", MessageType.Success); + await LoadData(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting HallOfFame {Error}", ex.Message); + AddModuleMessage("Fehler beim Löschen des Eintrags.", MessageType.Error); + } + } } \ No newline at end of file diff --git a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/ModuleInfo.cs b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/ModuleInfo.cs index 5898e34..ffaaeda 100644 --- a/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/ModuleInfo.cs +++ b/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/ModuleInfo.cs @@ -9,9 +9,9 @@ namespace SZUAbsolventenverein.Module.HallOfFame { Name = "HallOfFame", Description = "The Hall of Fame module displays selected individuals or achievements within the CMS. Entries are shown online, can be exported as a PDF, and are only published with user consen.", - Version = "1.0.0", + Version = "1.0.3", ServerManagerType = "SZUAbsolventenverein.Module.HallOfFame.Manager.HallOfFameManager, SZUAbsolventenverein.Module.HallOfFame.Server.Oqtane", - ReleaseVersions = "1.0.0", + ReleaseVersions = "1.0.0,1.0.2,1.0.3", Dependencies = "SZUAbsolventenverein.Module.HallOfFame.Shared.Oqtane", PackageName = "SZUAbsolventenverein.Module.HallOfFame" }; diff --git a/Client/Services/HallOfFameService.cs b/Client/Services/HallOfFameService.cs index 8451938..6b0d31b 100644 --- a/Client/Services/HallOfFameService.cs +++ b/Client/Services/HallOfFameService.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net; using System.Threading.Tasks; using Oqtane.Services; using Oqtane.Shared; +using System.Text.Json; namespace SZUAbsolventenverein.Module.HallOfFame.Services { @@ -18,8 +20,11 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services Task AddHallOfFameAsync(Models.HallOfFame HallOfFame); Task UpdateHallOfFameAsync(Models.HallOfFame HallOfFame); - Task DeleteHallOfFameAsync(int HallOfFameId, int ModuleId); + Task ReportAsync(int HallOfFameId, int ModuleId, string reason); + Task> GetHallOfFameReportsAsync(int HallOfFameId, int ModuleId); + Task DeleteHallOfFameReportAsync(int HallOfFameReportId, int ModuleId); + Task UploadFileAsync(System.IO.Stream stream, string fileName, int ModuleId); } public class HallOfFameService : ServiceBase, IHallOfFameService @@ -30,8 +35,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services public async Task> GetHallOfFamesAsync(int ModuleId) { - List HallOfFames = await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty().ToList()); - return HallOfFames.OrderBy(item => item.Name).ToList(); + return await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty().ToList()); } public async Task GetHallOfFameAsync(int HallOfFameId, int ModuleId) @@ -58,5 +62,38 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services { await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{HallOfFameId}/{ModuleId}", EntityNames.Module, ModuleId)); } + + public async Task ReportAsync(int HallOfFameId, int ModuleId, string reason) + { + await PutAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/report/{HallOfFameId}?reason={WebUtility.UrlEncode(reason)}", EntityNames.Module, ModuleId)); + } + + public async Task> GetHallOfFameReportsAsync(int HallOfFameId, int ModuleId) + { + return await GetJsonAsync>(CreateAuthorizationPolicyUrl($"{Apiurl}/reports/{HallOfFameId}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Enumerable.Empty().ToList()); + } + + public async Task DeleteHallOfFameReportAsync(int HallOfFameReportId, int ModuleId) + { + await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/report/{HallOfFameReportId}/{ModuleId}", EntityNames.Module, ModuleId)); + } + + public async Task UploadFileAsync(System.IO.Stream stream, string fileName, int ModuleId) + { + var uri = CreateAuthorizationPolicyUrl($"{Apiurl}/upload", EntityNames.Module, ModuleId); + using var content = new MultipartFormDataContent(); + var fileContent = new StreamContent(stream); + fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); + content.Add(fileContent, "file", fileName); + + var response = await GetHttpClient().PostAsync(uri, content); + if (response.IsSuccessStatusCode) + { + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return result["url"]; + } + return null; + } } } diff --git a/Package/debug.sh b/Package/debug.sh index f90961d..5b29862 100755 --- a/Package/debug.sh +++ b/Package/debug.sh @@ -9,5 +9,5 @@ cp -f "../Server/bin/Debug/$TargetFramework/$ProjectName.Server.Oqtane.dll" "../ cp -f "../Server/bin/Debug/$TargetFramework/$ProjectName.Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName.Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/$TargetFramework/" -mkdir -p "../../oqtane.framework/Oqtane.Server/wwwroot/_content/$ProjectName/" + cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/_content/$ProjectName/" diff --git a/Server/Controllers/HallOfFameController.cs b/Server/Controllers/HallOfFameController.cs index fb95661..c6aa6dc 100644 --- a/Server/Controllers/HallOfFameController.cs +++ b/Server/Controllers/HallOfFameController.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using System.Collections.Generic; @@ -10,6 +11,8 @@ using SZUAbsolventenverein.Module.HallOfFame.Services; using Oqtane.Controllers; using System.Net; using System.Threading.Tasks; +using System.IO; +using Microsoft.AspNetCore.Hosting; namespace SZUAbsolventenverein.Module.HallOfFame.Controllers { @@ -17,10 +20,12 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers public class HallOfFameController : ModuleControllerBase { private readonly IHallOfFameService _HallOfFameService; + private readonly IWebHostEnvironment _environment; - public HallOfFameController(IHallOfFameService HallOfFameService, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) + public HallOfFameController(IHallOfFameService HallOfFameService, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor) { _HallOfFameService = HallOfFameService; + _environment = environment; } // GET: api/?moduleid=x @@ -33,9 +38,10 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) { var list = await _HallOfFameService.GetHallOfFamesAsync(ModuleId); - // Filter: Show only Published unless user has Edit permissions (simplified check for now, can be expanded) - // For now, let's filter in memory or service. The requirement says: "Hauptseite zeigt nur Published". - // We will filter here. + if (User.IsInRole(RoleNames.Admin) || User.IsInRole(RoleNames.Host)) + { + return list; + } return list.Where(item => item.Status == "Published"); } else @@ -138,7 +144,47 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers return HallOfFame; } - // DELETE api//5 + // PUT api//report/5 + [HttpPut("report/{id}")] + [Authorize(Policy = PolicyNames.ViewModule)] + public async Task Report(int id, [FromQuery] string reason) + { + Models.HallOfFame HallOfFame = await _HallOfFameService.GetHallOfFameAsync(id, -1); + if (HallOfFame != null && IsAuthorizedEntityId(EntityNames.Module, HallOfFame.ModuleId)) + { + await _HallOfFameService.ReportAsync(id, HallOfFame.ModuleId, reason); + } + } + + // GET api//reports/5?moduleid=x + [HttpGet("reports/{id}")] + [Authorize(Policy = PolicyNames.EditModule)] + public async Task> GetReports(int id, string moduleid) + { + int ModuleId; + if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) + { + return await _HallOfFameService.GetHallOfFameReportsAsync(id, ModuleId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame GetReports Attempt {HallOfFameId} {ModuleId}", id, moduleid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // DELETE api//report/5/x + [HttpDelete("report/{id}/{moduleid}")] + [Authorize(Policy = PolicyNames.EditModule)] + public async Task DeleteReport(int id, int moduleid) + { + if (IsAuthorizedEntityId(EntityNames.Module, moduleid)) + { + await _HallOfFameService.DeleteHallOfFameReportAsync(id, moduleid); + } + } + [HttpDelete("{id}/{moduleid}")] [Authorize(Policy = PolicyNames.EditModule)] public async Task Delete(int id, int moduleid) @@ -154,5 +200,33 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } + [HttpPost("upload")] + [Authorize(Policy = PolicyNames.EditModule)] + public async Task Upload(IFormFile file) + { + if (file == null || file.Length == 0) return BadRequest("Keine Datei ausgewählt."); + + var extension = Path.GetExtension(file.FileName).ToLower(); + if (extension != ".jpg" && extension != ".jpeg" && extension != ".png") + { + return BadRequest("Nur JPG und PNG Dateien sind erlaubt."); + } + + var folder = Path.Combine(_environment.WebRootPath, "Content", "HallOfFame"); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + var fileName = Guid.NewGuid().ToString() + extension; + var path = Path.Combine(folder, fileName); + + using (var stream = new FileStream(path, FileMode.Create)) + { + await file.CopyToAsync(stream); + } + + return Ok(new { url = "/Content/HallOfFame/" + fileName }); + } } } diff --git a/Server/Migrations/01000001_AddHallOfFameColumns.cs b/Server/Migrations/01000001_AddHallOfFameColumns.cs deleted file mode 100644 index 3956fc5..0000000 --- a/Server/Migrations/01000001_AddHallOfFameColumns.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Oqtane.Databases.Interfaces; -using Oqtane.Migrations; -using SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders; -using SZUAbsolventenverein.Module.HallOfFame.Repository; - -namespace SZUAbsolventenverein.Module.HallOfFame.Migrations -{ - [DbContext(typeof(HallOfFameContext))] - [Migration("SZUAbsolventenverein.Module.HallOfFame.01.00.00.01")] - public class AddHallOfFameColumns : MultiDatabaseMigration - { - public AddHallOfFameColumns(IDatabase database) : base(database) - { - } - - protected override void Up(MigrationBuilder migrationBuilder) - { - var entityBuilder = new HallOfFameEntityBuilder(migrationBuilder, ActiveDatabase); - - migrationBuilder.AddColumn( - name: "Year", - table: "SZUAbsolventenvereinHallOfFame", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "Description", - table: "SZUAbsolventenvereinHallOfFame", - nullable: true); - - migrationBuilder.AddColumn( - name: "Image", - table: "SZUAbsolventenvereinHallOfFame", - nullable: true); - - migrationBuilder.AddColumn( - name: "Link", - table: "SZUAbsolventenvereinHallOfFame", - nullable: true); - - migrationBuilder.AddColumn( - name: "Status", - table: "SZUAbsolventenvereinHallOfFame", - maxLength: 50, - nullable: true); - - migrationBuilder.AddColumn( - name: "UserId", - table: "SZUAbsolventenvereinHallOfFame", - nullable: false, - defaultValue: 0); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Year", - table: "SZUAbsolventenvereinHallOfFame"); - - migrationBuilder.DropColumn( - name: "Description", - table: "SZUAbsolventenvereinHallOfFame"); - - migrationBuilder.DropColumn( - name: "Image", - table: "SZUAbsolventenvereinHallOfFame"); - - migrationBuilder.DropColumn( - name: "Link", - table: "SZUAbsolventenvereinHallOfFame"); - - migrationBuilder.DropColumn( - name: "Status", - table: "SZUAbsolventenvereinHallOfFame"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "SZUAbsolventenvereinHallOfFame"); - } - } -} diff --git a/Server/Migrations/01000002_AddReportingColumns.cs b/Server/Migrations/01000002_AddReportingColumns.cs new file mode 100644 index 0000000..65e918e --- /dev/null +++ b/Server/Migrations/01000002_AddReportingColumns.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations; +using SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders; +using SZUAbsolventenverein.Module.HallOfFame.Repository; + +namespace SZUAbsolventenverein.Module.HallOfFame.Migrations +{ + [DbContext(typeof(HallOfFameContext))] + [Migration("SZUAbsolventenverein.Module.HallOfFame.01.00.00.02")] + public class AddReportingColumns : MultiDatabaseMigration + { + public AddReportingColumns(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsReported", + table: "SZUAbsolventenvereinHallOfFame", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ReportReason", + table: "SZUAbsolventenvereinHallOfFame", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsReported", + table: "SZUAbsolventenvereinHallOfFame"); + + migrationBuilder.DropColumn( + name: "ReportReason", + table: "SZUAbsolventenvereinHallOfFame"); + } + } +} diff --git a/Server/Migrations/01000003_AddReportTable.cs b/Server/Migrations/01000003_AddReportTable.cs new file mode 100644 index 0000000..def5ba8 --- /dev/null +++ b/Server/Migrations/01000003_AddReportTable.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations; +using SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders; +using SZUAbsolventenverein.Module.HallOfFame.Repository; + +namespace SZUAbsolventenverein.Module.HallOfFame.Migrations +{ + [DbContext(typeof(HallOfFameContext))] + [Migration("SZUAbsolventenverein.Module.HallOfFame.01.00.00.03")] + public class AddReportTable : MultiDatabaseMigration + { + public AddReportTable(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var entityBuilder = new HallOfFameReportEntityBuilder(migrationBuilder, ActiveDatabase); + entityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + var entityBuilder = new HallOfFameReportEntityBuilder(migrationBuilder, ActiveDatabase); + entityBuilder.Drop(); + } + } +} diff --git a/Server/Migrations/EntityBuilders/HallOfFameEntityBuilder.cs b/Server/Migrations/EntityBuilders/HallOfFameEntityBuilder.cs index b3d6d56..9226896 100644 --- a/Server/Migrations/EntityBuilders/HallOfFameEntityBuilder.cs +++ b/Server/Migrations/EntityBuilders/HallOfFameEntityBuilder.cs @@ -44,6 +44,8 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders public OperationBuilder Link { get; set; } public OperationBuilder Status { get; set; } public OperationBuilder UserId { get; set; } + public OperationBuilder IsReported { get; set; } + public OperationBuilder ReportReason { get; set; } } } diff --git a/Server/Migrations/EntityBuilders/HallOfFameReportEntityBuilder.cs b/Server/Migrations/EntityBuilders/HallOfFameReportEntityBuilder.cs new file mode 100644 index 0000000..3489c31 --- /dev/null +++ b/Server/Migrations/EntityBuilders/HallOfFameReportEntityBuilder.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations; +using Oqtane.Migrations.EntityBuilders; + +namespace SZUAbsolventenverein.Module.HallOfFame.Migrations.EntityBuilders +{ + public class HallOfFameReportEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SZUAbsolventenvereinHallOfFameReport"; + private readonly PrimaryKey _primaryKey = new("PK_SZUAbsolventenvereinHallOfFameReport", x => x.HallOfFameReportId); + private readonly ForeignKey _hallOfFameForeignKey = new("FK_SZUAbsolventenvereinHallOfFameReport_HallOfFame", x => x.HallOfFameId, "SZUAbsolventenvereinHallOfFame", "HallOfFameId", ReferentialAction.Cascade); + + public HallOfFameReportEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_hallOfFameForeignKey); + } + + protected override HallOfFameReportEntityBuilder BuildTable(ColumnsBuilder table) + { + HallOfFameReportId = AddAutoIncrementColumn(table, "HallOfFameReportId"); + HallOfFameId = AddIntegerColumn(table, "HallOfFameId"); + Reason = AddMaxStringColumn(table, "Reason"); + AddAuditableColumns(table); + return this; + } + + public OperationBuilder HallOfFameReportId { get; set; } + public OperationBuilder HallOfFameId { get; set; } + public OperationBuilder Reason { get; set; } + } +} diff --git a/Server/Repository/HallOfFameContext.cs b/Server/Repository/HallOfFameContext.cs index 88d3d62..e0a6612 100644 --- a/Server/Repository/HallOfFameContext.cs +++ b/Server/Repository/HallOfFameContext.cs @@ -10,6 +10,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository public class HallOfFameContext : DBContextBase, ITransientService, IMultiDatabase { public virtual DbSet HallOfFame { get; set; } + public virtual DbSet HallOfFameReport { get; set; } public HallOfFameContext(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies) { @@ -21,6 +22,7 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository base.OnModelCreating(builder); builder.Entity().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinHallOfFame")); + builder.Entity().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinHallOfFameReport")); } } } diff --git a/Server/Repository/HallOfFameRepository.cs b/Server/Repository/HallOfFameRepository.cs index 5f50af0..32dd537 100644 --- a/Server/Repository/HallOfFameRepository.cs +++ b/Server/Repository/HallOfFameRepository.cs @@ -13,6 +13,11 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository Models.HallOfFame AddHallOfFame(Models.HallOfFame HallOfFame); Models.HallOfFame UpdateHallOfFame(Models.HallOfFame HallOfFame); void DeleteHallOfFame(int HallOfFameId); + + IEnumerable GetHallOfFameReports(int HallOfFameId); + Models.HallOfFameReport GetHallOfFameReport(int HallOfFameReportId); + Models.HallOfFameReport AddHallOfFameReport(Models.HallOfFameReport HallOfFameReport); + void DeleteHallOfFameReport(int HallOfFameReportId); } public class HallOfFameRepository : IHallOfFameRepository, ITransientService @@ -27,7 +32,14 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository public IEnumerable GetHallOfFames(int ModuleId) { using var db = _factory.CreateDbContext(); - return db.HallOfFame.Where(item => item.ModuleId == ModuleId).ToList(); + var items = db.HallOfFame.Where(item => item.ModuleId == ModuleId) + .OrderByDescending(item => item.CreatedOn) + .ToList(); + foreach (var item in items) + { + item.Description = item.Description?.Replace("\t", " "); + } + return items; } public Models.HallOfFame GetHallOfFame(int HallOfFameId) @@ -38,19 +50,26 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository public Models.HallOfFame GetHallOfFame(int HallOfFameId, bool tracking) { using var db = _factory.CreateDbContext(); + Models.HallOfFame item; if (tracking) { - return db.HallOfFame.Find(HallOfFameId); + item = db.HallOfFame.Find(HallOfFameId); } else { - return db.HallOfFame.AsNoTracking().FirstOrDefault(item => item.HallOfFameId == HallOfFameId); + item = db.HallOfFame.AsNoTracking().FirstOrDefault(i => i.HallOfFameId == HallOfFameId); } + if (item != null) + { + item.Description = item.Description?.Replace("\t", " "); + } + return item; } public Models.HallOfFame AddHallOfFame(Models.HallOfFame HallOfFame) { using var db = _factory.CreateDbContext(); + HallOfFame.Description = HallOfFame.Description?.Replace("\t", " "); db.HallOfFame.Add(HallOfFame); db.SaveChanges(); return HallOfFame; @@ -59,17 +78,72 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Repository public Models.HallOfFame UpdateHallOfFame(Models.HallOfFame HallOfFame) { using var db = _factory.CreateDbContext(); + HallOfFame.Description = HallOfFame.Description?.Replace("\t", " "); db.Entry(HallOfFame).State = EntityState.Modified; db.SaveChanges(); return HallOfFame; } public void DeleteHallOfFame(int HallOfFameId) + { + // First transaction: Delete all associated reports + using (var db = _factory.CreateDbContext()) + { + var reports = db.HallOfFameReport.Where(item => item.HallOfFameId == HallOfFameId).ToList(); + if (reports.Any()) + { + db.HallOfFameReport.RemoveRange(reports); + db.SaveChanges(); + } + } + + // Second transaction: Delete the HallOfFame entry itself + using (var db = _factory.CreateDbContext()) + { + var hallOfFame = db.HallOfFame.Find(HallOfFameId); + if (hallOfFame != null) + { + db.HallOfFame.Remove(hallOfFame); + db.SaveChanges(); + } + } + } + + public IEnumerable GetHallOfFameReports(int HallOfFameId) { using var db = _factory.CreateDbContext(); - Models.HallOfFame HallOfFame = db.HallOfFame.Find(HallOfFameId); - db.HallOfFame.Remove(HallOfFame); + return db.HallOfFameReport.Where(item => item.HallOfFameId == HallOfFameId) + .OrderByDescending(item => item.CreatedOn) + .ToList(); + } + + public Models.HallOfFameReport GetHallOfFameReport(int HallOfFameReportId) + { + using var db = _factory.CreateDbContext(); + return db.HallOfFameReport.Find(HallOfFameReportId); + } + + public Models.HallOfFameReport AddHallOfFameReport(Models.HallOfFameReport HallOfFameReport) + { + using var db = _factory.CreateDbContext(); + db.HallOfFameReport.Add(HallOfFameReport); db.SaveChanges(); + return HallOfFameReport; + } + + public void DeleteHallOfFameReport(int HallOfFameReportId) + { + using var db = _factory.CreateDbContext(); + + // Clear any tracked entities to avoid conflicts + db.ChangeTracker.Clear(); + + Models.HallOfFameReport HallOfFameReport = db.HallOfFameReport.Find(HallOfFameReportId); + if (HallOfFameReport != null) + { + db.HallOfFameReport.Remove(HallOfFameReport); + db.SaveChanges(); + } } } } diff --git a/Server/Services/HallOfFameService.cs b/Server/Services/HallOfFameService.cs index 6d8845a..db8d1b6 100644 --- a/Server/Services/HallOfFameService.cs +++ b/Server/Services/HallOfFameService.cs @@ -2,12 +2,16 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using System.Net.Http; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Security; using Oqtane.Shared; using SZUAbsolventenverein.Module.HallOfFame.Repository; +using Microsoft.AspNetCore.Hosting; +using System.IO; +using System; namespace SZUAbsolventenverein.Module.HallOfFame.Services { @@ -18,14 +22,16 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services private readonly ILogManager _logger; private readonly IHttpContextAccessor _accessor; private readonly Alias _alias; + private readonly IWebHostEnvironment _environment; - public ServerHallOfFameService(IHallOfFameRepository HallOfFameRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) + public ServerHallOfFameService(IHallOfFameRepository HallOfFameRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) { _HallOfFameRepository = HallOfFameRepository; _userPermissions = userPermissions; _logger = logger; _accessor = accessor; _alias = tenantManager.GetAlias(); + _environment = environment; } public Task> GetHallOfFamesAsync(int ModuleId) @@ -111,5 +117,104 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Services } return Task.CompletedTask; } + + public Task ReportAsync(int HallOfFameId, int ModuleId, string reason) + { + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.View)) + { + var report = new Models.HallOfFameReport + { + HallOfFameId = HallOfFameId, + Reason = reason + }; + _HallOfFameRepository.AddHallOfFameReport(report); + + var hallOfFame = _HallOfFameRepository.GetHallOfFame(HallOfFameId); + if (hallOfFame != null && !hallOfFame.IsReported) + { + hallOfFame.IsReported = true; + _HallOfFameRepository.UpdateHallOfFame(hallOfFame); + } + _logger.Log(LogLevel.Information, this, LogFunction.Update, "HallOfFame Reported {HallOfFameId}", HallOfFameId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Report Attempt {HallOfFameId} {ModuleId}", HallOfFameId, ModuleId); + } + return Task.CompletedTask; + } + + public Task> GetHallOfFameReportsAsync(int HallOfFameId, int ModuleId) + { + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) + { + return Task.FromResult(_HallOfFameRepository.GetHallOfFameReports(HallOfFameId).ToList()); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Get Reports Attempt {HallOfFameId} {ModuleId}", HallOfFameId, ModuleId); + return null; + } + } + + public Task DeleteHallOfFameReportAsync(int HallOfFameReportId, int ModuleId) + { + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) + { + var report = _HallOfFameRepository.GetHallOfFameReport(HallOfFameReportId); + if (report != null) + { + int hallOfFameId = report.HallOfFameId; + _HallOfFameRepository.DeleteHallOfFameReport(HallOfFameReportId); + + // Check if there are any reports left for this entry + var remainingReports = _HallOfFameRepository.GetHallOfFameReports(hallOfFameId); + if (!remainingReports.Any()) + { + var hallOfFame = _HallOfFameRepository.GetHallOfFame(hallOfFameId); + if (hallOfFame != null) + { + hallOfFame.IsReported = false; + _HallOfFameRepository.UpdateHallOfFame(hallOfFame); + } + } + } + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "HallOfFame Report Deleted {HallOfFameReportId}", HallOfFameReportId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame Delete Report Attempt {HallOfFameReportId} {ModuleId}", HallOfFameReportId, ModuleId); + } + return Task.CompletedTask; + } + public async Task UploadFileAsync(Stream stream, string fileName, int ModuleId) + { + if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) + { + var extension = Path.GetExtension(fileName).ToLower(); + + if (extension != ".jpg" && extension != ".jpeg" && extension != ".png") + { + return null; + } + + var folder = Path.Combine(_environment.WebRootPath, "Content", "HallOfFame"); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + var newFileName = Guid.NewGuid().ToString() + extension; + var path = Path.Combine(folder, newFileName); + + using (var fileStream = new FileStream(path, FileMode.Create)) + { + await stream.CopyToAsync(fileStream); + } + + return "/Content/HallOfFame/" + newFileName; + } + return null; + } } } diff --git a/Server/wwwroot/Module.css b/Server/wwwroot/Module.css index 0856a26..2f6c06b 100644 --- a/Server/wwwroot/Module.css +++ b/Server/wwwroot/Module.css @@ -1 +1,17 @@ -/* Module Custom Styles */ \ No newline at end of file +/* Module Custom Styles */ + +.hof-description-container { + min-height: 120px; + /* Adjust this value based on the desired card size */ + margin-bottom: 1rem; +} + +.hof-description-line { + display: block; + padding-left: 1.1em; + text-indent: -1.1em; + margin-bottom: 0.2rem; + line-height: 1.5; + word-break: break-word; + text-align: left; +} \ No newline at end of file diff --git a/Shared/Models/HallOfFame.cs b/Shared/Models/HallOfFame.cs index 4219d5b..7678226 100644 --- a/Shared/Models/HallOfFame.cs +++ b/Shared/Models/HallOfFame.cs @@ -19,6 +19,8 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Models public string Link { get; set; } public string Status { get; set; } // "Draft" or "Published" public int UserId { get; set; } // Owner + public bool IsReported { get; set; } + public string ReportReason { get; set; } public string CreatedBy { get; set; } diff --git a/Shared/Models/HallOfFameReport.cs b/Shared/Models/HallOfFameReport.cs new file mode 100644 index 0000000..48514e4 --- /dev/null +++ b/Shared/Models/HallOfFameReport.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Oqtane.Models; + +namespace SZUAbsolventenverein.Module.HallOfFame.Models +{ + [Table("SZUAbsolventenvereinHallOfFameReport")] + public class HallOfFameReport : IAuditable + { + [Key] + public int HallOfFameReportId { get; set; } + public int HallOfFameId { get; set; } + public string Reason { get; set; } + + public string CreatedBy { get; set; } + public DateTime CreatedOn { get; set; } + public string ModifiedBy { get; set; } + public DateTime ModifiedOn { get; set; } + } +}