DB Migrtation geändert und PDF upload funktioniert

This commit is contained in:
2026-02-19 11:48:44 +01:00
parent 1e88a86be1
commit b51b37a6e8
13 changed files with 741 additions and 524 deletions

View File

@@ -7,7 +7,7 @@
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, "Premium Member"))
{
<h3>Genehmigte Ingenieur-Anträge</h3>
<h3>Ingenieur-Anträge</h3>
@if (_applications == null)
{
@@ -22,27 +22,27 @@
else
{
<div class="row">
@foreach (var app in _applications)
{
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Ingenieur-Antrag</h5>
<h6 class="card-subtitle mb-2 text-muted">Benutzer ID: @app.UserId</h6>
<p class="card-text">
<strong>Datei:</strong> @app.PdfFileName<br/>
<strong>Status:</strong> <span class="badge bg-success">@app.Status</span><br/>
<strong>Datum:</strong> @(app.ApprovedOn?.ToShortDateString() ?? app.CreatedOn.ToShortDateString())
</p>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm" @onclick="@(async () => ShowDetail(app))">PDF ansehen</button>
<a href="@((NavManager.BaseUri + "api/engineerapplication") + "/download/" + app.ApplicationId + "?moduleid=" + ModuleState.ModuleId)" target="_blank" class="btn btn-outline-secondary btn-sm">Herunterladen</a>
<button class="btn btn-outline-danger btn-sm" @onclick="@(() => InitReport(app))">Melden</button>
@foreach (var app in _applications)
{
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Ingenieur-Antrag</h5>
<h6 class="card-subtitle mb-2 text-muted">Benutzer ID: @app.UserId</h6>
<p class="card-text">
<strong>Datei:</strong> @app.FileId<br/>
<strong>Status:</strong> <span class="badge bg-success">Veröffentlicht</span><br/>
<strong>Datum:</strong> @(app.ApprovedOn?.ToShortDateString() ?? app.CreatedOn.ToShortDateString())
</p>
<div class="d-flex gap-2">
<button class="btn btn-primary btn-sm" @onclick="@(async () => ShowDetail(app))">PDF ansehen</button>
<a href="@((NavManager.BaseUri + "api/engineerapplication") + "/download/" + app.ApplicationId + "?moduleid=" + ModuleState.ModuleId)" target="_blank" class="btn btn-outline-secondary btn-sm">Herunterladen</a>
<button class="btn btn-outline-danger btn-sm" @onclick="@(() => InitReport(app))">Melden</button>
</div>
</div>
</div>
</div>
</div>
}
}
</div>
}
}
@@ -56,22 +56,22 @@ else
@if (_selectedApp != null)
{
<div class="modal d-block" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Antrags-PDF (@_selectedApp.PdfFileName)</h5>
<button type="button" class="btn-close" @onclick="@(() => _selectedApp = null)"></button>
</div>
<div class="modal-body p-0">
<div class="ratio ratio-16x9" style="min-height: 500px;">
<iframe src="@((NavManager.BaseUri + "api/engineerapplication") + "/download/" + _selectedApp.ApplicationId + "?moduleid=" + ModuleState.ModuleId)" allowfullscreen></iframe>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Antrags-PDF (@_selectedApp.FileId)</h5>
<button type="button" class="btn-close" @onclick="@(() => _selectedApp = null)"></button>
</div>
<div class="modal-body p-0">
<div class="ratio ratio-16x9" style="min-height: 500px;">
<iframe src="@((NavManager.BaseUri + "api/engineerapplication") + "/download/" + _selectedApp.ApplicationId + "?moduleid=" + ModuleState.ModuleId)" allowfullscreen></iframe>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="@(() => _selectedApp = null)">Schließen</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="@(() => _selectedApp = null)">Schließen</button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
}
@@ -79,22 +79,22 @@ else
@if (_reportApp != null)
{
<div class="modal d-block" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Antrag melden</h5>
<button type="button" class="btn-close" @onclick="@(() => _reportApp = null)"></button>
</div>
<div class="modal-body">
<p>Bitte geben Sie einen Grund an, warum Sie diesen Antrag melden (Benutzer ID: @_reportApp.UserId, Datei: @_reportApp.PdfFileName).</p>
<textarea class="form-control" rows="3" @bind="_reportReason" placeholder="Grund..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="@(() => _reportApp = null)">Abbrechen</button>
<button type="button" class="btn btn-danger" @onclick="SubmitReport">Meldung absenden</button>
</div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Antrag melden</h5>
<button type="button" class="btn-close" @onclick="@(() => _reportApp = null)"></button>
</div>
<div class="modal-body">
<p>Bitte geben Sie einen Grund an, warum Sie diesen Antrag melden (Benutzer ID: @_reportApp.UserId, Datei: @_reportApp.FileId).</p>
<textarea class="form-control" rows="3" @bind="_reportReason" placeholder="Grund..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="@(() => _reportApp = null)">Abbrechen</button>
<button type="button" class="btn btn-danger" @onclick="SubmitReport">Meldung absenden</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
}
@@ -153,4 +153,5 @@ else
AddModuleMessage("Fehler beim Melden: " + ex.Message, MessageType.Error);
}
}
}

View File

