Ingenieur Anträge: UI und Services

This commit is contained in:
2026-02-11 10:56:15 +01:00
parent 54f90ea3fb
commit 51b8f1c916
9 changed files with 1311 additions and 58 deletions

View File

@@ -0,0 +1,317 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using SZUAbsolventenverein.Module.PremiumArea.Services;
using Oqtane.Controllers;
using System.Net;
using System.Threading.Tasks;
using SZUAbsolventenverein.Module.PremiumArea.Models;
using System.IO;
using System;
using Oqtane.Models;
using Microsoft.AspNetCore.Hosting;
using Oqtane.Repository;
using Oqtane.Security;
using System.Linq;
using Oqtane.Managers;
using SZUAbsolventenverein.Module.PremiumArea.Repository;
namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class EngineerApplicationController : ModuleControllerBase
{
private readonly IEngineerApplicationService _service;
private readonly IFileRepository _files;
private readonly IFolderRepository _folders;
private readonly IUserManager _users;
private readonly IUserPremiumRepository _premiums;
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)
{
_service = service;
_files = files;
_folders = folders;
_users = users;
_premiums = premiums;
_accessor = accessor;
_environment = environment;
}
// GET: api/<controller>?moduleid=x
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<EngineerApplication>> Get(string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _service.GetApplicationsAsync(ModuleId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Get Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET: api/<controller>/status/Approved?moduleid=x
[HttpGet("status/{status}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<EngineerApplication>> GetByStatus(string status, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _service.GetApplicationsAsync(ModuleId, status);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication GetByStatus Attempt {ModuleId}", moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// GET api/<controller>/5
[HttpGet("{id}")]
[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;
}
}
// POST api/<controller>
[HttpPost]
[Authorize(Policy = PolicyNames.ViewModule)] // Users can Create
public async Task<EngineerApplication> Post([FromBody] EngineerApplication Application)
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, Application.ModuleId))
{
return await _service.AddApplicationAsync(Application);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Post Attempt {Application}", Application);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// PUT api/<controller>/5
[HttpPut("{id}")]
[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))
{
return await _service.UpdateApplicationAsync(Application);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized EngineerApplication Put Attempt {Application}", Application);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// DELETE api/<controller>/5
[HttpDelete("{id}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task Delete(int id, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
await _service.DeleteApplicationAsync(id, ModuleId);
}
else
{
_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)
{
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
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpPost("upload")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IActionResult> Upload(string moduleid)
{
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)
{
// 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);
}
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
};
var addedFile = _files.AddFile(fileObj);
return Ok(new { FileId = addedFile.FileId, FileName = file.FileName });
}
return StatusCode((int)HttpStatusCode.Forbidden);
}
[HttpPost("report/{id}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task Report(int id, [FromBody] string reason, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
await _service.ReportApplicationAsync(id, ModuleId, reason);
}
else
{
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);
}
}
return NotFound();
}
}
}

View File

