4 Commits

28 changed files with 2134 additions and 65 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
**/bin/
**/obj/

View File

@@ -0,0 +1,179 @@
@using SZUAbsolventenverein.Module.PremiumArea.Services
@using SZUAbsolventenverein.Module.PremiumArea.Models
@namespace SZUAbsolventenverein.Module.PremiumArea
@inherits ModuleBase
@inject IEngineerApplicationService ApplicationService
@inject NavigationManager NavManager
<h3>Ingenieur-Anträge Prüfen</h3>
@if (_applications == null)
{
<p>Laden...</p>
}
else
{
<div class="row">
<div class="col-md-7">
<div class="mb-3">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="statusfilter" id="filterValidation" autocomplete="off" checked="@(_filterStatus == "Validation")" @onchange="@(() => _filterStatus = "Validation")">
<label class="btn btn-outline-primary" for="filterValidation">Validierung (Entwurf/Eingereicht)</label>
<input type="radio" class="btn-check" name="statusfilter" id="filterApproved" autocomplete="off" checked="@(_filterStatus == "Approved")" @onchange="@(() => _filterStatus = "Approved")">
<label class="btn btn-outline-success" for="filterApproved">Genehmigt</label>
<input type="radio" class="btn-check" name="statusfilter" id="filterRejected" autocomplete="off" checked="@(_filterStatus == "Rejected")" @onchange="@(() => _filterStatus = "Rejected")">
<label class="btn btn-outline-danger" for="filterRejected">Abgelehnt</label>
<input type="radio" class="btn-check" name="statusfilter" id="filterReported" autocomplete="off" checked="@(_filterStatus == "Reported")" @onchange="@(() => _filterStatus = "Reported")">
<label class="btn btn-outline-warning" for="filterReported">Gemeldet</label>
</div>
</div>
@if (FilteredApplications.Count == 0)
{
<p>Keine Anträge gefunden.</p>
}
else
{
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Benutzer ID</th>
<th>Dateiname</th>
<th>Datum</th>
<th>Status</th>
@if (_filterStatus == "Reported")
{
<th>Meldegrund</th>
}
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@foreach (var app in FilteredApplications)
{
<tr @onclick="@(() => SelectApp(app))" style="cursor: pointer;" class="@(_selectedApp == app ? "table-active" : "")">
<td>@app.UserId</td>
<td>@app.PdfFileName</td>
<td>@(app.SubmittedOn?.ToShortDateString() ?? app.CreatedOn.ToShortDateString())</td>
<td>
@app.Status
@if(app.IsReported) { <span class="badge bg-warning text-dark">Gemeldet</span> }
</td>
@if (_filterStatus == "Reported")
{
<td class="text-danger">@app.ReportReason</td>
}
<td>
<button class="btn btn-sm btn-primary" @onclick="@((e) => SelectApp(app))">Prüfen</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
<div class="col-md-5">
@if (_selectedApp != null)
{
<div class="card">
<div class="card-header">
Antragsdetails
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Benutzer ID</dt>
<dd class="col-sm-8">@_selectedApp.UserId</dd>
<dt class="col-sm-4">Datei</dt>
<dd class="col-sm-8">@_selectedApp.PdfFileName</dd>
<dt class="col-sm-4">Status</dt>
<dd class="col-sm-8">@_selectedApp.Status</dd>
@if (_selectedApp.IsReported)
{
<dt class="col-sm-4 text-danger">Meldegrund</dt>
<dd class="col-sm-8 text-danger">@_selectedApp.ReportReason (Anzahl: @_selectedApp.ReportCount)</dd>
}
</dl>
<div class="mb-3">
<div class="ratio ratio-16x9">
<iframe src="@((NavManager.BaseUri + "api/engineerapplication") + "/download/" + _selectedApp.ApplicationId + "?moduleid=" + ModuleState.ModuleId)" allowfullscreen></iframe>
</div>
</div>
<div class="d-grid gap-2">
@if (_selectedApp.Status != "Approved" || _selectedApp.IsReported)
{
<button class="btn @(_selectedApp.IsReported ? "btn-warning" : "btn-success")" @onclick="ApproveApp">
@(_selectedApp.IsReported ? "Meldung verwerfen / Behalten" : "Genehmigen & Premium gewähren")
</button>
}
@if (_selectedApp.Status != "Rejected")
{
<button class="btn btn-danger" @onclick="RejectApp">Ablehnen / Löschen</button>
}
</div>
</div>
</div>
}
</div>
</div>
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; // Admin Only
private List<EngineerApplication> _applications;
private EngineerApplication _selectedApp;
private string _filterStatus = "Validation";
protected override async Task OnInitializedAsync()
{
await LoadApps();
}
private async Task LoadApps()
{
// Load All applications
_applications = await ApplicationService.GetApplicationsAsync(ModuleState.ModuleId);
}
private List<EngineerApplication> FilteredApplications
{
get
{
if (_applications == null) return new List<EngineerApplication>();
if (_filterStatus == "Validation")
return _applications.Where(a => a.Status == "Draft" || a.Status == "Submitted" || a.Status == "Published").ToList();
if (_filterStatus == "Reported")
return _applications.Where(a => a.IsReported).ToList();
return _applications.Where(a => a.Status == _filterStatus).ToList();
}
}
private void SelectApp(EngineerApplication app)
{
_selectedApp = app;
}
private async Task ApproveApp()
{
if (_selectedApp == null) return;
await ApplicationService.ApproveApplicationAsync(_selectedApp.ApplicationId, ModuleState.ModuleId);
await LoadApps();
_selectedApp = null;
}
private async Task RejectApp()
{
if (_selectedApp == null) return;
// Basic rejection without custom reason for now since UI input was removed
await ApplicationService.RejectApplicationAsync(_selectedApp.ApplicationId, ModuleState.ModuleId, "Abgelehnt durch Admin");
await LoadApps();
_selectedApp = null;
}
}

View File

@@ -0,0 +1,156 @@
@using SZUAbsolventenverein.Module.PremiumArea.Services
@using SZUAbsolventenverein.Module.PremiumArea.Models
@namespace SZUAbsolventenverein.Module.PremiumArea
@inherits ModuleBase
@inject IEngineerApplicationService ApplicationService
@inject NavigationManager NavManager
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, "Premium Member"))
{
<h3>Genehmigte Ingenieur-Anträge</h3>
@if (_applications == null)
{
<p>Prüfe Premium-Zugang...</p>
}
else if (_applications.Count == 0)
{
<div class="alert alert-warning">
Keine genehmigten Anträge gefunden.
</div>
}
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>
</div>
</div>
</div>
</div>
}
</div>
}
}
else
{
<div class="alert alert-warning">
Sie müssen Premium Kunde sein um diese Funktion zu nutzen.
</div>
}
@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>
</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>
}
@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>
</div>
</div>
<div class="modal-backdrop fade show"></div>
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private List<EngineerApplication> _applications;
private EngineerApplication _selectedApp;
private EngineerApplication _reportApp;
private string _reportReason;
protected override async Task OnInitializedAsync()
{
try
{
var published = await ApplicationService.GetApplicationsAsync(ModuleState.ModuleId, "Published");
var approved = await ApplicationService.GetApplicationsAsync(ModuleState.ModuleId, "Approved");
_applications = new List<EngineerApplication>();
if (published != null) _applications.AddRange(published);
if (approved != null) _applications.AddRange(approved);
_applications = _applications.GroupBy(a => a.ApplicationId).Select(g => g.First()).ToList();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
_applications = null;
}
}
private void ShowDetail(EngineerApplication app)
{
_selectedApp = app;
}
private void InitReport(EngineerApplication app)
{
_reportApp = app;
_reportReason = "";
}
private async Task SubmitReport()
{
if (_reportApp == null || string.IsNullOrWhiteSpace(_reportReason)) return;
try
{
await ApplicationService.ReportApplicationAsync(_reportApp.ApplicationId, ModuleState.ModuleId, _reportReason);
_reportApp = null;
AddModuleMessage("Antrag erfolgreich gemeldet.", MessageType.Success);
}
catch (Exception ex)
{
AddModuleMessage("Fehler beim Melden: " + ex.Message, MessageType.Error);
}
}
}