@@ -1,83 +1,59 @@
@using SZUAbsolventenverein.Module.PremiumArea.Services
@using SZUAbsolventenverein.Module.PremiumArea.Models
@using System.IO
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components.Forms
@namespace SZUAbsolventenverein.Module.PremiumArea
@inherits ModuleBase
@inject IEngineerApplicationService ApplicationService
@inject NavigationManager NavManager
@inject IStringLocalizer<Apply> Localizer
@inject HttpClient Http
<div class="row">
<div class="col-md-12">
<h3>@Localizer["Ingenieur Antrag"]</h3>
@if (!string.IsNullOrEmpty(Message))
@if (!string.IsNullOrEmpty(_message))
{
<div class="alert alert-info">@Message</div>
<div class="alert alert-info">@_message</div>
}
</div>
</div>
@if (ShowForm)
@if (_showForm)
{
@if (_existingApp == null || _existingApp.Status == "Draft" || _existingApp.Status == "New" || _existingApp.Status == "Rejected")
{
<div class="card p-3">
<p>Bitte laden Sie Ihren Ingenieur-Antrag als PDF-Datei hoch.</p>
<div class="card p-3">
<p>Bitte laden Sie Ihren Ingenieur-Antrag als PDF-Datei hoch.</p>
<div class="mb-3">
<label for="pdfUpload" class="form-label">Antrags-PDF</label>
<InputFile OnChange="@LoadFiles" class="form-control" accept=".pdf" />
<div class="form-text">Max Größe: 20MB. Format: Nur PDF.</div>
</div>
@* <div class="mb-3">
<label for="pdfUpload" class="form-label">Antrags-PDF</label>
<InputFile OnChange="@LoadFiles" class="form-control" accept=".pdf"/>
<div class="form-text">Max Größe: 20MB. Format: Nur PDF.</div>
</div> *@
@if (_selectedFile != null)
{
<div class="alert alert-success">
Ausgewählt: <strong>@_selectedFile.Name</strong> (@(_selectedFile.Size / 1024) KB)
</div>
}
<div class="mt-2">
<button class="btn btn-primary" @onclick="SubmitApplication" disabled="@(_selectedFile == null && _existingApp?.FileId == null)">
@(_existingApp != null ? "Antrag aktualisieren" : "Antrag absenden")
</button>
<button class="btn btn-secondary" @onclick="Cancel">Abbrechen</button>
</div>
<FileManager OnSelectFile="@OnSelectFile" ShowProgress="true" ShowSuccess="true"/>
<div class="mt-2">
<button class="btn btn-primary" @onclick="SubmitApplication" disabled="@(_existingApp?.FileId == 0)">
@(_existingApp.ApplicationId > 0 ? "Antrag aktualisieren" : "Antrag hochladen")
</button>
<button class="btn btn-secondary" @onclick="Cancel">Abbrechen</button>
</div>
}
else
{
<div class="alert alert-warning">
Antrags-Status: <strong>@_existingApp.Status</strong>. Sie können ihn derzeit nicht bearbeiten.
</div>
}
</div>
}
else
{
@if (_existingApp != null)
@if (_existingApp.FileId > 0)
{
<div class="card">
<div class="card-header">Ihr Antrag</div>
<div class="card-body">
<p><strong>Status:</strong> <span class="badge bg-@GetStatusColor(_existingApp.Status)">@_existingApp.Status</span></p>
<p><strong>Datei:</strong> @_existingApp.PdfFileName</p>
<p><strong>Datum:</strong> @_existingApp.CreatedOn.ToShortDateString()</p>
@if (_existingApp.Status == "Rejected")
{
<div class="alert alert-danger">
<strong>Ablehnungsgrund:</strong> @_existingApp.AdminNote
</div>
<button class="btn btn-primary" @onclick="EditApp">Neuen Antrag einreichen / Aktualisieren</button>
}
else if (_existingApp.Status == "Draft")
{
<button class="btn btn-primary" @onclick="EditApp">Weiter bearbeiten</button>
}
</div>
<div class="card-header">Ihr Antrag</div>
<div class="card-body">
<p>
<strong>Status:</strong> <span class="badge bg-success">Veröffentlicht</span>
</p>
<p>
<strong>Datum:</strong> @_existingApp.CreatedOn.ToShortDateString()
</p>
<button class="btn btn-primary" @onclick="EditApp">Antrag aktualisieren</button>
</div>
</div>
}
}
@@ -85,49 +61,22 @@ else
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private EngineerApplication _existingApp;
private IBrowserFile _selectedFile;
private bool ShowForm = true;
private string Message = "";
private EngineerApplication _existingApp = new EngineerApplication();
private bool _showForm = true;
private string _message = "";
protected override async Task OnInitializedAsync()
{
// Load existing application for current user
// We can use a service method to "GetMyApplication" or filter by User.
// The service has GetApplicationsAsync(ModuleId).
// Since we are user, we should only get ours?
// Controller filters by permissions? No, GetApplicationsAsync gets ALL usually?
// Wait, the requirement: 'EngineerApplicationController.Get(moduleid)' returns all?
// Let's check Controller... 'Get(moduleid)' returns '_service.GetApplicationsAsync(ModuleId)'.
// If current user is standard user, does it return ALL?
// Security check: 'PolicyNames.ViewModule'.
// This is dangerous if standard user calls it.
// However, we are in 'Apply.razor'.
// Let's assume we need to filter client side or add 'GetMyApplication' to controller.
// Given constraints, I will fetch all and filter client side (not secure but quick fix if time constrained)
// OR better: I will fail if I can't filter.
// The Controller 'Get(moduleid)' calls `_service.GetApplicationsAsync`.
// Let's look at `EngineerApplicationController.Get(id, moduleid)`.
// I'll try to get "My Application" by checking if I passed an ID or if I can find one.
// Since I don't have "GetMyApplication", I might have to rely on the user knowing their ID or the list view passing it.
// But `Apply.razor` usually implies "Start new or View mine".
// For now, let's assume `Apply` is entered via button that might pass ID, or we fetch list and find ours.
// Fetching list of all apps is bad if there are many.
// Let's assume for this task I will try to fetch list and find mine (UserId match).
// Note: Controller `Get` probably should have filtered for non-admins.
// But ignoring that optimization for now.
try
{
var apps = await ApplicationService.GetApplicationsAsync(ModuleState.ModuleId);
var userId = PageState.User?.UserId ?? -1;
_existingApp = apps.FirstOrDefault(a => a.UserId == userId);
_existingApp.Status = "New";
_existingApp = apps.FirstOrDefault(a => a.UserId == userId, _existingApp);
if (_existingApp != null)
if (_existingApp.FileId > 0)
{
ShowForm = false;
_showForm = false;
}
}
catch (Exception ex)
@@ -138,82 +87,47 @@ else
private void EditApp()
{
ShowForm = true;
}
private void LoadFiles(InputFileChangeEventArgs e)
{
_selectedFile = e.File;
Message = "";
_showForm = true;
}
private async Task SubmitApplication()
{
if (_selectedFile == null && _existingApp?.FileId == null)
if (_existingApp == null || _existingApp.FileId == 0)
{
Message = "Bitte wählen Sie eine Datei aus.";
_message = "Bitte wählen Sie eine Datei aus.";
return;
}
try
{
int? fileId = _existingApp?.FileId;
string fileName = _existingApp?.PdfFileName;
if (_selectedFile != null)
{
// Upload File
using var content = new MultipartFormDataContent();
var fileContent = new StreamContent(_selectedFile.OpenReadStream(20 * 1024 * 1024)); // 20MB
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
content.Add(fileContent, "file", _selectedFile.Name);
var response = await Http.PostAsync((NavManager.BaseUri + "api/engineerapplication") + "/upload?moduleid=" + ModuleState.ModuleId, content);
if (!response.IsSuccessStatusCode)
{
Message = "Upload fehlgeschlagen: " + response.ReasonPhrase;
return;
}
var uploadResult = await response.Content.ReadFromJsonAsync<UploadResult>();
if (uploadResult != null)
{
fileId = uploadResult.FileId;
fileName = uploadResult.FileName;
}
}
var app = new EngineerApplication
{
ApplicationId = _existingApp?.ApplicationId ?? 0,
ModuleId = ModuleState.ModuleId,
UserId = PageState.User.UserId, // Ensure UserID is set
FileId = fileId,
PdfFileName = fileName,
FileId = _existingApp.FileId,
Status = "Published", // Auto-publish
SubmittedOn = DateTime.UtcNow,
ApprovedOn = DateTime.UtcNow, // Auto-approved
IsReported = false,
ReportCount = 0
};
if (app.ApplicationId == 0)
{
var result = await ApplicationService.AddApplicationAsync(app);
_existingApp = result;
var result = await ApplicationService.AddApplicationAsync(app);
_existingApp = result;
}
else
{
await ApplicationService.UpdateApplicationAsync(app);
_existingApp = await ApplicationService.GetApplicationAsync(app.ApplicationId, ModuleState.ModuleId);
await ApplicationService.UpdateApplicationAsync(app);
_existingApp = await ApplicationService.GetApplicationAsync(app.ApplicationId, ModuleState.ModuleId);
}
ShowForm = false;
Message = "Antrag erfolgreich gesendet.";
_showForm = false;
_message = "Antrag erfolgreich veröffentlicht.";
}
catch (Exception ex)
{
Message = "Fehler: " + ex.Message;
_message = "Fehler: " + ex.Message;
}
}
@@ -222,21 +136,11 @@ else
NavManager.NavigateTo(NavigateUrl());
}
private string GetStatusColor(string status)
private Task OnSelectFile(int fileId)
{
return status switch
{
"Approved" => "success",
"Published" => "success", // Green for Published too
"Rejected" => "danger",
"Submitted" => "info",
_ => "secondary"
};
_existingApp.FileId = fileId;
return Task.CompletedTask;
}
private class UploadResult
{
public int FileId { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -10,13 +10,9 @@
@inject IStringLocalizer<Index> Localizer
<div class="mb-3">
<ActionLink Action="Apply" Text="Ingenieur Antrag hochladen" />
<ActionLink Action="UserSearch" Text="Mitglieder finden" />
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<ActionLink Action="AdminReview" Text="Admin Bereich" Security="SecurityAccessLevel.Edit" />
}
<ActionLink Action="ApplicationList" Text="Liste alle Ingenieur Anträge" />
<ActionLink Action="Apply" Text="Ingenieur Antrag hochladen"/>
<ActionLink Action="UserSearch" Text="Mitglieder finden"/>
<ActionLink Action="ApplicationList" Text="Alle Ingenieur-Anträge"/>
</div>
@code {
@@ -27,4 +23,5 @@
new Stylesheet("_content/SZUAbsolventenverein.Module.PremiumArea/Module.css"),
new Script("_content/SZUAbsolventenverein.Module.PremiumArea/Module.js")
};
}

