Compare commits
15 Commits
a1b5d0371d
...
206b974ab3
| Author | SHA1 | Date | |
|---|---|---|---|
| 206b974ab3 | |||
| 97900dddbf | |||
| 3f23b957b0 | |||
| 10388d9a7f | |||
| 0c98c6d86a | |||
| fb5536d29d | |||
| c54a33c159 | |||
| eda0ad794d | |||
| b51b37a6e8 | |||
| 1e88a86be1 | |||
| f80e9d00ee | |||
| 51b8f1c916 | |||
| 54f90ea3fb | |||
| de2312838b | |||
| 4a1d334a5c |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
@@ -0,0 +1,221 @@
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Services
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Models
|
||||
@namespace SZUAbsolventenverein.Module.PremiumArea
|
||||
@inherits ModuleBase
|
||||
@inject IEngineerApplicationService ApplicationService
|
||||
@inject NavigationManager NavManager
|
||||
@inject Oqtane.Services.IUserService UserService
|
||||
|
||||
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) || Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, "Premium Member"))
|
||||
{
|
||||
<h3>Ingenieur-Anträge</h3>
|
||||
|
||||
@if (_applications == null)
|
||||
{
|
||||
<p>Prüfe Premium-Zugang...</p>
|
||||
}
|
||||
else if (_applications.Count == 0)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
Keine 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">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<h5 class="card-title mb-0">@(string.IsNullOrEmpty(app.Title) ? "Ingenieur-Antrag" : app.Title)</h5>
|
||||
@if (Oqtane.Security.UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
|
||||
{
|
||||
@if (_confirmDeleteId == app.ApplicationId)
|
||||
{
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-danger btn-sm" @onclick="@(() => DeleteApp(app))" title="Bestätigen">
|
||||
<span class="oi oi-check"></span>
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" @onclick="@(() => _confirmDeleteId = -1)" title="Abbrechen">
|
||||
<span class="oi oi-x"></span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-outline-danger btn-sm" @onclick="@(() => _confirmDeleteId = app.ApplicationId)" title="Antrag löschen">
|
||||
<span class="oi oi-trash"></span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<h6 class="card-subtitle mb-2 text-muted mt-1">von @GetUserName(app.UserId)</h6>
|
||||
@if (!string.IsNullOrEmpty(app.ShortDescription))
|
||||
{
|
||||
<p class="card-text">@app.ShortDescription</p>
|
||||
}
|
||||
<p class="card-text text-muted">
|
||||
<small>
|
||||
<strong>Datum:</strong> @(app.ApprovedOn?.ToShortDateString() ?? app.CreatedOn.ToShortDateString())
|
||||
</small>
|
||||
</p>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-primary btn-sm" @onclick="@(async () => ShowDetail(app))">PDF ansehen</button>
|
||||
<a href="@(PageState.Alias.Path == "" ? "" : "/" + PageState.Alias.Path)/api/file/download/@(app.FileId)/attach" 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</h5>
|
||||
<button type="button" class="btn-close" @onclick="@(() => _selectedApp = null)"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<div style="min-height: 600px; height: 75vh;">
|
||||
<iframe src="@(PageState.Alias.Path == "" ? "" : "/" + PageState.Alias.Path)/api/file/download/@(_selectedApp.FileId)" style="width: 100%; height: 100%; border: none;" 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 (von @GetUserName(_reportApp.UserId)).</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;
|
||||
private Dictionary<int, string> _userNames = new();
|
||||
private int _confirmDeleteId = -1;
|
||||
|
||||
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();
|
||||
|
||||
// Benutzernamen laden
|
||||
foreach (var userId in _applications.Select(a => a.UserId).Distinct())
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await UserService.GetUserAsync(userId, ModuleState.SiteId);
|
||||
_userNames[userId] = user?.DisplayName ?? $"Benutzer {userId}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
_userNames[userId] = $"Benutzer {userId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteApp(EngineerApplication app)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApplicationService.DeleteApplicationAsync(app.ApplicationId, ModuleState.ModuleId);
|
||||
_applications.Remove(app);
|
||||
_confirmDeleteId = -1;
|
||||
AddModuleMessage("Antrag erfolgreich gelöscht.", MessageType.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddModuleMessage("Fehler beim Löschen: " + ex.Message, MessageType.Error);
|
||||
_confirmDeleteId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUserName(int userId)
|
||||
{
|
||||
return _userNames.TryGetValue(userId, out var name) ? name : $"Benutzer {userId}";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Services
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Models
|
||||
|
||||
@namespace SZUAbsolventenverein.Module.PremiumArea
|
||||
@inherits ModuleBase
|
||||
@inject IEngineerApplicationService ApplicationService
|
||||
@inject NavigationManager NavManager
|
||||
@inject IStringLocalizer<Apply> Localizer
|
||||
|
||||
<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)
|
||||
{
|
||||
<div class="card p-3">
|
||||
<p>Bitte laden Sie Ihren Ingenieur-Antrag als PDF-Datei hoch.</p>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Titel</label>
|
||||
<input id="title" type="text" class="form-control" @bind="_existingApp.Title" maxlength="256"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Kurzbeschreibung</label>
|
||||
<textarea id="description" class="form-control" rows="3" @bind="_existingApp.ShortDescription" placeholder="Kurze Beschreibung Ihres Ingenieur-Antrags..."></textarea>
|
||||
</div>
|
||||
|
||||
@* <div class="mb-3">
|
||||
<label for="pdfUpload" class="form-label">Antrags-PDF</label>
|
||||
<InputFile OnChange="@LoadFiles" class="form-control" accept=".pdf"/>
|
||||
<div class="form-text">Max Größe: 20MB. Format: Nur PDF.</div>
|
||||
</div> *@
|
||||
|
||||
|
||||
<FileManager OnSelectFile="@OnSelectFile" ShowProgress="true" ShowSuccess="true"/>
|
||||
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary" @onclick="SubmitApplication" disabled="@(_existingApp?.FileId == 0)">
|
||||
@(_existingApp.ApplicationId > 0 ? "Antrag aktualisieren" : "Antrag hochladen")
|
||||
</button>
|
||||
<button class="btn btn-secondary" @onclick="Cancel">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (_existingApp.FileId > 0)
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">Ihr Antrag</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong>Status:</strong> <span class="badge bg-success">Veröffentlicht</span>
|
||||
</p>
|
||||
@if (!string.IsNullOrEmpty(_existingApp.Title))
|
||||
{
|
||||
<p>
|
||||
<strong>Titel:</strong> @_existingApp.Title
|
||||
</p>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(_existingApp.ShortDescription))
|
||||
{
|
||||
<p>
|
||||
<strong>Kurzbeschreibung:</strong> @_existingApp.ShortDescription
|
||||
</p>
|
||||
}
|
||||
<p>
|
||||
<strong>Datum:</strong> @_existingApp.CreatedOn.ToShortDateString()
|
||||
</p>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-primary" @onclick="EditApp">Antrag aktualisieren</button>
|
||||
@if (!_confirmDelete)
|
||||
{
|
||||
<button class="btn btn-outline-danger" @onclick="() => _confirmDelete = true">Antrag löschen</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-danger" @onclick="DeleteApp">Wirklich löschen?</button>
|
||||
<button class="btn btn-secondary" @onclick="() => _confirmDelete = false">Abbrechen</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
|
||||
|
||||
private EngineerApplication _existingApp = new EngineerApplication();
|
||||
private bool _showForm = true;
|
||||
private bool _confirmDelete = false;
|
||||
private string _message = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var apps = await ApplicationService.GetApplicationsAsync(ModuleState.ModuleId);
|
||||
var userId = PageState.User?.UserId ?? -1;
|
||||
_existingApp.Status = "New";
|
||||
_existingApp = apps.FirstOrDefault(a => a.UserId == userId, _existingApp);
|
||||
|
||||
if (_existingApp.FileId > 0)
|
||||
{
|
||||
_showForm = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void EditApp()
|
||||
{
|
||||
_showForm = true;
|
||||
}
|
||||
|
||||
private async Task SubmitApplication()
|
||||
{
|
||||
if (_existingApp == null || _existingApp.FileId == 0)
|
||||
{
|
||||
_message = "Bitte wählen Sie eine Datei aus.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var app = new EngineerApplication
|
||||
{
|
||||
ApplicationId = _existingApp?.ApplicationId ?? 0,
|
||||
ModuleId = ModuleState.ModuleId,
|
||||
UserId = PageState.User.UserId,
|
||||
FileId = _existingApp.FileId,
|
||||
Title = _existingApp.Title,
|
||||
ShortDescription = _existingApp.ShortDescription,
|
||||
Status = "Published", // Auto-publish
|
||||
SubmittedOn = DateTime.UtcNow,
|
||||
ApprovedOn = DateTime.UtcNow, // Auto-approved
|
||||
};
|
||||
|
||||
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 veröffentlicht.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = "Fehler: " + ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
NavManager.NavigateTo(NavigateUrl());
|
||||
}
|
||||
|
||||
private async Task DeleteApp()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApplicationService.DeleteApplicationAsync(_existingApp.ApplicationId, ModuleState.ModuleId);
|
||||
_existingApp = new EngineerApplication { Status = "New" };
|
||||
_showForm = true;
|
||||
_confirmDelete = false;
|
||||
_message = "Antrag erfolgreich gelöscht.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = "Fehler beim Löschen: " + ex.Message;
|
||||
_confirmDelete = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Task OnSelectFile(int fileId)
|
||||
{
|
||||
_existingApp.FileId = fileId;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Services
|
||||
@using SZUAbsolventenverein.Module.PremiumArea.Models
|
||||
@using Oqtane.Security
|
||||
@using Oqtane.Shared
|
||||
|
||||
@namespace SZUAbsolventenverein.Module.PremiumArea
|
||||
@inherits ModuleBase
|
||||
@@ -7,35 +9,11 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IStringLocalizer<Index> Localizer
|
||||
|
||||
@if (_PremiumAreas == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ActionLink Action="Add" Security="SecurityAccessLevel.Edit" Text="Add PremiumArea" ResourceKey="Add" />
|
||||
<br />
|
||||
<br />
|
||||
@if (@_PremiumAreas.Count != 0)
|
||||
{
|
||||
<Pager Items="@_PremiumAreas">
|
||||
<Header>
|
||||
<th style="width: 1px;"> </th>
|
||||
<th style="width: 1px;"> </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
|
||||
{
|
||||
<p>@Localizer["Message.DisplayNone"]</p>
|
||||
}
|
||||
}
|
||||
<div class="mb-3">
|
||||
<ActionLink Action="Apply" Text="Ingenieur Antrag hochladen"/>
|
||||
<ActionLink Action="UserSearch" Text="Mitglieder finden"/>
|
||||
<ActionLink Action="ApplicationList" Text="Alle Ingenieur-Anträge"/>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
public override string RenderMode => RenderModes.Static;
|
||||
@@ -46,34 +24,4 @@ else
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Modules;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace SZUAbsolventenverein.Module.PremiumArea
|
||||
{
|
||||
@@ -8,12 +9,16 @@ namespace SZUAbsolventenverein.Module.PremiumArea
|
||||
public ModuleDefinition ModuleDefinition => new ModuleDefinition
|
||||
{
|
||||
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.",
|
||||
Version = "1.0.0",
|
||||
ServerManagerType = "SZUAbsolventenverein.Module.PremiumArea.Manager.PremiumAreaManager, SZUAbsolventenverein.Module.PremiumArea.Server.Oqtane",
|
||||
ReleaseVersions = "1.0.0",
|
||||
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.4",
|
||||
ServerManagerType =
|
||||
"SZUAbsolventenverein.Module.PremiumArea.Manager.PremiumAreaManager, SZUAbsolventenverein.Module.PremiumArea.Server.Oqtane",
|
||||
ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4",
|
||||
Dependencies = "SZUAbsolventenverein.Module.PremiumArea.Shared.Oqtane",
|
||||
PackageName = "SZUAbsolventenverein.Module.PremiumArea"
|
||||
PackageName = "SZUAbsolventenverein.Module.PremiumArea",
|
||||
// Hier definieren Sie, WELCHE Permissions verfügbar sind
|
||||
PermissionNames = $"{PermissionNames.View},{PermissionNames.Edit},{PermissionNames.Browse}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Client/Services/EngineerApplicationService.cs
Normal file
75
Client/Services/EngineerApplicationService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Client/Services/UserContactService.cs
Normal file
32
Client/Services/UserContactService.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup
|
||||
{
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||
<metadata>
|
||||
<id>$ProjectName$</id>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.4</version>
|
||||
<authors>SZUAbsolventenverein</authors>
|
||||
<owners>SZUAbsolventenverein</owners>
|
||||
<title>PremiumArea</title>
|
||||
|
||||
@@ -3,5 +3,5 @@ ProjectName=$2
|
||||
|
||||
find . -name *.nupkg -delete
|
||||
dotnet run --project ../../fixProps/FixProps/FixProps.csproj
|
||||
nuget pack $ProjectName.nuspec -Properties "targetframework=${TargetFramework};ProjectName=${ProjectName}"
|
||||
dotnet pack $ProjectName.nuspec "/p:targetframework=${TargetFramework};ProjectName=${ProjectName}"
|
||||
cp -f *.nupkg ../../oqtane.framework/Oqtane.Server/Packages/
|
||||
|
||||
396
Server/Controllers/EngineerApplicationController.cs
Normal file
396
Server/Controllers/EngineerApplicationController.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
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))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Create,
|
||||
"DEBUG: Attempting to save application. UserId: {UserId}, ModuleId: {ModuleId}, FileId: {FileId}",
|
||||
Application.UserId, Application.ModuleId, Application.FileId);
|
||||
|
||||
// Manual validation before EF Core sees it
|
||||
if (Application.UserId == 0)
|
||||
_logger.Log(LogLevel.Warning, this, LogFunction.Create, "DEBUG: UserId is 0!");
|
||||
if (Application.FileId == null || Application.FileId == 0)
|
||||
_logger.Log(LogLevel.Warning, this, LogFunction.Create, "DEBUG: FileId is null or 0!");
|
||||
|
||||
var result = await _service.AddApplicationAsync(Application);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Create, "DEBUG: Save successful!");
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var innerMessage = ex.InnerException?.Message ?? "No inner exception";
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
|
||||
"CRITICAL DB ERROR: {Message}. Inner: {Inner}", ex.Message, innerMessage);
|
||||
|
||||
// Force output to console so the user sees it immediately
|
||||
Console.WriteLine("========================================");
|
||||
Console.WriteLine($"!!! DATABASE INSERT FAILED !!!");
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
Console.WriteLine($"Inner: {innerMessage}");
|
||||
Console.WriteLine($"Stack: {ex.StackTrace}");
|
||||
Console.WriteLine("========================================");
|
||||
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security,
|
||||
"Unauthorized EngineerApplication Post Attempt {Application}", Application);
|
||||
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))
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _service.UpdateApplicationAsync(Application);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Update, ex,
|
||||
"Error updating application: {Message}. Inner: {Inner}", ex.Message,
|
||||
ex.InnerException?.Message);
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security,
|
||||
"Unauthorized EngineerApplication Put Attempt {Application}", Application);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
[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))
|
||||
{
|
||||
await _service.ApproveApplicationAsync(id, ModuleId);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("log")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetLog()
|
||||
{
|
||||
try
|
||||
{
|
||||
var repo = (EngineerApplicationRepository)_service.GetType().GetField("_repository",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||
.GetValue(_service);
|
||||
var factory = (IDbContextFactory<PremiumAreaContext>)repo.GetType().GetField("_factory",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(repo);
|
||||
using var db = factory.CreateDbContext();
|
||||
|
||||
var connection = db.Database.GetDbConnection();
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "PRAGMA table_info(SZUAbsolventenvereinEngineerApplications);";
|
||||
var columns = new List<string>();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
columns.Add($"{reader["name"]} ({reader["type"]})");
|
||||
}
|
||||
|
||||
if (columns.Count == 0)
|
||||
{
|
||||
// Maybe it's rewritten?
|
||||
command.CommandText =
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%Engineer%';";
|
||||
using var reader2 = command.ExecuteReader();
|
||||
while (reader2.Read())
|
||||
{
|
||||
columns.Add($"Found table: {reader2["name"]}");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(columns);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
|
||||
"Error getting columns: {Message}. Inner: {Inner}", ex.Message, ex.InnerException?.Message);
|
||||
return Ok($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[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))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Request.Form.Files.Count == 0) return BadRequest("No file uploaded");
|
||||
|
||||
var file = Request.Form.Files[0];
|
||||
if (file.ContentType != "application/pdf") return BadRequest("Only PDF files are allowed");
|
||||
|
||||
var alias = _accessor.HttpContext.Items["Alias"] as Alias; // Retained original alias retrieval
|
||||
var siteId = alias.SiteId;
|
||||
var folderPath = "EngineerApplications";
|
||||
|
||||
var folder = _folders.GetFolder(siteId, folderPath);
|
||||
|
||||
if (folder == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create folder
|
||||
folder = new Folder
|
||||
{
|
||||
SiteId = siteId,
|
||||
ParentId = null,
|
||||
Name = "EngineerApplications",
|
||||
Path = folderPath,
|
||||
PermissionList = new List<Permission>
|
||||
{
|
||||
new Permission(PermissionNames.View, RoleNames.Admin, true),
|
||||
new Permission(PermissionNames.Edit, RoleNames.Admin, true)
|
||||
}
|
||||
};
|
||||
folder = _folders.AddFolder(folder);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
|
||||
"Error creating folder: {Message}", ex.Message);
|
||||
return BadRequest($"Folder creation failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(file.FileName).ToLower();
|
||||
if (ext != ".pdf") return BadRequest("Invalid file extension");
|
||||
|
||||
var tenantId = alias.TenantId;
|
||||
var uploadPath = Path.Combine(_environment.ContentRootPath, "Content", "Tenants",
|
||||
tenantId.ToString(), "Sites", siteId.ToString(), folderPath);
|
||||
if (!Directory.Exists(uploadPath)) Directory.CreateDirectory(uploadPath);
|
||||
|
||||
var uniqueName = $"{Guid.NewGuid()}{ext}";
|
||||
var filePath = Path.Combine(uploadPath, uniqueName);
|
||||
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
var fileObj = new Oqtane.Models.File
|
||||
{
|
||||
FolderId = folder.FolderId,
|
||||
Name = uniqueName,
|
||||
Extension = ext.Substring(1),
|
||||
Size = (int)file.Length,
|
||||
ImageHeight = 0,
|
||||
ImageWidth = 0
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var addedFile = _files.AddFile(fileObj);
|
||||
return Ok(new { FileId = addedFile.FileId, FileName = file.FileName });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex,
|
||||
"Error saving file record to DB: {Message}. Inner: {Inner}", ex.Message,
|
||||
ex.InnerException?.Message);
|
||||
// Critical: This is where we suspect the DbUpdateException
|
||||
Console.WriteLine($"UPLOAD DB ERROR: {ex.Message} | {ex.InnerException?.Message}");
|
||||
return BadRequest($"Database error during file registration: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "General Upload Error: {Message}",
|
||||
ex.Message);
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Server/Controllers/UserContactController.cs
Normal file
59
Server/Controllers/UserContactController.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Server/Migrations/01000001_AddPremiumTables copy.cs
Normal file
42
Server/Migrations/01000001_AddPremiumTables copy.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Oqtane.Databases.Interfaces;
|
||||
using Oqtane.Migrations;
|
||||
using SZUAbsolventenverein.Module.PremiumArea.Migrations.EntityBuilders;
|
||||
using SZUAbsolventenverein.Module.PremiumArea.Repository;
|
||||
|
||||
namespace SZUAbsolventenverein.Module.PremiumArea.Migrations
|
||||
{
|
||||
[DbContext(typeof(PremiumAreaContext))]
|
||||
[Migration("SZUAbsolventenverein.Module.PremiumArea.01.00.00.02")]
|
||||
public class RemoveReportingAndMoveToFileManager : MultiDatabaseMigration
|
||||
{
|
||||
public RemoveReportingAndMoveToFileManager(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var engAppBuilder = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
engAppBuilder.DropColumn("PdfFileName");
|
||||
engAppBuilder.DropColumn("IsReported");
|
||||
engAppBuilder.DropColumn("ReportReason");
|
||||
engAppBuilder.DropColumn("ReportCount");
|
||||
engAppBuilder.DropColumn("AdminReviewedBy");
|
||||
engAppBuilder.DropColumn("AdminReviewedAt");
|
||||
engAppBuilder.DropColumn("AdminNote");
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Server/Migrations/01000003_AddTitleAndDescription.cs
Normal file
32
Server/Migrations/01000003_AddTitleAndDescription.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
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.03")]
|
||||
public class AddTitleAndDescription : MultiDatabaseMigration
|
||||
{
|
||||
public AddTitleAndDescription(IDatabase database) : base(database)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var table = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
table.AddStringColumn("Title", 256, true);
|
||||
table.AddMaxStringColumn("ShortDescription", true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
var table = new EngineerApplicationEntityBuilder(migrationBuilder, ActiveDatabase);
|
||||
table.DropColumn("Title");
|
||||
table.DropColumn("ShortDescription");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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);
|
||||
Title = AddStringColumn(table, "Title", 256, true);
|
||||
ShortDescription = AddMaxStringColumn(table, "ShortDescription", 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> Title { get; set; }
|
||||
public OperationBuilder<AddColumnOperation> ShortDescription { 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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
36
Server/Migrations/EntityBuilders/UserPremiumEntityBuilder.cs
Normal file
36
Server/Migrations/EntityBuilders/UserPremiumEntityBuilder.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
122
Server/Repository/EngineerApplicationRepository.cs
Normal file
122
Server/Repository/EngineerApplicationRepository.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
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();
|
||||
try
|
||||
{
|
||||
EngineerApplication.CreatedBy = EngineerApplication.CreatedBy ?? "system";
|
||||
EngineerApplication.CreatedOn = DateTime.UtcNow;
|
||||
EngineerApplication.ModifiedBy = EngineerApplication.ModifiedBy ?? "system";
|
||||
EngineerApplication.ModifiedOn = DateTime.UtcNow;
|
||||
db.EngineerApplication.Add(EngineerApplication);
|
||||
db.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Throwing a new exception with more details so it's visible
|
||||
var msg = $"DB Error: {ex.Message} | Inner: {ex.InnerException?.Message}";
|
||||
Console.WriteLine(msg); // Log to console for dotnet run
|
||||
throw new Exception(msg, ex);
|
||||
}
|
||||
|
||||
return EngineerApplication;
|
||||
}
|
||||
|
||||
public EngineerApplication UpdateEngineerApplication(EngineerApplication EngineerApplication)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
try
|
||||
{
|
||||
var existing = db.EngineerApplication.Find(EngineerApplication.ApplicationId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.FileId = EngineerApplication.FileId;
|
||||
existing.Title = EngineerApplication.Title;
|
||||
existing.ShortDescription = EngineerApplication.ShortDescription;
|
||||
existing.Status = EngineerApplication.Status;
|
||||
existing.SubmittedOn = EngineerApplication.SubmittedOn;
|
||||
existing.ApprovedOn = EngineerApplication.ApprovedOn;
|
||||
|
||||
existing.ModifiedBy = EngineerApplication.ModifiedBy ?? "system";
|
||||
existing.ModifiedOn = DateTime.UtcNow;
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = $"DB Error (Update): {ex.Message} | Inner: {ex.InnerException?.Message}";
|
||||
Console.WriteLine(msg);
|
||||
throw new Exception(msg, ex);
|
||||
}
|
||||
|
||||
return EngineerApplication;
|
||||
}
|
||||
|
||||
public void DeleteEngineerApplication(int ApplicationId)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
EngineerApplication EngineerApplication = db.EngineerApplication.Find(ApplicationId);
|
||||
db.EngineerApplication.Remove(EngineerApplication);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
|
||||
public class PremiumAreaContext : DBContextBase, ITransientService, IMultiDatabase
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -21,6 +24,9 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
@@ -51,6 +52,10 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
|
||||
public Models.PremiumArea AddPremiumArea(Models.PremiumArea PremiumArea)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
PremiumArea.CreatedBy = PremiumArea.CreatedBy ?? "system";
|
||||
PremiumArea.CreatedOn = DateTime.UtcNow;
|
||||
PremiumArea.ModifiedBy = PremiumArea.ModifiedBy ?? "system";
|
||||
PremiumArea.ModifiedOn = DateTime.UtcNow;
|
||||
db.PremiumArea.Add(PremiumArea);
|
||||
db.SaveChanges();
|
||||
return PremiumArea;
|
||||
@@ -59,8 +64,14 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Repository
|
||||
public Models.PremiumArea UpdatePremiumArea(Models.PremiumArea PremiumArea)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
db.Entry(PremiumArea).State = EntityState.Modified;
|
||||
db.SaveChanges();
|
||||
var existing = db.PremiumArea.Find(PremiumArea.PremiumAreaId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Name = PremiumArea.Name;
|
||||
existing.ModifiedBy = PremiumArea.ModifiedBy ?? "system";
|
||||
existing.ModifiedOn = DateTime.UtcNow;
|
||||
db.SaveChanges();
|
||||
}
|
||||
return PremiumArea;
|
||||
}
|
||||
|
||||
|
||||
77
Server/Repository/UserPremiumRepository.cs
Normal file
77
Server/Repository/UserPremiumRepository.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
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)
|
||||
{
|
||||
var existing = db.UserPremium.Find(UserPremium.Id);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.UserId = UserPremium.UserId;
|
||||
existing.PremiumUntil = UserPremium.PremiumUntil;
|
||||
existing.ModifiedBy = UserPremium.ModifiedBy ?? "system";
|
||||
existing.ModifiedOn = DateTime.UtcNow;
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UserPremium.CreatedBy = UserPremium.CreatedBy ?? "system";
|
||||
UserPremium.CreatedOn = DateTime.UtcNow;
|
||||
UserPremium.ModifiedBy = UserPremium.ModifiedBy ?? "system";
|
||||
UserPremium.ModifiedOn = DateTime.UtcNow;
|
||||
db.UserPremium.Add(UserPremium);
|
||||
db.SaveChanges();
|
||||
}
|
||||
return UserPremium;
|
||||
}
|
||||
|
||||
public void AddPremiumEvent(PremiumEvent premiumEvent)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
premiumEvent.CreatedBy = premiumEvent.CreatedBy ?? "system";
|
||||
premiumEvent.CreatedOn = DateTime.UtcNow;
|
||||
premiumEvent.ModifiedBy = premiumEvent.ModifiedBy ?? "system";
|
||||
premiumEvent.ModifiedOn = DateTime.UtcNow;
|
||||
db.PremiumEvent.Add(premiumEvent);
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
public IEnumerable<PremiumEvent> GetPremiumEvents(int UserId)
|
||||
{
|
||||
using var db = _factory.CreateDbContext();
|
||||
return db.PremiumEvent.Where(item => item.UserId == UserId).OrderByDescending(x => x.CreatedOn).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
Server/Services/PremiumService.cs
Normal file
98
Server/Services/PremiumService.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using SZUAbsolventenverein.Module.PremiumArea.Models;
|
||||
using SZUAbsolventenverein.Module.PremiumArea.Repository;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Shared;
|
||||
using Oqtane.Modules;
|
||||
using Oqtane.Enums;
|
||||
|
||||
namespace SZUAbsolventenverein.Module.PremiumArea.Services
|
||||
{
|
||||
public interface IPremiumService
|
||||
{
|
||||
bool IsPremium(int userId);
|
||||
DateTime? GetPremiumUntil(int userId);
|
||||
void GrantPremium(int userId, int durationMonths, string source, string referenceId = null);
|
||||
}
|
||||
|
||||
public class PremiumService : IPremiumService, ITransientService
|
||||
{
|
||||
private readonly IUserPremiumRepository _userPremiumRepository;
|
||||
private readonly ILogManager _logger;
|
||||
|
||||
public PremiumService(IUserPremiumRepository userPremiumRepository, ILogManager logger)
|
||||
{
|
||||
_userPremiumRepository = userPremiumRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsPremium(int userId)
|
||||
{
|
||||
var premium = _userPremiumRepository.GetUserPremium(userId);
|
||||
return premium != null && premium.PremiumUntil.HasValue && premium.PremiumUntil.Value > DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public DateTime? GetPremiumUntil(int userId)
|
||||
{
|
||||
var premium = _userPremiumRepository.GetUserPremium(userId);
|
||||
return premium?.PremiumUntil;
|
||||
}
|
||||
|
||||
public void GrantPremium(int userId, int durationMonths, string source, string referenceId = null)
|
||||
{
|
||||
var current = _userPremiumRepository.GetUserPremium(userId);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
DateTime startBase = now;
|
||||
if (current != null && current.PremiumUntil.HasValue && current.PremiumUntil.Value > now)
|
||||
{
|
||||
startBase = current.PremiumUntil.Value;
|
||||
}
|
||||
|
||||
var newUntil = startBase.AddMonths(durationMonths);
|
||||
|
||||
// delta days for audit
|
||||
int deltaDays = (newUntil - (current?.PremiumUntil ?? now)).Days;
|
||||
// correction: actually we want to know how many days we ADDED to the existing flow.
|
||||
// If they had 0 days separately, we added durationMonths * 30 approx.
|
||||
// If they had existing time, we added durationMonths.
|
||||
// Audit Log usually tracks "What did this action add?". It added 1 year.
|
||||
// But let's calculate days added relative to "previous state".
|
||||
// If expired: Added (NewUntil - Now) days.
|
||||
// If active: Added (NewUntil - OldUntil) = durationMonths roughly.
|
||||
|
||||
// Simpler: Just store the DurationMonths converted to days roughly, or the stored delta.
|
||||
int addedDays = (newUntil - startBase).Days;
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
current = new UserPremium
|
||||
{
|
||||
UserId = userId,
|
||||
CreatedOn = now,
|
||||
ModifiedOn = now
|
||||
};
|
||||
}
|
||||
|
||||
current.PremiumUntil = newUntil;
|
||||
current.Source = source;
|
||||
current.ModifiedOn = now;
|
||||
|
||||
_userPremiumRepository.SaveUserPremium(current);
|
||||
|
||||
// Audit
|
||||
var audit = new PremiumEvent
|
||||
{
|
||||
UserId = userId,
|
||||
DeltaDays = addedDays,
|
||||
Source = source,
|
||||
ReferenceId = referenceId,
|
||||
CreatedOn = now,
|
||||
ModifiedOn = now
|
||||
};
|
||||
_userPremiumRepository.AddPremiumEvent(audit);
|
||||
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Granted Premium for User {UserId} until {Until}", userId, newUntil);
|
||||
}
|
||||
}
|
||||
}
|
||||
326
Server/Services/ServerEngineerApplicationService.cs
Normal file
326
Server/Services/ServerEngineerApplicationService.cs
Normal file
@@ -0,0 +1,326 @@
|
||||
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());
|
||||
}
|
||||
|
||||
var userId = _accessor.HttpContext.GetUserId();
|
||||
var results = new List<EngineerApplication>();
|
||||
|
||||
// Always include the user's own applications (needed for Apply.razor)
|
||||
if (userId != -1)
|
||||
{
|
||||
var ownApps = _repository.GetEngineerApplications(ModuleId)
|
||||
.Where(a => a.UserId == userId).ToList();
|
||||
results.AddRange(ownApps);
|
||||
}
|
||||
|
||||
// Check if Premium - also show approved/published apps from others
|
||||
if (IsUserPremium(user))
|
||||
{
|
||||
var approved = _repository.GetEngineerApplications(ModuleId, "Approved");
|
||||
var published = _repository.GetEngineerApplications(ModuleId, "Published");
|
||||
results.AddRange(approved.Union(published));
|
||||
// Remove duplicates (own apps might already be in approved/published)
|
||||
results = results.GroupBy(a => a.ApplicationId).Select(g => g.First()).ToList();
|
||||
}
|
||||
|
||||
return Task.FromResult(results);
|
||||
}
|
||||
|
||||
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);
|
||||
bool isOwner = (userId != -1 && existing.UserId == userId);
|
||||
|
||||
if (isAdmin || isOwner)
|
||||
{
|
||||
if (!isAdmin)
|
||||
{
|
||||
// Owner can update their own application
|
||||
// Accept "Published" status from client (auto-publish without admin approval)
|
||||
if (Application.Status == "Published")
|
||||
{
|
||||
Application.SubmittedOn ??= DateTime.UtcNow;
|
||||
Application.ApprovedOn ??= DateTime.UtcNow;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep existing status if client didn't explicitly set to Published
|
||||
Application.Status = existing.Status;
|
||||
}
|
||||
}
|
||||
|
||||
Application = _repository.UpdateEngineerApplication(Application);
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Application Updated {Application}",
|
||||
Application);
|
||||
return Task.FromResult(Application);
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Error, this, LogFunction.Security,
|
||||
"Unauthorized Update Attempt. UserId: {UserId}, AppOwner: {OwnerId}, IsAdmin: {IsAdmin}",
|
||||
userId, existing.UserId, isAdmin);
|
||||
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.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";
|
||||
_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)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
if (!user.Identity.IsAuthenticated)
|
||||
return false;
|
||||
|
||||
// Check 1: Oqtane role "Premium Member" (matches the UI-level check in ApplicationList.razor)
|
||||
if (user.IsInRole("Premium Member"))
|
||||
return true;
|
||||
|
||||
// Check 2: Custom UserPremium DB table (for premium granted via GrantPremium/payment)
|
||||
int userId = -1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Server/Services/ServerUserContactService.cs
Normal file
114
Server/Services/ServerUserContactService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ namespace SZUAbsolventenverein.Module.PremiumArea.Startup
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IPremiumAreaService, ServerPremiumAreaService>();
|
||||
services.AddTransient<IEngineerApplicationService, ServerEngineerApplicationService>();
|
||||
services.AddTransient<IUserContactService, ServerUserContactService>();
|
||||
services.AddDbContextFactory<PremiumAreaContext>(opt => { }, ServiceLifetime.Transient);
|
||||
}
|
||||
}
|
||||
|
||||
24
Shared/Models/EngineerApplication.cs
Normal file
24
Shared/Models/EngineerApplication.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
public int FileId { get; set; }
|
||||
[StringLength(256)] public string Title { get; set; }
|
||||
public string ShortDescription { get; set; }
|
||||
|
||||
// Status: "Draft", "Submitted", "Approved", "Rejected"
|
||||
[StringLength(50)] public string Status { get; set; }
|
||||
|
||||
public DateTime? SubmittedOn { get; set; }
|
||||
public DateTime? ApprovedOn { get; set; }
|
||||
}
|
||||
}
|
||||
23
Shared/Models/PremiumEvent.cs
Normal file
23
Shared/Models/PremiumEvent.cs
Normal 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"
|
||||
}
|
||||
}
|
||||
21
Shared/Models/UserPremium.cs
Normal file
21
Shared/Models/UserPremium.cs
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user