View File

@@ -0,0 +1,242 @@
@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))
{
<div class="alert alert-info">@Message</div>
}
</div>
</div>
@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="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>
</div>
}
else
{
<div class="alert alert-warning">
Antrags-Status: <strong>@_existingApp.Status</strong>. Sie können ihn derzeit nicht bearbeiten.
</div>
}
}
else
{
@if (_existingApp != null)
{
<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>
}
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private EngineerApplication _existingApp;
private IBrowserFile _selectedFile;
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);
if (_existingApp != null)
{
ShowForm = false;
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private void EditApp()
{
ShowForm = true;
}
private void LoadFiles(InputFileChangeEventArgs e)
{
_selectedFile = e.File;
Message = "";
}
private async Task SubmitApplication()
{
if (_selectedFile == null && _existingApp?.FileId == null)
{
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,
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;
}
else
{
await ApplicationService.UpdateApplicationAsync(app);
_existingApp = await ApplicationService.GetApplicationAsync(app.ApplicationId, ModuleState.ModuleId);
}
ShowForm = false;
Message = "Antrag erfolgreich gesendet.";
}
catch (Exception ex)
{
Message = "Fehler: " + ex.Message;
}
}
private void Cancel()
{
NavManager.NavigateTo(NavigateUrl());
}
private string GetStatusColor(string status)
{
return status switch
{
"Approved" => "success",
"Published" => "success", // Green for Published too
"Rejected" => "danger",
"Submitted" => "info",
_ => "secondary"
};
}
private class UploadResult
{
public int FileId { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -1,5 +1,7 @@
@using SZUAbsolventenverein.Module.PremiumArea.Services @using SZUAbsolventenverein.Module.PremiumArea.Services
@using SZUAbsolventenverein.Module.PremiumArea.Models @using SZUAbsolventenverein.Module.PremiumArea.Models
@using Oqtane.Security
@using Oqtane.Shared
@namespace SZUAbsolventenverein.Module.PremiumArea @namespace SZUAbsolventenverein.Module.PremiumArea
@inherits ModuleBase @inherits ModuleBase
@@ -7,35 +9,15 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@if (_PremiumAreas == null) <div class="mb-3">
{ <ActionLink Action="Apply" Text="Ingenieur Antrag hochladen" />
<p><em>Loading...</em></p> <ActionLink Action="UserSearch" Text="Mitglieder finden" />
} @if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
else
{
<ActionLink Action="Add" Security="SecurityAccessLevel.Edit" Text="Add PremiumArea" ResourceKey="Add" />
<br />
<br />
@if (@_PremiumAreas.Count != 0)
{ {
<Pager Items="@_PremiumAreas"> <ActionLink Action="AdminReview" Text="Admin Bereich" Security="SecurityAccessLevel.Edit" />
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Name"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Parameters="@($"id=" + context.PremiumAreaId.ToString())" ResourceKey="Edit" /></td>
<td><ActionDialog Header="Delete PremiumArea" Message="Are You Sure You Wish To Delete This PremiumArea?" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await Delete(context))" ResourceKey="Delete" Id="@context.PremiumAreaId.ToString()" /></td>
<td>@context.Name</td>
</Row>
</Pager>
} }
else <ActionLink Action="ApplicationList" Text="Liste alle Ingenieur Anträge" />
{ </div>
<p>@Localizer["Message.DisplayNone"]</p>
}
}
@code { @code {
public override string RenderMode => RenderModes.Static; public override string RenderMode => RenderModes.Static;
@@ -45,35 +27,4 @@ else
new Stylesheet("_content/SZUAbsolventenverein.Module.PremiumArea/Module.css"), new Stylesheet("_content/SZUAbsolventenverein.Module.PremiumArea/Module.css"),
new Script("_content/SZUAbsolventenverein.Module.PremiumArea/Module.js") new Script("_content/SZUAbsolventenverein.Module.PremiumArea/Module.js")
}; };
List<PremiumArea> _PremiumAreas;
protected override async Task OnInitializedAsync()
{
try
{
_PremiumAreas = await PremiumAreaService.GetPremiumAreasAsync(ModuleState.ModuleId);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Loading PremiumArea {Error}", ex.Message);
AddModuleMessage(Localizer["Message.LoadError"], MessageType.Error);
}
}
private async Task Delete(PremiumArea PremiumArea)
{
try
{
await PremiumAreaService.DeletePremiumAreaAsync(PremiumArea.PremiumAreaId, ModuleState.ModuleId);
await logger.LogInformation("PremiumArea Deleted {PremiumArea}", PremiumArea);
_PremiumAreas = await PremiumAreaService.GetPremiumAreasAsync(ModuleState.ModuleId);
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Deleting PremiumArea {PremiumArea} {Error}", PremiumArea, ex.Message);
AddModuleMessage(Localizer["Message.DeleteError"], MessageType.Error);
}
}
} }