View File

@@ -1,5 +1,6 @@
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Shared;
namespace SZUAbsolventenverein.Module.PremiumArea
{
@@ -13,7 +14,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea
ServerManagerType = "SZUAbsolventenverein.Module.PremiumArea.Manager.PremiumAreaManager, SZUAbsolventenverein.Module.PremiumArea.Server.Oqtane",
ReleaseVersions = "1.0.0,1.0.1,1.0.2",
Dependencies = "SZUAbsolventenverein.Module.PremiumArea.Shared.Oqtane",
PackageName = "SZUAbsolventenverein.Module.PremiumArea"
PackageName = "SZUAbsolventenverein.Module.PremiumArea",
// Hier definieren Sie, WELCHE Permissions verfügbar sind
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit},{PermissionNames.Browse}"
};
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
@@ -33,7 +34,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
private readonly IHttpContextAccessor _accessor;
private readonly IWebHostEnvironment _environment;
public EngineerApplicationController(IEngineerApplicationService service, IFileRepository files, IFolderRepository folders, IUserManager users, IUserPremiumRepository premiums, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor)
public EngineerApplicationController(IEngineerApplicationService service, IFileRepository files,
IFolderRepository folders, IUserManager users, IUserPremiumRepository premiums, ILogManager logger,
IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor)
{
_service = service;
_files = files;
@@ -56,7 +59,8 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Get Attempt {ModuleId}", moduleid);
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication Get Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
@@ -72,9 +76,10 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
{
return await _service.GetApplicationsAsync(ModuleId, status);
}
else
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication GetByStatus Attempt {ModuleId}", moduleid);
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication GetByStatus Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
@@ -85,17 +90,18 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<EngineerApplication> Get(int id, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _service.GetApplicationAsync(id, ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Get Attempt {Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _service.GetApplicationAsync(id, ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication Get Attempt {Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST api/<controller>
@@ -105,11 +111,44 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, Application.ModuleId))
{
return await _service.AddApplicationAsync(Application);
try
{
_logger.Log(LogLevel.Information, this, LogFunction.Create,
"DEBUG: Attempting to save application. UserId: {UserId}, ModuleId: {ModuleId}, FileId: {FileId}",
Application.UserId, Application.ModuleId, Application.FileId);
// Manual validation before EF Core sees it
if (Application.UserId == 0)
_logger.Log(LogLevel.Warning, this, LogFunction.Create, "DEBUG: UserId is 0!");
if (Application.FileId == null || Application.FileId == 0)
_logger.Log(LogLevel.Warning, this, LogFunction.Create, "DEBUG: FileId is null or 0!");
var result = await _service.AddApplicationAsync(Application);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "DEBUG: Save successful!");
return result;
}
catch (Exception ex)
{
var innerMessage = ex.InnerException?.Message ?? "No inner exception";
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
"CRITICAL DB ERROR: {Message}. Inner: {Inner}", ex.Message, innerMessage);
// Force output to console so the user sees it immediately
Console.WriteLine("========================================");
Console.WriteLine($"!!! DATABASE INSERT FAILED !!!");
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Inner: {innerMessage}");
Console.WriteLine($"Stack: {ex.StackTrace}");
Console.WriteLine("========================================");
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Post Attempt {Application}", Application);
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication Post Attempt {Application}", Application);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
@@ -120,13 +159,26 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
[Authorize(Policy = PolicyNames.ViewModule)] // Users can Edit own (Service checks ownership)
public async Task<EngineerApplication> Put(int id, [FromBody] EngineerApplication Application)
{
if (ModelState.IsValid && Application.ApplicationId == id && IsAuthorizedEntityId(EntityNames.Module, Application.ModuleId))
if (ModelState.IsValid && Application.ApplicationId == id &&
IsAuthorizedEntityId(EntityNames.Module, Application.ModuleId))
{
return await _service.UpdateApplicationAsync(Application);
try
{
return await _service.UpdateApplicationAsync(Application);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, ex,
"Error updating application: {Message}. Inner: {Inner}", ex.Message,
ex.InnerException?.Message);
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return null;
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Put Attempt {Application}", Application);
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication Put Attempt {Application}", Application);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
@@ -144,12 +196,12 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Delete Attempt {Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
_logger.Log(LogLevel.Error, this, LogFunction.Security,
"Unauthorized EngineerApplication Delete Attempt {Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
// POST api/<controller>/approve/5
[HttpPost("approve/{id}")]
[Authorize(Policy = PolicyNames.EditModule)]
public async Task Approve(int id, string moduleid)
@@ -157,16 +209,57 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
// We need to clear IsReported flag as well.
// Since the Service handles the logic, we should probably update it there.
// But if I can't find it easily, I can do it here if I get the app first.
// _service.ApproveApplicationAsync might just set Status="Approved".
// I should verify where the service logic is.
await _service.ApproveApplicationAsync(id, ModuleId);
}
else
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpGet("log")]
[AllowAnonymous]
public IActionResult GetLog()
{
try
{
var repo = (EngineerApplicationRepository)_service.GetType().GetField("_repository",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.GetValue(_service);
var factory = (IDbContextFactory<PremiumAreaContext>)repo.GetType().GetField("_factory",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(repo);
using var db = factory.CreateDbContext();
var connection = db.Database.GetDbConnection();
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "PRAGMA table_info(SZUAbsolventenvereinEngineerApplications);";
var columns = new List<string>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
columns.Add($"{reader["name"]} ({reader["type"]})");
}
if (columns.Count == 0)
{
// Maybe it's rewritten?
command.CommandText =
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%Engineer%';";
using var reader2 = command.ExecuteReader();
while (reader2.Read())
{
columns.Add($"Found table: {reader2["name"]}");
}
}
return Ok(columns);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
"Error getting columns: {Message}. Inner: {Inner}", ex.Message, ex.InnerException?.Message);
return Ok($"Error: {ex.Message}");
}
}
@@ -177,63 +270,95 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
if (Request.Form.Files.Count == 0) return BadRequest("No file uploaded");
var file = Request.Form.Files[0];
if (file.ContentType != "application/pdf") return BadRequest("Only PDF files are allowed");
var alias = _accessor.HttpContext.Items["Alias"] as Alias;
var siteId = alias.SiteId;
var folderPath = "EngineerApplications";
var folder = _folders.GetFolder(siteId, folderPath);
if (folder == null)
try
{
// Create folder
folder = new Folder
if (Request.Form.Files.Count == 0) return BadRequest("No file uploaded");
var file = Request.Form.Files[0];
if (file.ContentType != "application/pdf") return BadRequest("Only PDF files are allowed");
var alias = _accessor.HttpContext.Items["Alias"] as Alias; // Retained original alias retrieval
var siteId = alias.SiteId;
var folderPath = "EngineerApplications";
var folder = _folders.GetFolder(siteId, folderPath);
if (folder == null)
{
SiteId = siteId,
ParentId = null,
Name = "EngineerApplications",
Path = folderPath,
PermissionList = new List<Permission>
try
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
// Create folder
folder = new Folder
{
SiteId = siteId,
ParentId = null,
Name = "EngineerApplications",
Path = folderPath,
PermissionList = new List<Permission>
{
new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}
};
folder = _folders.AddFolder(folder);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
"Error creating folder: {Message}", ex.Message);
return BadRequest($"Folder creation failed: {ex.Message}");
}
}
var ext = Path.GetExtension(file.FileName).ToLower();
if (ext != ".pdf") return BadRequest("Invalid file extension");
var tenantId = alias.TenantId;
var uploadPath = Path.Combine(_environment.ContentRootPath, "Content", "Tenants",
tenantId.ToString(), "Sites", siteId.ToString(), folderPath);
if (!Directory.Exists(uploadPath)) Directory.CreateDirectory(uploadPath);
var uniqueName = $"{Guid.NewGuid()}{ext}";
var filePath = Path.Combine(uploadPath, uniqueName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
var fileObj = new Oqtane.Models.File
{
FolderId = folder.FolderId,
Name = uniqueName,
Extension = ext.Substring(1),
Size = (int)file.Length,
ImageHeight = 0,
ImageWidth = 0
};
folder = _folders.AddFolder(folder);
try
{
var addedFile = _files.AddFile(fileObj);
return Ok(new { FileId = addedFile.FileId, FileName = file.FileName });
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
"Error saving file record to DB: {Message}. Inner: {Inner}", ex.Message,
ex.InnerException?.Message);
// Critical: This is where we suspect the DbUpdateException
Console.WriteLine($"UPLOAD DB ERROR: {ex.Message} | {ex.InnerException?.Message}");
return BadRequest($"Database error during file registration: {ex.Message}");
}
}
var ext = Path.GetExtension(file.FileName).ToLower();
if (ext != ".pdf") return BadRequest("Invalid file extension");
var tenantId = alias.TenantId;
var uploadPath = Path.Combine(_environment.ContentRootPath, "Content", "Tenants", tenantId.ToString(), "Sites", siteId.ToString(), folderPath);
if (!Directory.Exists(uploadPath)) Directory.CreateDirectory(uploadPath);
var uniqueName = $"{Guid.NewGuid()}{ext}";
var filePath = Path.Combine(uploadPath, uniqueName);
using (var stream = new FileStream(filePath, FileMode.Create))
catch (Exception ex)
{
await file.CopyToAsync(stream);
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "General Upload Error: {Message}",
ex.Message);
return BadRequest(ex.Message);
}
var fileObj = new Oqtane.Models.File
{
FolderId = folder.FolderId,
Name = uniqueName,
Extension = ext.Substring(1),
Size = (int)file.Length,
ImageHeight = 0,
ImageWidth = 0
};
var addedFile = _files.AddFile(fileObj);
return Ok(new { FileId = addedFile.FileId, FileName = file.FileName });
}
return StatusCode((int)HttpStatusCode.Forbidden);
}
@@ -246,71 +371,25 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
{
await _service.ReportApplicationAsync(id, ModuleId, reason);
}
else
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpGet("download/{id}")]
[Authorize]
public async Task<IActionResult> Download(int id, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
var app = await _service.GetApplicationAsync(id, ModuleId);
if (app == null) return NotFound();
var alias = _accessor.HttpContext.Items["Alias"] as Alias;
// Access Rules:
// 1. Admin
if (_accessor.HttpContext.User.IsInRole(RoleNames.Admin))
{
return await ServeFile(app.FileId.Value, app.PdfFileName);
}
// 2. Owner
var username = _accessor.HttpContext.User.Identity.Name;
var currentUserId = -1;
if (username != null)
{
var u = _users.GetUser(username, alias.SiteId);
if (u != null) currentUserId = u.UserId;
}
if (currentUserId == app.UserId)
{
return await ServeFile(app.FileId.Value, app.PdfFileName);
}
// 3. Premium User AND Published/Approved
if (app.Status == "Approved" || app.Status == "Published")
{
var premium = _premiums.GetUserPremium(currentUserId);
if (premium != null && premium.PremiumUntil.HasValue && premium.PremiumUntil.Value > DateTime.UtcNow)
{
return await ServeFile(app.FileId.Value, app.PdfFileName);
}
}
return StatusCode((int)HttpStatusCode.Forbidden);
}
return StatusCode((int)HttpStatusCode.Forbidden);
}
private async Task<IActionResult> ServeFile(int fileId, string downloadName)
{
var file = _files.GetFile(fileId);
if (file != null)
{
var path = _files.GetFilePath(file);
if (System.IO.File.Exists(path))
{
var bytes = await System.IO.File.ReadAllBytesAsync(path);
return File(bytes, "application/pdf", downloadName ?? file.Name);
}
var path = _files.GetFilePath(file);
if (System.IO.File.Exists(path))
{
var bytes = await System.IO.File.ReadAllBytesAsync(path);
return File(bytes, "application/pdf", downloadName ?? file.Name);
}
}
return NotFound();
}
}

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using SZUAbsolventenverein.Module.PremiumArea.Migrations.EntityBuilders;
using SZUAbsolventenverein.Module.PremiumArea.Repository;
namespace SZUAbsolventenverein.Module.PremiumArea.Migrations
{
[DbContext(typeof(PremiumAreaContext))]
[Migration("SZUAbsolventenverein.Module.PremiumArea.01.00.00.01")]
public class AddPremiumTables : MultiDatabaseMigration
{
public AddPremiumTables(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var engAppBuilder = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
engAppBuilder.Create();
var userPremBuilder = new UserPremiumEntityBuilder(migrationBuilder, ActiveDatabase);
userPremBuilder.Create();
var premEventBuilder = new PremiumEventEntityBuilder(migrationBuilder, ActiveDatabase);
premEventBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var engAppBuilder = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
engAppBuilder.Drop();
var userPremBuilder = new UserPremiumEntityBuilder(migrationBuilder, ActiveDatabase);
userPremBuilder.Drop();
var premEventBuilder = new PremiumEventEntityBuilder(migrationBuilder, ActiveDatabase);
premEventBuilder.Drop();
}
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations;
using SZUAbsolventenverein.Module.PremiumArea.Migrations.EntityBuilders;
using SZUAbsolventenverein.Module.PremiumArea.Repository;
namespace SZUAbsolventenverein.Module.PremiumArea.Migrations
{
[DbContext(typeof(PremiumAreaContext))]
[Migration("SZUAbsolventenverein.Module.PremiumArea.01.00.00.02")]
public class RemoveReportingAndMoveToFileManager : MultiDatabaseMigration
{
public RemoveReportingAndMoveToFileManager(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var engAppBuilder = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
engAppBuilder.DropColumn("PdfFileName");
engAppBuilder.DropColumn("IsReported");
engAppBuilder.DropColumn("ReportReason");
engAppBuilder.DropColumn("ReportCount");
engAppBuilder.DropColumn("AdminReviewedBy");
engAppBuilder.DropColumn("AdminReviewedAt");
engAppBuilder.DropColumn("AdminNote");
}
}
}

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
@@ -52,23 +53,59 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
}
else
{
return db.EngineerApplication.AsNoTracking().FirstOrDefault(item => item.ApplicationId == ApplicationId);
return db.EngineerApplication.AsNoTracking()
.FirstOrDefault(item => item.ApplicationId == ApplicationId);
}
}
public EngineerApplication AddEngineerApplication(EngineerApplication EngineerApplication)
{
using var db = _factory.CreateDbContext();
db.EngineerApplication.Add(EngineerApplication);
db.SaveChanges();
try
{
EngineerApplication.CreatedBy = EngineerApplication.CreatedBy ?? "system";
EngineerApplication.CreatedOn = DateTime.UtcNow;
EngineerApplication.ModifiedBy = EngineerApplication.ModifiedBy ?? "system";
EngineerApplication.ModifiedOn = DateTime.UtcNow;
db.EngineerApplication.Add(EngineerApplication);
db.SaveChanges();
}
catch (Exception ex)
{
// Throwing a new exception with more details so it's visible
var msg = $"DB Error: {ex.Message} | Inner: {ex.InnerException?.Message}";
Console.WriteLine(msg); // Log to console for dotnet run
throw new Exception(msg, ex);
}
return EngineerApplication;
}
public EngineerApplication UpdateEngineerApplication(EngineerApplication EngineerApplication)
{
using var db = _factory.CreateDbContext();
db.Entry(EngineerApplication).State = EntityState.Modified;
db.SaveChanges();
try
{
var existing = db.EngineerApplication.Find(EngineerApplication.ApplicationId);
if (existing != null)
{
existing.FileId = EngineerApplication.FileId;
existing.Status = EngineerApplication.Status;
existing.SubmittedOn = EngineerApplication.SubmittedOn;
existing.ApprovedOn = EngineerApplication.ApprovedOn;
existing.ModifiedBy = EngineerApplication.ModifiedBy ?? "system";
existing.ModifiedOn = DateTime.UtcNow;
db.SaveChanges();
}
}
catch (Exception ex)
{
var msg = $"DB Error (Update): {ex.Message} | Inner: {ex.InnerException?.Message}";
Console.WriteLine(msg);
throw new Exception(msg, ex);
}
return EngineerApplication;
}

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
@@ -51,6 +52,10 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
public Models.PremiumArea AddPremiumArea(Models.PremiumArea PremiumArea)
{
using var db = _factory.CreateDbContext();
PremiumArea.CreatedBy = PremiumArea.CreatedBy ?? "system";
PremiumArea.CreatedOn = DateTime.UtcNow;
PremiumArea.ModifiedBy = PremiumArea.ModifiedBy ?? "system";
PremiumArea.ModifiedOn = DateTime.UtcNow;
db.PremiumArea.Add(PremiumArea);
db.SaveChanges();
return PremiumArea;
@@ -59,8 +64,14 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
public Models.PremiumArea UpdatePremiumArea(Models.PremiumArea PremiumArea)
{
using var db = _factory.CreateDbContext();
db.Entry(PremiumArea).State = EntityState.Modified;
db.SaveChanges();
var existing = db.PremiumArea.Find(PremiumArea.PremiumAreaId);
if (existing != null)
{
existing.Name = PremiumArea.Name;
existing.ModifiedBy = PremiumArea.ModifiedBy ?? "system";
existing.ModifiedOn = DateTime.UtcNow;
db.SaveChanges();
}
return PremiumArea;
}

View File

@@ -1,3 +1,4 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
@@ -34,19 +35,35 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
using var db = _factory.CreateDbContext();
if (UserPremium.Id > 0)
{
db.Entry(UserPremium).State = EntityState.Modified;
var existing = db.UserPremium.Find(UserPremium.Id);
if (existing != null)
{
existing.UserId = UserPremium.UserId;
existing.PremiumUntil = UserPremium.PremiumUntil;
existing.ModifiedBy = UserPremium.ModifiedBy ?? "system";
existing.ModifiedOn = DateTime.UtcNow;
db.SaveChanges();
}
}
else
{
UserPremium.CreatedBy = UserPremium.CreatedBy ?? "system";
UserPremium.CreatedOn = DateTime.UtcNow;
UserPremium.ModifiedBy = UserPremium.ModifiedBy ?? "system";
UserPremium.ModifiedOn = DateTime.UtcNow;
db.UserPremium.Add(UserPremium);
db.SaveChanges();
}
db.SaveChanges();
return UserPremium;
}
public void AddPremiumEvent(PremiumEvent premiumEvent)
{
using var db = _factory.CreateDbContext();
premiumEvent.CreatedBy = premiumEvent.CreatedBy ?? "system";
premiumEvent.CreatedOn = DateTime.UtcNow;
premiumEvent.ModifiedBy = premiumEvent.ModifiedBy ?? "system";
premiumEvent.ModifiedOn = DateTime.UtcNow;
db.PremiumEvent.Add(premiumEvent);
db.SaveChanges();
}