@@ -0,0 +1,328 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Security;
using Oqtane.Shared;
using SZUAbsolventenverein.Module.PremiumArea.Models;
using SZUAbsolventenverein.Module.PremiumArea.Repository;
namespace SZUAbsolventenverein.Module.PremiumArea.Services
{
public class ServerEngineerApplicationService : IEngineerApplicationService
{
private readonly IEngineerApplicationRepository _repository;
private readonly IPremiumService _premiumService;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly Alias _alias;
public ServerEngineerApplicationService(IEngineerApplicationRepository repository, IPremiumService premiumService, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_repository = repository;
_premiumService = premiumService;
_userPermissions = userPermissions;
_logger = logger;
_accessor = accessor;
_alias = tenantManager.GetAlias();
}
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());
}
// 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>());
}
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());
}
if ((status == "Approved" || status == "Published") && IsUserPremium(user))
{
return Task.FromResult(_repository.GetEngineerApplications(ModuleId, status).ToList());
}
return Task.FromResult(new List<EngineerApplication>());
}
public Task<EngineerApplication> GetApplicationAsync(int ApplicationId, int ModuleId)
{
var app = _repository.GetEngineerApplication(ApplicationId);
if (app == null || app.ModuleId != ModuleId) return Task.FromResult<EngineerApplication>(null);
var user = _accessor.HttpContext.User;
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 isOwner = (userId != -1 && app.UserId == userId);
bool isPremiumViewer = ((app.Status == "Approved" || app.Status == "Published") && IsUserPremium(user));
if (isAdmin || isOwner || isPremiumViewer)
{
return Task.FromResult(app);
}
return Task.FromResult<EngineerApplication>(null);
}
public Task<EngineerApplication> AddApplicationAsync(EngineerApplication Application)
{
var user = _accessor.HttpContext.User;
var userId = _accessor.HttpContext.GetUserId();
if (userId == -1) // Not logged in
{
_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))
{
Application.UserId = userId;
// Auto-publish if file uploaded (Checked by Status=Published from client)
// If client sends "Published", we accept it.
if (string.IsNullOrEmpty(Application.Status)) Application.Status = "Draft";
// Set ApprovedOn if Published? user asked for removal of admin approval.
if (Application.Status == "Published")
{
Application.SubmittedOn = DateTime.UtcNow;
Application.ApprovedOn = DateTime.UtcNow; // Effectively approved.
}
Application = _repository.AddEngineerApplication(Application);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Application Added {Application}", Application);
return Task.FromResult(Application);
}
return Task.FromResult<EngineerApplication>(null);
}
public Task<EngineerApplication> UpdateApplicationAsync(EngineerApplication Application)
{
var existing = _repository.GetEngineerApplication(Application.ApplicationId);
if (existing == null) return Task.FromResult<EngineerApplication>(null);
var user = _accessor.HttpContext.User;
var userId = _accessor.HttpContext.GetUserId();
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
// 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);
return Task.FromResult(Application);
}
return Task.FromResult<EngineerApplication>(null);
}
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);
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;
// Clear Reports
app.IsReported = false;
// app.ReportReason = null; // Optional: keep history?
// app.ReportCount = 0;
_repository.UpdateEngineerApplication(app);
// Grant Premium
_premiumService.GrantPremium(app.UserId, 12, "engineer_application", $"AppId:{app.ApplicationId}");
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Approved {Id}", ApplicationId);
}
}
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);
_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.
// 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;
// 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;
// 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;
}
private bool IsUserPremium(System.Security.Claims.ClaimsPrincipal user)
{
// Oqtane's GetUserId() extension returns -1 if not found.
// We need to parse User object myself or use "User.Identity.Name" to find user if needed.
// But _premiumService.IsPremium(userId) asks for Int.
// I'll assume I can get UserId.
// Since Oqtane is weird with Claims sometimes, I'll use the Helper Extension `GetUserId()`.
// But wait, "GetUserId" is an extension on HttpContext or ClaimsPrincipal?
// In Oqtane.Shared.UserSecurity class? Or Oqtane.Extensions?
// Usually `_accessor.HttpContext.GlobalUserId()` ??
// Let's rely on standard DI/Context.
// NOTE: IsUserPremium check requires querying the DB via PremiumService.
// This could be expensive on every list call. Caching might be better but for now DB is fine.
// To get UserId from ClaimsPrincipal:
// var userId = int.Parse(user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value);
// I will use `_accessor.HttpContext.GetUserId()` from `Oqtane.Infrastructure` or similar if available.
// Since I am already using `_accessor`, I'll rely on Oqtane extensions being available.
int userId = -1;
// 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.
// 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;
}
}
// 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);
if (claim != null && int.TryParse(claim.Value, out int userId))
{
return userId;
}
}
return -1;
}
}
}

View File

@@ -22,6 +22,8 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IPremiumAreaService, ServerPremiumAreaService>();
services.AddTransient<IEngineerApplicationService, ServerEngineerApplicationService>();
services.AddDbContextFactory<PremiumAreaContext>(opt => { }, ServiceLifetime.Transient);
}
}