View File

@@ -9,9 +9,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea
{ {
Name = "PremiumArea", Name = "PremiumArea",
Description = "This module adds a premium member system to Octane. Users receive premium status after completing a payment. Premium members get access to exclusive features and content.", Description = "This module adds a premium member system to Octane. Users receive premium status after completing a payment. Premium members get access to exclusive features and content.",
Version = "1.0.0", Version = "1.0.2",
ServerManagerType = "SZUAbsolventenverein.Module.PremiumArea.Manager.PremiumAreaManager, SZUAbsolventenverein.Module.PremiumArea.Server.Oqtane", ServerManagerType = "SZUAbsolventenverein.Module.PremiumArea.Manager.PremiumAreaManager, SZUAbsolventenverein.Module.PremiumArea.Server.Oqtane",
ReleaseVersions = "1.0.0", ReleaseVersions = "1.0.0,1.0.1,1.0.2",
Dependencies = "SZUAbsolventenverein.Module.PremiumArea.Shared.Oqtane", Dependencies = "SZUAbsolventenverein.Module.PremiumArea.Shared.Oqtane",
PackageName = "SZUAbsolventenverein.Module.PremiumArea" PackageName = "SZUAbsolventenverein.Module.PremiumArea"
}; };

View File

@@ -0,0 +1,107 @@
@using SZUAbsolventenverein.Module.PremiumArea.Services
@using Oqtane.Models
@namespace SZUAbsolventenverein.Module.PremiumArea
@inherits ModuleBase
@inject IUserContactService ContactService
@inject NavigationManager NavigationManager
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, "Premium Member"))
{
<h3>Mitglieder Suche</h3>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Mitglieder suchen (min 3 Zeichen)..." @bind="_query" @onkeyup="@(e => { if (e.Key == "Enter") Search(); })" />
<button class="btn btn-primary" @onclick="Search">Suchen</button>
</div>
@if (_searchResults != null)
{
@if (_searchResults.Count == 0)
{
<p class="text-muted">Keine Mitglieder gefunden.</p>
}
else
{
<ul class="list-group">
@foreach (var user in _searchResults)
{
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<strong>@user.DisplayName</strong> <small class="text-muted">(@user.Username)</small>
</span>
<button class="btn btn-sm btn-outline-info" @onclick="@(() => InitContact(user))">Kontaktieren</button>
</li>
}
</ul>
}
}
@if (_selectedUser != null)
{
<div class="card mt-4">
<div class="card-header">Nachricht an: @_selectedUser.DisplayName</div>
<div class="card-body">
<div class="mb-3">
<label>Nachricht</label>
<textarea class="form-control" rows="3" @bind="_messageBody"></textarea>
</div>
<button class="btn btn-primary" @onclick="Send">Nachricht senden</button>
<button class="btn btn-secondary" @onclick="@(() => _selectedUser = null)">Abbrechen</button>
@if (!string.IsNullOrEmpty(_statusMsg))
{
<div class="alert alert-info mt-2">@_statusMsg</div>
}
</div>
</div>
}
}
else
{
<div class="alert alert-warning">
Sie müssen Premium Kunde sein um diese Funktion zu nutzen.
</div>
}
@code {
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
private string _query;
private List<User> _searchResults;
private User _selectedUser;
private string _messageBody;
private string _statusMsg;
private async Task Search()
{
if (string.IsNullOrWhiteSpace(_query) || _query.Length < 3) return;
_searchResults = await ContactService.SearchUsersAsync(_query, ModuleState.ModuleId);
_selectedUser = null;
}
private void InitContact(User user)
{
_selectedUser = user;
_messageBody = "";
_statusMsg = "";
}
private async Task Send()
{
if (string.IsNullOrWhiteSpace(_messageBody)) return;
try
{
await ContactService.SendMessageAsync(_selectedUser.UserId, _messageBody, ModuleState.ModuleId);
_statusMsg = "Message Sent Successully!";
// Reset after delay or allow closing
await Task.Delay(2000);
_selectedUser = null;
StateHasChanged();
}
catch (Exception ex)
{
_statusMsg = "Error sending message: " + ex.Message;
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Services;
using Oqtane.Shared;
using SZUAbsolventenverein.Module.PremiumArea.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Services
{
public interface IEngineerApplicationService
{
Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId);
Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId, string status);
Task<EngineerApplication> GetApplicationAsync(int ApplicationId, int ModuleId);
Task<EngineerApplication> AddApplicationAsync(EngineerApplication Application);
Task<EngineerApplication> UpdateApplicationAsync(EngineerApplication Application);
Task DeleteApplicationAsync(int ApplicationId, int ModuleId);
Task ApproveApplicationAsync(int ApplicationId, int ModuleId);
Task RejectApplicationAsync(int ApplicationId, int ModuleId, string Reason);
Task ReportApplicationAsync(int ApplicationId, int ModuleId, string Reason);
}
public class EngineerApplicationService : ServiceBase, IEngineerApplicationService
{
public EngineerApplicationService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("EngineerApplication");
public async Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId)
{
return await GetJsonAsync<List<EngineerApplication>>(CreateAuthorizationPolicyUrl($"{Apiurl}?moduleid={ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<List<EngineerApplication>> GetApplicationsAsync(int ModuleId, string status)
{
return await GetJsonAsync<List<EngineerApplication>>(CreateAuthorizationPolicyUrl($"{Apiurl}/status/{status}?moduleid={ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<EngineerApplication> GetApplicationAsync(int ApplicationId, int ModuleId)
{
return await GetJsonAsync<EngineerApplication>(CreateAuthorizationPolicyUrl($"{Apiurl}/{ApplicationId}?moduleid={ModuleId}", EntityNames.Module, ModuleId));
}
public async Task<EngineerApplication> AddApplicationAsync(EngineerApplication Application)
{
return await PostJsonAsync<EngineerApplication>(CreateAuthorizationPolicyUrl($"{Apiurl}", EntityNames.Module, Application.ModuleId), Application);
}
public async Task<EngineerApplication> UpdateApplicationAsync(EngineerApplication Application)
{
return await PutJsonAsync<EngineerApplication>(CreateAuthorizationPolicyUrl($"{Apiurl}/{Application.ApplicationId}", EntityNames.Module, Application.ModuleId), Application);
}
public async Task DeleteApplicationAsync(int ApplicationId, int ModuleId)
{
await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{ApplicationId}?moduleid={ModuleId}", EntityNames.Module, ModuleId));
}
public async Task ApproveApplicationAsync(int ApplicationId, int ModuleId)
{
await PostAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/approve/{ApplicationId}?moduleid={ModuleId}", EntityNames.Module, ModuleId));
}
public async Task RejectApplicationAsync(int ApplicationId, int ModuleId, string Reason)
{
await PostAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/reject/{ApplicationId}?moduleid={ModuleId}&reason={System.Net.WebUtility.UrlEncode(Reason)}", EntityNames.Module, ModuleId));
}
public async Task ReportApplicationAsync(int ApplicationId, int ModuleId, string Reason)
{
await PostJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/report/{ApplicationId}?moduleid={ModuleId}", EntityNames.Module, ModuleId), Reason);
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Models;
using Oqtane.Services;
using Oqtane.Shared;
namespace SZUAbsolventenverein.Module.PremiumArea.Services
{
public interface IUserContactService
{
Task<List<User>> SearchUsersAsync(string query, int moduleId);
Task SendMessageAsync(int recipientUserId, string message, int moduleId);
}
public class UserContactService : ServiceBase, IUserContactService
{
public UserContactService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("UserContact");
public async Task<List<User>> SearchUsersAsync(string query, int moduleId)
{
return await GetJsonAsync<List<User>>(CreateAuthorizationPolicyUrl($"{Apiurl}/search/{query}?moduleid={moduleId}", EntityNames.Module, moduleId));
}
public async Task SendMessageAsync(int recipientUserId, string message, int moduleId)
{
await PostAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/send?recipientId={recipientUserId}&moduleid={moduleId}&message={System.Net.WebUtility.UrlEncode(message)}", EntityNames.Module, moduleId));
}
}
}

View File

@@ -13,6 +13,14 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup
{ {
services.AddScoped<IPremiumAreaService, PremiumAreaService>(); services.AddScoped<IPremiumAreaService, PremiumAreaService>();
} }
if (!services.Any(s => s.ServiceType == typeof(IEngineerApplicationService)))
{
services.AddScoped<IEngineerApplicationService, EngineerApplicationService>();
}
if (!services.Any(s => s.ServiceType == typeof(IUserContactService)))
{
services.AddScoped<IUserContactService, UserContactService>();
}
} }
} }
} }

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata> <metadata>
<id>$ProjectName$</id> <id>$projectname$</id>
<version>1.0.0</version> <version>1.0.0</version>
<authors>SZUAbsolventenverein</authors> <authors>SZUAbsolventenverein</authors>
<owners>SZUAbsolventenverein</owners> <owners>SZUAbsolventenverein</owners>

View File

@@ -1,7 +1,7 @@
TargetFramework=$1 TargetFramework=$1
ProjectName=$2 ProjectName=$2
find . -name *.nupkg -delete find . -name "*.nupkg" -delete
dotnet run --project ../../fixProps/FixProps/FixProps.csproj "..\..\oqtane.framework\oqtane.package\FixProps.exe"
dotnet pack $ProjectName.nuspec "/p:targetframework=${TargetFramework};ProjectName=${ProjectName}" "..\..\oqtane.framework\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName%
cp -f *.nupkg ../../oqtane.framework/Oqtane.Server/Packages/ cp -f "*.nupkg" "..\..\oqtane.framework\Oqtane.Server\Packages\"

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,59 @@
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 Oqtane.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Controllers
{
[Route(ControllerRoutes.ApiRoute)]
public class UserContactController : ModuleControllerBase
{
private readonly IUserContactService _service;
public UserContactController(IUserContactService service, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{
_service = service;
}
// GET: api/<controller>/search/query?moduleid=x
[HttpGet("search/{query}")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IEnumerable<User>> Search(string query, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
return await _service.SearchUsersAsync(query, ModuleId);
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null;
}
}
// POST: api/<controller>/send
[HttpPost("send")]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task Send(int recipientId, string message, string moduleid)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
await _service.SendMessageAsync(recipientId, message, ModuleId);
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
}
}

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,90 @@
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;
using System;
namespace SZUAbsolventenverein.Module.PremiumArea.Migrations
{
[DbContext(typeof(PremiumAreaContext))]
[Migration("SZUAbsolventenverein.Module.PremiumArea.01.00.00.02")]
public class AddReportAndFileColumns : MultiDatabaseMigration
{
public AddReportAndFileColumns(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
// Add FileId (nullable int) - assuming missing
migrationBuilder.AddColumn<int>(
name: "FileId",
table: "SZUAbsolventenvereinEngineerApplications",
nullable: true);
// Add PdfFileName (string 256)
migrationBuilder.AddColumn<string>(
name: "PdfFileName",
table: "SZUAbsolventenvereinEngineerApplications",
maxLength: 256,
nullable: true);
// Add ApprovedOn (DateTime nullable) - might exist but adding if missing?
// MigrationBuilder will fail if exists. We assume schema drift needs this.
// If it exists, user must handle.
migrationBuilder.AddColumn<DateTime>(
name: "ApprovedOn",
table: "SZUAbsolventenvereinEngineerApplications",
nullable: true);
// Add IsReported (bool not null default false)
migrationBuilder.AddColumn<bool>(
name: "IsReported",
table: "SZUAbsolventenvereinEngineerApplications",
nullable: false,
defaultValue: false);
// Add ReportReason (string max nullable)
migrationBuilder.AddColumn<string>(
name: "ReportReason",
table: "SZUAbsolventenvereinEngineerApplications",
nullable: true);
// Add ReportCount (int not null default 0)
migrationBuilder.AddColumn<int>(
name: "ReportCount",
table: "SZUAbsolventenvereinEngineerApplications",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsReported",
table: "SZUAbsolventenvereinEngineerApplications");
migrationBuilder.DropColumn(
name: "ReportReason",
table: "SZUAbsolventenvereinEngineerApplications");
migrationBuilder.DropColumn(
name: "ReportCount",
table: "SZUAbsolventenvereinEngineerApplications");
migrationBuilder.DropColumn(
name: "FileId",
table: "SZUAbsolventenvereinEngineerApplications");
migrationBuilder.DropColumn(
name: "PdfFileName",
table: "SZUAbsolventenvereinEngineerApplications");
migrationBuilder.DropColumn(
name: "ApprovedOn",
table: "SZUAbsolventenvereinEngineerApplications");
}
}
}

View File

@@ -0,0 +1,61 @@
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;
using SZUAbsolventenverein.Module.PremiumArea.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Migrations.EntityBuilders
{
public class EngineerApplicationEntityBuilder : AuditableBaseEntityBuilder<EngineerApplicationEntityBuilder>
{
private const string _entityTableName = "SZUAbsolventenvereinEngineerApplications";
private readonly PrimaryKey<EngineerApplicationEntityBuilder> _primaryKey = new("PK_SZUAbsolventenvereinEngineerApplications", x => x.ApplicationId);
private readonly ForeignKey<EngineerApplicationEntityBuilder> _moduleForeignKey = new("FK_SZUAbsolventenvereinEngineerApplications_Module", x => x.ModuleId, "Module", "ModuleId", ReferentialAction.Cascade);
public EngineerApplicationEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_moduleForeignKey);
}
protected override EngineerApplicationEntityBuilder BuildTable(ColumnsBuilder table)
{
ApplicationId = AddAutoIncrementColumn(table, "ApplicationId");
UserId = AddIntegerColumn(table, "UserId");
ModuleId = AddIntegerColumn(table, "ModuleId");
FileId = AddIntegerColumn(table, "FileId", true);
PdfFileName = AddStringColumn(table, "PdfFileName", 256);
Status = AddStringColumn(table, "Status", 50);
AdminReviewedBy = AddIntegerColumn(table, "AdminReviewedBy", true);
AdminReviewedAt = AddDateTimeColumn(table, "AdminReviewedAt", true);
AdminNote = AddMaxStringColumn(table, "AdminNote");
SubmittedOn = AddDateTimeColumn(table, "SubmittedOn", true);
ApprovedOn = AddDateTimeColumn(table, "ApprovedOn", true);
IsReported = AddBooleanColumn(table, "IsReported", false);
ReportReason = AddMaxStringColumn(table, "ReportReason", true);
ReportCount = AddIntegerColumn(table, "ReportCount", false);
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> ApplicationId { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
public OperationBuilder<AddColumnOperation> ModuleId { get; set; }
public OperationBuilder<AddColumnOperation> FileId { get; set; }
public OperationBuilder<AddColumnOperation> PdfFileName { get; set; }
public OperationBuilder<AddColumnOperation> Status { get; set; }
public OperationBuilder<AddColumnOperation> AdminReviewedBy { get; set; }
public OperationBuilder<AddColumnOperation> AdminReviewedAt { get; set; }
public OperationBuilder<AddColumnOperation> AdminNote { get; set; }
public OperationBuilder<AddColumnOperation> SubmittedOn { get; set; }
public OperationBuilder<AddColumnOperation> ApprovedOn { get; set; }
public OperationBuilder<AddColumnOperation> IsReported { get; set; }
public OperationBuilder<AddColumnOperation> ReportReason { get; set; }
public OperationBuilder<AddColumnOperation> ReportCount { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
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.PremiumArea.Migrations.EntityBuilders
{
public class PremiumEventEntityBuilder : AuditableBaseEntityBuilder<PremiumEventEntityBuilder>
{
private const string _entityTableName = "SZUAbsolventenvereinPremiumEvents";
private readonly PrimaryKey<PremiumEventEntityBuilder> _primaryKey = new("PK_SZUAbsolventenvereinPremiumEvents", x => x.Id);
public PremiumEventEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override PremiumEventEntityBuilder BuildTable(ColumnsBuilder table)
{
Id = AddAutoIncrementColumn(table, "Id");
UserId = AddIntegerColumn(table, "UserId");
DeltaDays = AddIntegerColumn(table, "DeltaDays");
Source = AddStringColumn(table, "Source", 50);
ReferenceId = AddMaxStringColumn(table, "ReferenceId");
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> Id { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
public OperationBuilder<AddColumnOperation> DeltaDays { get; set; }
public OperationBuilder<AddColumnOperation> Source { get; set; }
public OperationBuilder<AddColumnOperation> ReferenceId { get; set; }
}
}

View File

@@ -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.PremiumArea.Migrations.EntityBuilders
{
public class UserPremiumEntityBuilder : AuditableBaseEntityBuilder<UserPremiumEntityBuilder>
{
private const string _entityTableName = "SZUAbsolventenvereinUserPremium";
private readonly PrimaryKey<UserPremiumEntityBuilder> _primaryKey = new("PK_SZUAbsolventenvereinUserPremium", x => x.Id);
public UserPremiumEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override UserPremiumEntityBuilder BuildTable(ColumnsBuilder table)
{
Id = AddAutoIncrementColumn(table, "Id");
UserId = AddIntegerColumn(table, "UserId");
PremiumUntil = AddDateTimeColumn(table, "PremiumUntil", true);
Source = AddStringColumn(table, "Source", 50);
AddAuditableColumns(table);
return this;
}
public OperationBuilder<AddColumnOperation> Id { get; set; }
public OperationBuilder<AddColumnOperation> UserId { get; set; }
public OperationBuilder<AddColumnOperation> PremiumUntil { get; set; }
public OperationBuilder<AddColumnOperation> Source { get; set; }
}
}

View File

@@ -0,0 +1,83 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
using SZUAbsolventenverein.Module.PremiumArea.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Repository
{
public interface IEngineerApplicationRepository
{
IEnumerable<EngineerApplication> GetEngineerApplications(int ModuleId);
IEnumerable<EngineerApplication> GetEngineerApplications(int ModuleId, string status);
EngineerApplication GetEngineerApplication(int ApplicationId);
EngineerApplication GetEngineerApplication(int ApplicationId, bool tracking);
EngineerApplication AddEngineerApplication(EngineerApplication EngineerApplication);
EngineerApplication UpdateEngineerApplication(EngineerApplication EngineerApplication);
void DeleteEngineerApplication(int ApplicationId);
}
public class EngineerApplicationRepository : IEngineerApplicationRepository, ITransientService
{
private readonly IDbContextFactory<PremiumAreaContext> _factory;
public EngineerApplicationRepository(IDbContextFactory<PremiumAreaContext> factory)
{
_factory = factory;
}
public IEnumerable<EngineerApplication> GetEngineerApplications(int ModuleId)
{
using var db = _factory.CreateDbContext();
return db.EngineerApplication.Where(item => item.ModuleId == ModuleId).ToList();
}
public IEnumerable<EngineerApplication> GetEngineerApplications(int ModuleId, string status)
{
using var db = _factory.CreateDbContext();
return db.EngineerApplication.Where(item => item.ModuleId == ModuleId && item.Status == status).ToList();
}
public EngineerApplication GetEngineerApplication(int ApplicationId)
{
return GetEngineerApplication(ApplicationId, true);
}
public EngineerApplication GetEngineerApplication(int ApplicationId, bool tracking)
{
using var db = _factory.CreateDbContext();
if (tracking)
{
return db.EngineerApplication.Find(ApplicationId);
}
else
{
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();
return EngineerApplication;
}
public EngineerApplication UpdateEngineerApplication(EngineerApplication EngineerApplication)
{
using var db = _factory.CreateDbContext();
db.Entry(EngineerApplication).State = EntityState.Modified;
db.SaveChanges();
return EngineerApplication;
}
public void DeleteEngineerApplication(int ApplicationId)
{
using var db = _factory.CreateDbContext();
EngineerApplication EngineerApplication = db.EngineerApplication.Find(ApplicationId);
db.EngineerApplication.Remove(EngineerApplication);
db.SaveChanges();
}
}
}

View File

@@ -10,6 +10,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
public class PremiumAreaContext : DBContextBase, ITransientService, IMultiDatabase public class PremiumAreaContext : DBContextBase, ITransientService, IMultiDatabase
{ {
public virtual DbSet<Models.PremiumArea> PremiumArea { get; set; } public virtual DbSet<Models.PremiumArea> PremiumArea { get; set; }
public virtual DbSet<Models.EngineerApplication> EngineerApplication { get; set; }
public virtual DbSet<Models.UserPremium> UserPremium { get; set; }
public virtual DbSet<Models.PremiumEvent> PremiumEvent { get; set; }
public PremiumAreaContext(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies) public PremiumAreaContext(IDBContextDependencies DBContextDependencies) : base(DBContextDependencies)
{ {
@@ -21,6 +24,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
base.OnModelCreating(builder); base.OnModelCreating(builder);
builder.Entity<Models.PremiumArea>().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinPremiumArea")); builder.Entity<Models.PremiumArea>().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinPremiumArea"));
builder.Entity<Models.EngineerApplication>().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinEngineerApplications"));
builder.Entity<Models.UserPremium>().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinUserPremium"));
builder.Entity<Models.PremiumEvent>().ToTable(ActiveDatabase.RewriteName("SZUAbsolventenvereinPremiumEvents"));
} }
} }
} }

View File

@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Collections.Generic;
using Oqtane.Modules;
using SZUAbsolventenverein.Module.PremiumArea.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Repository
{
public interface IUserPremiumRepository
{
UserPremium GetUserPremium(int UserId);
UserPremium SaveUserPremium(UserPremium UserPremium);
void AddPremiumEvent(PremiumEvent premiumEvent);
IEnumerable<PremiumEvent> GetPremiumEvents(int UserId);
}
public class UserPremiumRepository : IUserPremiumRepository, ITransientService
{
private readonly IDbContextFactory<PremiumAreaContext> _factory;
public UserPremiumRepository(IDbContextFactory<PremiumAreaContext> factory)
{
_factory = factory;
}
public UserPremium GetUserPremium(int UserId)
{
using var db = _factory.CreateDbContext();
return db.UserPremium.FirstOrDefault(item => item.UserId == UserId);
}
public UserPremium SaveUserPremium(UserPremium UserPremium)
{
using var db = _factory.CreateDbContext();
if (UserPremium.Id > 0)
{
db.Entry(UserPremium).State = EntityState.Modified;
}
else
{
db.UserPremium.Add(UserPremium);
}
db.SaveChanges();
return UserPremium;
}
public void AddPremiumEvent(PremiumEvent premiumEvent)
{
using var db = _factory.CreateDbContext();
db.PremiumEvent.Add(premiumEvent);
db.SaveChanges();
}
public IEnumerable<PremiumEvent> GetPremiumEvents(int UserId)
{
using var db = _factory.CreateDbContext();
return db.PremiumEvent.Where(item => item.UserId == UserId).OrderByDescending(x => x.CreatedOn).ToList();
}
}
}

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

@@ -0,0 +1,114 @@
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.Repository;
using Oqtane.Security;
using Oqtane.Shared;
using SZUAbsolventenverein.Module.PremiumArea.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Services
{
public class ServerUserContactService : IUserContactService
{
private readonly IUserRepository _userRepository;
private readonly INotificationRepository _notificationRepository;
private readonly IUserPermissions _userPermissions;
private readonly ILogManager _logger;
private readonly IHttpContextAccessor _accessor;
private readonly ITenantManager _tenantManager;
private readonly Alias _alias;
public ServerUserContactService(IUserRepository userRepository, INotificationRepository notificationRepository, IUserPermissions userPermissions, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor)
{
_userRepository = userRepository;
_notificationRepository = notificationRepository;
_userPermissions = userPermissions;
_logger = logger;
_accessor = accessor;
_tenantManager = tenantManager;
_alias = tenantManager.GetAlias();
}
public Task<List<User>> SearchUsersAsync(string query, int moduleId)
{
// Note: moduleId param added to match Interface if it requires it, or just ignore if interface doesn't have it.
// Client interface: Task<List<User>> SearchUsersAsync(string query, int moduleId);
// My previous server impl: SearchUsersAsync(string query) -> Mismatch!
// I must match the signature of the Interface defined in Client.
if (string.IsNullOrWhiteSpace(query) || query.Length < 3)
{
return Task.FromResult(new List<User>());
}
if (!_accessor.HttpContext.User.Identity.IsAuthenticated)
{
return Task.FromResult(new List<User>());
}
// Try GetUsers() without params first
var users = _userRepository.GetUsers();
var results = users.Where(u =>
(u.DisplayName != null && u.DisplayName.Contains(query, StringComparison.OrdinalIgnoreCase)) ||
(u.Username != null && u.Username.Contains(query, StringComparison.OrdinalIgnoreCase))
).Take(20).ToList();
var sanitized = results.Select(u => new User
{
UserId = u.UserId,
Username = u.Username,
DisplayName = u.DisplayName,
PhotoFileId = u.PhotoFileId
}).ToList();
return Task.FromResult(sanitized);
}
public Task SendMessageAsync(int recipientUserId, string message, int moduleId)
{
var sender = _accessor.HttpContext.User;
if (!sender.Identity.IsAuthenticated) return Task.CompletedTask;
int senderId = _accessor.HttpContext.GetUserId();
var recipient = _userRepository.GetUser(recipientUserId);
if (recipient == null) return Task.CompletedTask;
var notification = new Notification
{
SiteId = _alias.SiteId,
FromUserId = senderId,
ToUserId = recipientUserId,
ToEmail = "",
Subject = "New Message from " + sender.Identity.Name,
Body = message,
ParentId = null,
CreatedOn = DateTime.UtcNow,
IsDelivered = false,
DeliveredOn = null
};
_notificationRepository.AddNotification(notification);
var emailNotification = new Notification
{
SiteId = _alias.SiteId,
FromUserId = senderId,
ToUserId = recipientUserId,
ToEmail = recipient.Email,
Subject = $"New Connection Request from {sender.Identity.Name}",
Body = $"Hello {recipient.DisplayName},<br><br>{sender.Identity.Name} sent you a message:<br><blockquote>{message}</blockquote><br><br>Login to reply.",
ParentId = null,
CreatedOn = DateTime.UtcNow,
IsDelivered = false
};
_notificationRepository.AddNotification(emailNotification);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Message sent from {SenderId} to {RecipientId}", senderId, recipientUserId);
return Task.CompletedTask;
}
}
}

View File

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

View File

@@ -0,0 +1,37 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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
[Required]
public int? FileId { get; set; }
[StringLength(256)]
public string PdfFileName { get; set; }
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; }
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; }
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Oqtane.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Models
{
[Table("SZUAbsolventenvereinPremiumEvents")]
public class PremiumEvent : ModelBase
{
[Key]
public int Id { get; set; }
public int UserId { get; set; }
public int DeltaDays { get; set; } // +365, etc.
[StringLength(50)]
public string Source { get; set; }
public string ReferenceId { get; set; } // e.g. "AppId:12"
}
}

View File

@@ -0,0 +1,21 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Oqtane.Models;
namespace SZUAbsolventenverein.Module.PremiumArea.Models
{
[Table("SZUAbsolventenvereinUserPremium")]
public class UserPremium : ModelBase
{
[Key]
public int Id { get; set; }
public int UserId { get; set; }
public DateTime? PremiumUntil { get; set; }
[StringLength(50)]
public string Source { get; set; } // "paid", "promo_engineer_application", "admin"
}
}