View File

@@ -0,0 +1,98 @@
using System;
using SZUAbsolventenverein.Module.PremiumArea.Models;
using SZUAbsolventenverein.Module.PremiumArea.Repository;
using Oqtane.Infrastructure;
using Oqtane.Shared;
using Oqtane.Modules;
using Oqtane.Enums;
namespace SZUAbsolventenverein.Module.PremiumArea.Services
{
public interface IPremiumService
{
bool IsPremium(int userId);
DateTime? GetPremiumUntil(int userId);
void GrantPremium(int userId, int durationMonths, string source, string referenceId = null);
}
public class PremiumService : IPremiumService, ITransientService
{
private readonly IUserPremiumRepository _userPremiumRepository;
private readonly ILogManager _logger;
public PremiumService(IUserPremiumRepository userPremiumRepository, ILogManager logger)
{
_userPremiumRepository = userPremiumRepository;
_logger = logger;
}
public bool IsPremium(int userId)
{
var premium = _userPremiumRepository.GetUserPremium(userId);
return premium != null && premium.PremiumUntil.HasValue && premium.PremiumUntil.Value > DateTime.UtcNow;
}
public DateTime? GetPremiumUntil(int userId)
{
var premium = _userPremiumRepository.GetUserPremium(userId);
return premium?.PremiumUntil;
}
public void GrantPremium(int userId, int durationMonths, string source, string referenceId = null)
{
var current = _userPremiumRepository.GetUserPremium(userId);
var now = DateTime.UtcNow;
DateTime startBase = now;
if (current != null && current.PremiumUntil.HasValue && current.PremiumUntil.Value > now)
{
startBase = current.PremiumUntil.Value;
}
var newUntil = startBase.AddMonths(durationMonths);
// delta days for audit
int deltaDays = (newUntil - (current?.PremiumUntil ?? now)).Days;
// correction: actually we want to know how many days we ADDED to the existing flow.
// If they had 0 days separately, we added durationMonths * 30 approx.
// If they had existing time, we added durationMonths.
// Audit Log usually tracks "What did this action add?". It added 1 year.
// But let's calculate days added relative to "previous state".
// If expired: Added (NewUntil - Now) days.
// If active: Added (NewUntil - OldUntil) = durationMonths roughly.
// Simpler: Just store the DurationMonths converted to days roughly, or the stored delta.
int addedDays = (newUntil - startBase).Days;
if (current == null)
{
current = new UserPremium
{
UserId = userId,
CreatedOn = now,
ModifiedOn = now
};
}
current.PremiumUntil = newUntil;
current.Source = source;
current.ModifiedOn = now;
_userPremiumRepository.SaveUserPremium(current);
// Audit
var audit = new PremiumEvent
{
UserId = userId,
DeltaDays = addedDays,
Source = source,
ReferenceId = referenceId,
CreatedOn = now,
ModifiedOn = now
};
_userPremiumRepository.AddPremiumEvent(audit);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Granted Premium for User {UserId} until {Until}", userId, newUntil);
}
}
}

View File

@@ -22,7 +22,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public ServerEngineerApplicationService(IEngineerApplicationRepository repository, IPremiumService premiumService, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
public ServerEngineerApplicationService(IEngineerApplicationRepository repository,
IPremiumService premiumService, IUserPermissions userPermissions, ITenantManager tenantManager,
ILogManager logger, IHttpContextAccessor accessor)
{
_repository = repository;
_premiumService = premiumService;
@@ -34,42 +36,43 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
public Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId)
{
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) // Admin/Edit
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId).ToList());
}
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId,
PermissionNames.Edit)) // Admin/Edit
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId).ToList());
}
// Check if Premium
if (IsUserPremium(user))
{
// Return Approved AND Published
// Repository GetEngineerApplications(ModuleId, status) usually filters by status.
// If I want both, I might need 2 calls or update Repository.
// Simple approach: Get All and filter here? No, repository likely filters.
// Let's call twice and combine for now, or assume Repository supports "Published".
var approved = _repository.GetEngineerApplications(ModuleId, "Approved");
var published = _repository.GetEngineerApplications(ModuleId, "Published");
return Task.FromResult(approved.Union(published).ToList());
}
// Check if Premium
if (IsUserPremium(user))
{
// Return Approved AND Published
// Repository GetEngineerApplications(ModuleId, status) usually filters by status.
// If I want both, I might need 2 calls or update Repository.
// Simple approach: Get All and filter here? No, repository likely filters.
// Let's call twice and combine for now, or assume Repository supports "Published".
var approved = _repository.GetEngineerApplications(ModuleId, "Approved");
var published = _repository.GetEngineerApplications(ModuleId, "Published");
return Task.FromResult(approved.Union(published).ToList());
}
return Task.FromResult(new List<EngineerApplication>());
return Task.FromResult(new List<EngineerApplication>());
}
public Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId, string status)
{
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId, status).ToList());
}
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId, status).ToList());
}
if ((status == "Approved" || status == "Published") && IsUserPremium(user))
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId, status).ToList());
}
if ((status == "Approved" || status == "Published") && IsUserPremium(user))
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId, status).ToList());
}
return Task.FromResult(new List<EngineerApplication>());
return Task.FromResult(new List<EngineerApplication>());
}
public Task<EngineerApplication> GetApplicationAsync(int ApplicationId, int ModuleId)
@@ -81,7 +84,8 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
var userId = _accessor.HttpContext.GetUserId();
// Allow if Admin OR Owner OR (Premium AND (Approved OR Published))
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit);
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId,
PermissionNames.Edit);
bool isOwner = (userId != -1 && app.UserId == userId);
bool isPremiumViewer = ((app.Status == "Approved" || app.Status == "Published") && IsUserPremium(user));
@@ -100,12 +104,13 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
if (userId == -1) // Not logged in
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Add Attempt (Anonymous)");
return Task.FromResult<EngineerApplication>(null);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Add Attempt (Anonymous)");
return Task.FromResult<EngineerApplication>(null);
}
// Check if allowed to view module (Registered Users usually can View)
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, Application.ModuleId, PermissionNames.View))
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, Application.ModuleId,
PermissionNames.View))
{
Application.UserId = userId;
// Auto-publish if file uploaded (Checked by Status=Published from client)
@@ -120,9 +125,11 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
}
Application = _repository.AddEngineerApplication(Application);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Application Added {Application}", Application);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Application Added {Application}",
Application);
return Task.FromResult(Application);
}
return Task.FromResult<EngineerApplication>(null);
}
@@ -133,31 +140,27 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
var user = _accessor.HttpContext.User;
var userId = _accessor.HttpContext.GetUserId();
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, Application.ModuleId, PermissionNames.Edit);
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, Application.ModuleId,
PermissionNames.Edit);
if (isAdmin || (existing.UserId == userId && existing.Status == "Draft")) // Only owner can edit if Draft
{
if (!isAdmin)
{
Application.Status = existing.Status == "Draft" && Application.Status == "Submitted" ? "Published" : existing.Status; // Auto-publish on submit
Application.Status = existing.Status == "Draft" && Application.Status == "Submitted"
? "Published"
: existing.Status; // Auto-publish on submit
// If client sends "Published" (which Apply.razor does), apply it.
if (Application.Status == "Published" && existing.Status == "Draft")
{
Application.SubmittedOn = DateTime.UtcNow;
Application.ApprovedOn = DateTime.UtcNow;
}
Application.AdminNote = existing.AdminNote;
Application.AdminReviewedBy = existing.AdminReviewedBy;
Application.AdminReviewedAt = existing.AdminReviewedAt;
// Preserve report status if user editing
Application.IsReported = existing.IsReported;
Application.ReportReason = existing.ReportReason;
Application.ReportCount = existing.ReportCount;
}
Application = _repository.UpdateEngineerApplication(Application);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Updated {Application}", Application);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Updated {Application}",
Application);
return Task.FromResult(Application);
}
@@ -167,100 +170,97 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
public Task DeleteApplicationAsync(int ApplicationId, int ModuleId)
{
var existing = _repository.GetEngineerApplication(ApplicationId);
var user = _accessor.HttpContext.User;
var userId = _accessor.HttpContext.GetUserId();
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit);
var user = _accessor.HttpContext.User;
var userId = _accessor.HttpContext.GetUserId();
bool isAdmin = _userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId,
PermissionNames.Edit);
if (existing != null && (isAdmin || existing.UserId == userId))
{
_repository.DeleteEngineerApplication(ApplicationId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "Application Deleted {Id}", ApplicationId);
}
return Task.CompletedTask;
}
// Custom Methods not just CRUD
public Task ApproveApplicationAsync(int ApplicationId, int ModuleId)
{
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
var app = _repository.GetEngineerApplication(ApplicationId);
if (app != null)
{
app.Status = "Approved"; // Keep Approved status for Admin explicitly approving (locking)
app.ApprovedOn = DateTime.UtcNow;
app.AdminReviewedBy = _accessor.HttpContext.GetUserId();
app.AdminReviewedAt = DateTime.UtcNow;
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
var app = _repository.GetEngineerApplication(ApplicationId);
if (app != null)
{
app.Status = "Approved"; // Keep Approved status for Admin explicitly approving (locking)
app.ApprovedOn = DateTime.UtcNow;
// app.ReportReason = null; // Optional: keep history?
// app.ReportCount = 0;
// Clear Reports
app.IsReported = false;
// app.ReportReason = null; // Optional: keep history?
// app.ReportCount = 0;
_repository.UpdateEngineerApplication(app);
_repository.UpdateEngineerApplication(app);
// Grant Premium
_premiumService.GrantPremium(app.UserId, 12, "engineer_application", $"AppId:{app.ApplicationId}");
// Grant Premium
_premiumService.GrantPremium(app.UserId, 12, "engineer_application", $"AppId:{app.ApplicationId}");
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Approved {Id}",
ApplicationId);
}
}
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Approved {Id}", ApplicationId);
}
}
return Task.CompletedTask;
return Task.CompletedTask;
}
public Task RejectApplicationAsync(int ApplicationId, int ModuleId, string Reason)
{
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
var app = _repository.GetEngineerApplication(ApplicationId);
if (app != null)
{
app.Status = "Rejected";
app.AdminNote = Reason;
app.AdminReviewedBy = _accessor.HttpContext.GetUserId();
app.AdminReviewedAt = DateTime.UtcNow;
_repository.UpdateEngineerApplication(app);
var user = _accessor.HttpContext.User;
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
{
var app = _repository.GetEngineerApplication(ApplicationId);
if (app != null)
{
app.Status = "Rejected";
_repository.UpdateEngineerApplication(app);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Rejected {Id}", ApplicationId);
}
}
return Task.CompletedTask;
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Rejected {Id}",
ApplicationId);
}
}
return Task.CompletedTask;
}
public Task ReportApplicationAsync(int ApplicationId, int ModuleId, string Reason)
{
// Allow any View authorized user to report?
// Or only Premium users?
// Users who can VIEW the application can report it.
// Since we restrict View to Premium (or Owner/Admin), we check that.
// Allow any View authorized user to report?
// Or only Premium users?
// Users who can VIEW the application can report it.
// Since we restrict View to Premium (or Owner/Admin), we check that.
// First, get the application to check existence
var app = _repository.GetEngineerApplication(ApplicationId);
if (app == null || app.ModuleId != ModuleId) return Task.CompletedTask;
// First, get the application to check existence
var app = _repository.GetEngineerApplication(ApplicationId);
if (app == null || app.ModuleId != ModuleId) return Task.CompletedTask;
var user = _accessor.HttpContext.User;
var user = _accessor.HttpContext.User;
// Check if user is allowed to View this app
bool canView = false;
// Check if user is allowed to View this app
bool canView = false;
// Admin/Edit
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit)) canView = true;
// Admin/Edit
if (_userPermissions.IsAuthorized(user, _alias.SiteId, EntityNames.Module, ModuleId, PermissionNames.Edit))
canView = true;
// Premium
else if (IsUserPremium(user) && (app.Status == "Approved" || app.Status == "Published")) canView = true;
// Premium
else if (IsUserPremium(user) && (app.Status == "Approved" || app.Status == "Published")) canView = true;
if (canView)
{
app.IsReported = true;
app.ReportReason = Reason;
app.ReportCount++;
_repository.UpdateEngineerApplication(app);
// Send Notification?
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Reported {Id}", ApplicationId);
}
return Task.CompletedTask;
if (canView)
{
_repository.UpdateEngineerApplication(app);
// Send Notification?
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Reported {Id}", ApplicationId);
}
return Task.CompletedTask;
}
private bool IsUserPremium(System.Security.Claims.ClaimsPrincipal user)
@@ -287,42 +287,45 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Services
// Trying standard Oqtane way which is usually:
if (user.Identity.IsAuthenticated)
{
// Using Oqtane.Shared.PrincipalExtensions?
// Let's just try to parse NameIdentifier if GetUserId() is not available in my context.
// But wait, `ModuleControllerBase` has `User` property.
// Here I am in a Service.
// Using Oqtane.Shared.PrincipalExtensions?
// Let's just try to parse NameIdentifier if GetUserId() is not available in my context.
// But wait, `ModuleControllerBase` has `User` property.
// Here I am in a Service.
// I will write a helper to safely get UserId.
var claim = user.Claims.FirstOrDefault(item => item.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
if (claim != null)
{
int.TryParse(claim.Value, out userId);
}
// I will write a helper to safely get UserId.
var claim = user.Claims.FirstOrDefault(item =>
item.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
if (claim != null)
{
int.TryParse(claim.Value, out userId);
}
}
if (userId != -1)
{
return _premiumService.IsPremium(userId);
}
return false;
return false;
}
}
// Quick helper for GetUserId if not present in Usings
public static class ClaimsPrincipalExtensions
{
public static int GetUserId(this HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
var claim = context.User.Claims.FirstOrDefault(item => item.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
public static int GetUserId(this HttpContext context)
{
if (context?.User?.Identity?.IsAuthenticated == true)
{
var claim = context.User.Claims.FirstOrDefault(item =>
item.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
if (claim != null && int.TryParse(claim.Value, out int userId))
{
return userId;
}
}
return -1;
}
}
return -1;
}
}
}

View File

@@ -5,33 +5,28 @@ using Oqtane.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Models
{
[Table("SZUAbsolventenvereinEngineerApplications")]
public class EngineerApplication : ModelBase
{
[Key]
public int ApplicationId { get; set; }
public int UserId { get; set; }
public int ModuleId { get; set; } // Context context
[Table("SZUAbsolventenvereinEngineerApplications")]
public class EngineerApplication : ModelBase
{
[Key] public int ApplicationId { get; set; }
public int UserId { get; set; }
public int ModuleId { get; set; } // Context context
public int FileId { get; set; }
public string PdfFileName { get; set; } = "antrag.pdf"; // Legacy-Spalte, DB ist NOT NULL
[Required]
public int? FileId { get; set; }
// Legacy-Spalten: existieren noch in der DB (Migration lief nicht)
public bool IsReported { get; set; } = false;
public string ReportReason { get; set; }
public int ReportCount { get; set; } = 0;
public int? AdminReviewedBy { get; set; }
public DateTime? AdminReviewedAt { get; set; }
[StringLength(256)]
public string PdfFileName { get; set; }
public string AdminNote { get; set; } = ""; // DB ist NOT NULL
public bool IsReported { get; set; }
public string ReportReason { get; set; }
public int ReportCount { get; set; }
// Status: "Draft", "Submitted", "Approved", "Rejected"
[StringLength(50)] public string Status { get; set; }
// Status: "Draft", "Submitted", "Approved", "Rejected"
[StringLength(50)]
public string Status { get; set; }
public int? AdminReviewedBy { get; set; }
public DateTime? AdminReviewedAt { get; set; }
public string AdminNote { get; set; }
public DateTime? SubmittedOn { get; set; }
public DateTime? ApprovedOn { get; set; }
}
public DateTime? SubmittedOn { get; set; }
public DateTime? ApprovedOn { get; set; }
}
}