From 58e562adb16950eed8cd09575b3c59d9a0c6a8a4 Mon Sep 17 00:00:00 2001 From: MarcWieland Date: Mon, 8 Jun 2026 16:24:51 +0200 Subject: [PATCH] WASM Mode activated --- Components/Layout/ReconnectModal.razor | 31 -- Components/Layout/ReconnectModal.razor.css | 157 --------- Components/Layout/ReconnectModal.razor.js | 63 ---- Data/UserNotificationService.cs | 19 -- Program.cs | 132 -------- docker-compose.yml | 28 +- run-local.sh | 16 + .../Components}/Layout/MainLayout.razor | 4 +- .../Components}/Layout/MainLayout.razor.css | 0 .../Components}/Layout/NavMenu.razor | 2 +- .../Components}/Layout/NavMenu.razor.css | 0 .../Components}/Pages/AdminUsers.razor | 6 +- .../Components}/Pages/Changelog.razor | 12 +- .../Components}/Pages/Feiertage.razor | 6 +- .../Components}/Pages/Home.razor | 6 +- .../Components}/Pages/Login.razor | 107 +++++- .../Components}/Pages/Month.razor | 6 +- .../Components}/Pages/NotFound.razor | 0 .../Components}/Pages/Settings.razor | 6 +- .../Components}/Pages/UrlaubsMaximizer.razor | 6 +- .../Components}/RedirectToLogin.razor | 0 .../Components}/Routes.razor | 8 +- timetracker.Client/Components/_Imports.razor | 17 + timetracker.Client/Program.cs | 27 ++ .../Properties/launchSettings.json | 25 ++ .../Services/ClientAuthService.cs | 74 +++++ .../Services/ClientHolidayService.cs | 49 +++ .../Services/ClientTimetrackerService.cs | 65 ++++ .../HostAuthenticationStateProvider.cs | 73 +++++ .../Services/UserNotificationService.cs | 56 ++++ timetracker.Client/_Imports.razor | 13 + timetracker.Client/timetracker.Client.csproj | 22 ++ timetracker.Client/wwwroot/icon-192.png | Bin 0 -> 2626 bytes .../Components}/App.razor | 3 +- .../Components}/Pages/Error.razor | 0 .../Components}/_Imports.razor | 7 +- .../Data}/AuthService.cs | 3 +- .../Data}/HolidayService.cs | 3 +- .../20260520133634_Initial.Designer.cs | 0 .../Migrations/20260520133634_Initial.cs | 0 ...260520200000_AddPublicHolidays.Designer.cs | 0 .../20260520200000_AddPublicHolidays.cs | 0 .../20260522081459_AddMultiUser.Designer.cs | 0 .../Migrations/20260522081459_AddMultiUser.cs | 0 ...215_AddFlexTimeAndHolidayState.Designer.cs | 0 ...260607213215_AddFlexTimeAndHolidayState.cs | 0 .../TimetrackerDbContextModelSnapshot.cs | 0 timetracker.Server/Data/NotificationHub.cs | 7 + .../Data}/TimetrackerDbContext.cs | 1 + .../Data}/TimetrackerService.cs | 3 +- .../Data/UserNotificationService.cs | 37 +++ Dockerfile => timetracker.Server/Dockerfile | 23 +- timetracker.Server/Program.cs | 305 ++++++++++++++++++ .../Properties}/launchSettings.json | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 .../timetracker.Server.csproj | 7 + .../wwwroot}/app.css | 0 .../wwwroot}/favicon.png | Bin .../wwwroot}/favicon.svg | 0 .../lib/bootstrap/dist/css/bootstrap-grid.css | 0 .../bootstrap/dist/css/bootstrap-grid.css.map | 0 .../bootstrap/dist/css/bootstrap-grid.min.css | 0 .../dist/css/bootstrap-grid.min.css.map | 0 .../bootstrap/dist/css/bootstrap-grid.rtl.css | 0 .../dist/css/bootstrap-grid.rtl.css.map | 0 .../dist/css/bootstrap-grid.rtl.min.css | 0 .../dist/css/bootstrap-grid.rtl.min.css.map | 0 .../bootstrap/dist/css/bootstrap-reboot.css | 0 .../dist/css/bootstrap-reboot.css.map | 0 .../dist/css/bootstrap-reboot.min.css | 0 .../dist/css/bootstrap-reboot.min.css.map | 0 .../dist/css/bootstrap-reboot.rtl.css | 0 .../dist/css/bootstrap-reboot.rtl.css.map | 0 .../dist/css/bootstrap-reboot.rtl.min.css | 0 .../dist/css/bootstrap-reboot.rtl.min.css.map | 0 .../dist/css/bootstrap-utilities.css | 0 .../dist/css/bootstrap-utilities.css.map | 0 .../dist/css/bootstrap-utilities.min.css | 0 .../dist/css/bootstrap-utilities.min.css.map | 0 .../dist/css/bootstrap-utilities.rtl.css | 0 .../dist/css/bootstrap-utilities.rtl.css.map | 0 .../dist/css/bootstrap-utilities.rtl.min.css | 0 .../css/bootstrap-utilities.rtl.min.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.css | 0 .../lib/bootstrap/dist/css/bootstrap.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.min.css | 0 .../bootstrap/dist/css/bootstrap.min.css.map | 0 .../lib/bootstrap/dist/css/bootstrap.rtl.css | 0 .../bootstrap/dist/css/bootstrap.rtl.css.map | 0 .../bootstrap/dist/css/bootstrap.rtl.min.css | 0 .../dist/css/bootstrap.rtl.min.css.map | 0 .../lib/bootstrap/dist/js/bootstrap.bundle.js | 0 .../bootstrap/dist/js/bootstrap.bundle.js.map | 0 .../bootstrap/dist/js/bootstrap.bundle.min.js | 0 .../dist/js/bootstrap.bundle.min.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.esm.js | 0 .../bootstrap/dist/js/bootstrap.esm.js.map | 0 .../bootstrap/dist/js/bootstrap.esm.min.js | 0 .../dist/js/bootstrap.esm.min.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.js | 0 .../lib/bootstrap/dist/js/bootstrap.js.map | 0 .../lib/bootstrap/dist/js/bootstrap.min.js | 0 .../bootstrap/dist/js/bootstrap.min.js.map | 0 {Data => timetracker.Shared}/AppSettings.cs | 2 +- {Data => timetracker.Shared}/BreakEntry.cs | 3 +- timetracker.Shared/IAuthService.cs | 10 + timetracker.Shared/IHolidayService.cs | 8 + timetracker.Shared/ITimetrackerService.cs | 14 + .../IUserNotificationService.cs | 7 + {Data => timetracker.Shared}/PublicHoliday.cs | 2 +- {Data => timetracker.Shared}/User.cs | 2 +- timetracker.Shared/UserInfo.cs | 7 + {Data => timetracker.Shared}/VacationDay.cs | 2 +- {Data => timetracker.Shared}/WorkDay.cs | 2 +- timetracker.Shared/timetracker.Shared.csproj | 9 + timetracker.db | Bin 49152 -> 0 bytes timetracker.slnx | 5 + 118 files changed, 1038 insertions(+), 470 deletions(-) delete mode 100644 Components/Layout/ReconnectModal.razor delete mode 100644 Components/Layout/ReconnectModal.razor.css delete mode 100644 Components/Layout/ReconnectModal.razor.js delete mode 100644 Data/UserNotificationService.cs delete mode 100644 Program.cs create mode 100755 run-local.sh rename {Components => timetracker.Client/Components}/Layout/MainLayout.razor (95%) rename {Components => timetracker.Client/Components}/Layout/MainLayout.razor.css (100%) rename {Components => timetracker.Client/Components}/Layout/NavMenu.razor (98%) rename {Components => timetracker.Client/Components}/Layout/NavMenu.razor.css (100%) rename {Components => timetracker.Client/Components}/Pages/AdminUsers.razor (98%) rename {Components => timetracker.Client/Components}/Pages/Changelog.razor (83%) rename {Components => timetracker.Client/Components}/Pages/Feiertage.razor (99%) rename {Components => timetracker.Client/Components}/Pages/Home.razor (99%) rename {Components => timetracker.Client/Components}/Pages/Login.razor (64%) rename {Components => timetracker.Client/Components}/Pages/Month.razor (99%) rename {Components => timetracker.Client/Components}/Pages/NotFound.razor (100%) rename {Components => timetracker.Client/Components}/Pages/Settings.razor (99%) rename {Components => timetracker.Client/Components}/Pages/UrlaubsMaximizer.razor (99%) rename {Components => timetracker.Client/Components}/RedirectToLogin.razor (100%) rename {Components => timetracker.Client/Components}/Routes.razor (54%) create mode 100644 timetracker.Client/Components/_Imports.razor create mode 100644 timetracker.Client/Program.cs create mode 100644 timetracker.Client/Properties/launchSettings.json create mode 100644 timetracker.Client/Services/ClientAuthService.cs create mode 100644 timetracker.Client/Services/ClientHolidayService.cs create mode 100644 timetracker.Client/Services/ClientTimetrackerService.cs create mode 100644 timetracker.Client/Services/HostAuthenticationStateProvider.cs create mode 100644 timetracker.Client/Services/UserNotificationService.cs create mode 100644 timetracker.Client/_Imports.razor create mode 100644 timetracker.Client/timetracker.Client.csproj create mode 100644 timetracker.Client/wwwroot/icon-192.png rename {Components => timetracker.Server/Components}/App.razor (95%) rename {Components => timetracker.Server/Components}/Pages/Error.razor (100%) rename {Components => timetracker.Server/Components}/_Imports.razor (79%) rename {Data => timetracker.Server/Data}/AuthService.cs (97%) rename {Data => timetracker.Server/Data}/HolidayService.cs (97%) rename {Data => timetracker.Server/Data}/Migrations/20260520133634_Initial.Designer.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260520133634_Initial.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260520200000_AddPublicHolidays.Designer.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260520200000_AddPublicHolidays.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260522081459_AddMultiUser.Designer.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260522081459_AddMultiUser.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260607213215_AddFlexTimeAndHolidayState.Designer.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/20260607213215_AddFlexTimeAndHolidayState.cs (100%) rename {Data => timetracker.Server/Data}/Migrations/TimetrackerDbContextModelSnapshot.cs (100%) create mode 100644 timetracker.Server/Data/NotificationHub.cs rename {Data => timetracker.Server/Data}/TimetrackerDbContext.cs (95%) rename {Data => timetracker.Server/Data}/TimetrackerService.cs (99%) create mode 100644 timetracker.Server/Data/UserNotificationService.cs rename Dockerfile => timetracker.Server/Dockerfile (57%) create mode 100644 timetracker.Server/Program.cs rename {Properties => timetracker.Server/Properties}/launchSettings.json (100%) rename appsettings.Development.json => timetracker.Server/appsettings.Development.json (100%) rename appsettings.json => timetracker.Server/appsettings.json (100%) rename timetracker.csproj => timetracker.Server/timetracker.Server.csproj (59%) rename {wwwroot => timetracker.Server/wwwroot}/app.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/favicon.png (100%) rename {wwwroot => timetracker.Server/wwwroot}/favicon.svg (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.rtl.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.rtl.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.rtl.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.rtl.min.css (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.bundle.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.bundle.js.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.bundle.min.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.esm.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.esm.js.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.esm.min.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.esm.min.js.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.js.map (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.min.js (100%) rename {wwwroot => timetracker.Server/wwwroot}/lib/bootstrap/dist/js/bootstrap.min.js.map (100%) rename {Data => timetracker.Shared}/AppSettings.cs (97%) rename {Data => timetracker.Shared}/BreakEntry.cs (75%) create mode 100644 timetracker.Shared/IAuthService.cs create mode 100644 timetracker.Shared/IHolidayService.cs create mode 100644 timetracker.Shared/ITimetrackerService.cs create mode 100644 timetracker.Shared/IUserNotificationService.cs rename {Data => timetracker.Shared}/PublicHoliday.cs (86%) rename {Data => timetracker.Shared}/User.cs (87%) create mode 100644 timetracker.Shared/UserInfo.cs rename {Data => timetracker.Shared}/VacationDay.cs (85%) rename {Data => timetracker.Shared}/WorkDay.cs (90%) create mode 100644 timetracker.Shared/timetracker.Shared.csproj delete mode 100644 timetracker.db create mode 100644 timetracker.slnx diff --git a/Components/Layout/ReconnectModal.razor b/Components/Layout/ReconnectModal.razor deleted file mode 100644 index e740b0c..0000000 --- a/Components/Layout/ReconnectModal.razor +++ /dev/null @@ -1,31 +0,0 @@ - - - -
- -

- Rejoining the server... -

-

- Rejoin failed... trying again in seconds. -

-

- Failed to rejoin.
Please retry or reload the page. -

- -

- The session has been paused by the server. -

-

- Failed to resume the session.
Please retry or reload the page. -

- -
-
diff --git a/Components/Layout/ReconnectModal.razor.css b/Components/Layout/ReconnectModal.razor.css deleted file mode 100644 index 3ad3773..0000000 --- a/Components/Layout/ReconnectModal.razor.css +++ /dev/null @@ -1,157 +0,0 @@ -.components-reconnect-first-attempt-visible, -.components-reconnect-repeated-attempt-visible, -.components-reconnect-failed-visible, -.components-pause-visible, -.components-resume-failed-visible, -.components-rejoining-animation { - display: none; -} - -#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, -#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-paused .components-pause-visible, -#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, -#components-reconnect-modal.components-reconnect-retrying, -#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, -#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, -#components-reconnect-modal.components-reconnect-failed, -#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { - display: block; -} - - -#components-reconnect-modal { - background-color: white; - width: 20rem; - margin: 20vh auto; - padding: 2rem; - border: 0; - border-radius: 0.5rem; - box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); - opacity: 0; - transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; - animation: components-reconnect-modal-fadeOutOpacity 0.5s both; - &[open] - -{ - animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; - animation-fill-mode: both; -} - -} - -#components-reconnect-modal::backdrop { - background-color: rgba(0, 0, 0, 0.4); - animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; - opacity: 1; -} - -@keyframes components-reconnect-modal-slideUp { - 0% { - transform: translateY(30px) scale(0.95); - } - - 100% { - transform: translateY(0); - } -} - -@keyframes components-reconnect-modal-fadeInOpacity { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@keyframes components-reconnect-modal-fadeOutOpacity { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.components-reconnect-container { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -#components-reconnect-modal p { - margin: 0; - text-align: center; -} - -#components-reconnect-modal button { - border: 0; - background-color: #6b9ed2; - color: white; - padding: 4px 24px; - border-radius: 4px; -} - - #components-reconnect-modal button:hover { - background-color: #3b6ea2; - } - - #components-reconnect-modal button:active { - background-color: #6b9ed2; - } - -.components-rejoining-animation { - position: relative; - width: 80px; - height: 80px; -} - - .components-rejoining-animation div { - position: absolute; - border: 3px solid #0087ff; - opacity: 1; - border-radius: 50%; - animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; - } - - .components-rejoining-animation div:nth-child(2) { - animation-delay: -0.5s; - } - -@keyframes components-rejoining-animation { - 0% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 4.9% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 0; - } - - 5% { - top: 40px; - left: 40px; - width: 0; - height: 0; - opacity: 1; - } - - 100% { - top: 0px; - left: 0px; - width: 80px; - height: 80px; - opacity: 0; - } -} diff --git a/Components/Layout/ReconnectModal.razor.js b/Components/Layout/ReconnectModal.razor.js deleted file mode 100644 index a44de78..0000000 --- a/Components/Layout/ReconnectModal.razor.js +++ /dev/null @@ -1,63 +0,0 @@ -// Set up event handlers -const reconnectModal = document.getElementById("components-reconnect-modal"); -reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); - -const retryButton = document.getElementById("components-reconnect-button"); -retryButton.addEventListener("click", retry); - -const resumeButton = document.getElementById("components-resume-button"); -resumeButton.addEventListener("click", resume); - -function handleReconnectStateChanged(event) { - if (event.detail.state === "show") { - reconnectModal.showModal(); - } else if (event.detail.state === "hide") { - reconnectModal.close(); - } else if (event.detail.state === "failed") { - document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - } else if (event.detail.state === "rejected") { - location.reload(); - } -} - -async function retry() { - document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - - try { - // Reconnect will asynchronously return: - // - true to mean success - // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) - // - exception to mean we didn't reach the server (this can be sync or async) - const successful = await Blazor.reconnect(); - if (!successful) { - // We have been able to reach the server, but the circuit is no longer available. - // We'll reload the page so the user can continue using the app as quickly as possible. - const resumeSuccessful = await Blazor.resumeCircuit(); - if (!resumeSuccessful) { - location.reload(); - } else { - reconnectModal.close(); - } - } - } catch (err) { - // We got an exception, server is currently unavailable - document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); - } -} - -async function resume() { - try { - const successful = await Blazor.resumeCircuit(); - if (!successful) { - location.reload(); - } - } catch { - reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed"); - } -} - -async function retryWhenDocumentBecomesVisible() { - if (document.visibilityState === "visible") { - await retry(); - } -} diff --git a/Data/UserNotificationService.cs b/Data/UserNotificationService.cs deleted file mode 100644 index 683222d..0000000 --- a/Data/UserNotificationService.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace timetracker.Data; - -public class UserNotificationService -{ - public event Func? OnUsersChanged; - public event Func? OnUserDeleted; - - public async Task NotifyUsersChangedAsync() - { - if (OnUsersChanged != null) - await OnUsersChanged.Invoke(); - } - - public async Task NotifyUserDeletedAsync(int userId) - { - if (OnUserDeleted != null) - await OnUserDeleted.Invoke(userId); - } -} diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 41fe168..0000000 --- a/Program.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.EntityFrameworkCore; -using MudBlazor.Services; -using timetracker.Components; -using timetracker.Data; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => - { - options.LoginPath = "/login"; - options.LogoutPath = "/auth/logout"; - options.ExpireTimeSpan = TimeSpan.FromDays(30); - options.SlidingExpiration = true; - }); -builder.Services.AddAuthorization(options => -{ - options.AddPolicy("AdminOnly", policy => - policy.RequireClaim(System.Security.Claims.ClaimTypes.Name, "marc")); -}); -builder.Services.AddCascadingAuthenticationState(); -builder.Services.AddHttpContextAccessor(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - -// Add services to the container. -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents(); -builder.Services.AddMudServices(); -builder.Services.AddHttpClient(); - -var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH") - ?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db"); -builder.Services.AddDbContextFactory(options => - options.UseSqlite($"Data Source={dbPath}")); -builder.Services.AddScoped(); - -var app = builder.Build(); - -using (var scope = app.Services.CreateScope()) -{ - var factory = scope.ServiceProvider.GetRequiredService>(); - await using var db = await factory.CreateDbContextAsync(); - await db.Database.MigrateAsync(); -} - -var forwardedHeadersOptions = new ForwardedHeadersOptions -{ - ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto -}; -forwardedHeadersOptions.KnownProxies.Clear(); -forwardedHeadersOptions.KnownIPNetworks.Clear(); -app.UseForwardedHeaders(forwardedHeadersOptions); - -// Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} -app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); -if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelopment())) -{ - app.UseHttpsRedirection(); -} - -// Statische Dateien (inkl. _framework/, _content/) vor Auth bedienen, -// damit Blazor-JS und MudBlazor-CSS nie durch Auth-Middleware geblockt werden -app.UseStaticFiles(); - -app.UseAuthentication(); -app.UseAuthorization(); - -app.UseAntiforgery(); - -app.MapStaticAssets(); -app.MapRazorComponents() - .AddInteractiveServerRenderMode(); - -// ── Auth-Endpoints ──────────────────────────────────────────────────────────── -app.MapPost("/auth/login", async (HttpContext ctx, AuthService authService) => -{ - var form = await ctx.Request.ReadFormAsync(); - var username = form["username"].ToString(); - var password = form["password"].ToString(); - var user = await authService.LoginAsync(username, password); - if (user == null) - return Results.Redirect("/login?error=invalid"); - - var claims = new[] { - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), - new Claim(ClaimTypes.Name, user.Username) - }; - var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - new AuthenticationProperties { IsPersistent = true }); - return Results.Redirect("/"); -}).DisableAntiforgery(); - -app.MapPost("/auth/register", async (HttpContext ctx, AuthService authService) => -{ - var form = await ctx.Request.ReadFormAsync(); - var username = form["username"].ToString(); - var password = form["password"].ToString(); - var (user, error) = await authService.RegisterAsync(username, password); - if (user == null) - return Results.Redirect($"/login?tab=register&error={Uri.EscapeDataString(error!)}"); - - var claims = new[] { - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), - new Claim(ClaimTypes.Name, user.Username) - }; - var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, - new ClaimsPrincipal(identity), - new AuthenticationProperties { IsPersistent = true }); - return Results.Redirect("/"); -}).DisableAntiforgery(); - -app.MapGet("/auth/logout", async (HttpContext ctx) => -{ - await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - return Results.Redirect("/login"); -}); - -app.Run(); diff --git a/docker-compose.yml b/docker-compose.yml index c998785..35c62fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,36 @@ version: '3.8' services: + db: + image: postgres:16-alpine + container_name: timetracker-db + restart: always + environment: + POSTGRES_USER: timetracker_user + POSTGRES_PASSWORD: SecretPassword123 + POSTGRES_DB: timetracker + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + timetracker: build: context: . - dockerfile: Dockerfile + dockerfile: timetracker.Server/Dockerfile container_name: timetracker-app restart: always ports: - "8090:8080" environment: - ASPNETCORE_ENVIRONMENT=Production - - ASPNETCORE_HTTP_PORTS=8080 # Lassen wir so, da intern völlig okay - - TIMETRACKER_DB_PATH=/data/timetracker.db + - ASPNETCORE_HTTP_PORTS=8080 + - DB_PROVIDER=PostgreSQL + - ConnectionStrings__DefaultConnection=Host=db;Database=timetracker;Username=timetracker_user;Password=SecretPassword123; - EnableHttpsRedirect=false - volumes: - - timetracker_data:/data + depends_on: + - db volumes: - timetracker_data: - name: timetracker_prod_data \ No newline at end of file + pgdata: + name: timetracker_pgdata \ No newline at end of file diff --git a/run-local.sh b/run-local.sh new file mode 100755 index 0000000..e41d113 --- /dev/null +++ b/run-local.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Exit on error +set -e + +echo "=== 1. Bereinige alte Build-Dateien ===" +dotnet clean + +echo "=== 2. Baue die gesamte Solution ===" +dotnet build + +echo "=== 3. Starte den Server & öffne Browser ===" +# Browser parallel nach kurzem Delay öffnen +(sleep 3 && open http://localhost:5065) & + +dotnet run --project timetracker.Server/timetracker.Server.csproj diff --git a/Components/Layout/MainLayout.razor b/timetracker.Client/Components/Layout/MainLayout.razor similarity index 95% rename from Components/Layout/MainLayout.razor rename to timetracker.Client/Components/Layout/MainLayout.razor index e08a347..e2caf38 100644 --- a/Components/Layout/MainLayout.razor +++ b/timetracker.Client/Components/Layout/MainLayout.razor @@ -1,7 +1,7 @@ -@inherits LayoutComponentBase +@inherits LayoutComponentBase @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager Nav -@inject timetracker.Data.UserNotificationService UserNotificationService +@inject IUserNotificationService UserNotificationService @implements IDisposable @using System.Security.Claims diff --git a/Components/Layout/MainLayout.razor.css b/timetracker.Client/Components/Layout/MainLayout.razor.css similarity index 100% rename from Components/Layout/MainLayout.razor.css rename to timetracker.Client/Components/Layout/MainLayout.razor.css diff --git a/Components/Layout/NavMenu.razor b/timetracker.Client/Components/Layout/NavMenu.razor similarity index 98% rename from Components/Layout/NavMenu.razor rename to timetracker.Client/Components/Layout/NavMenu.razor index f10b3dd..37c9aca 100644 --- a/Components/Layout/NavMenu.razor +++ b/timetracker.Client/Components/Layout/NavMenu.razor @@ -29,7 +29,7 @@ - Version 1.1 + Version 1.2 diff --git a/Components/Layout/NavMenu.razor.css b/timetracker.Client/Components/Layout/NavMenu.razor.css similarity index 100% rename from Components/Layout/NavMenu.razor.css rename to timetracker.Client/Components/Layout/NavMenu.razor.css diff --git a/Components/Pages/AdminUsers.razor b/timetracker.Client/Components/Pages/AdminUsers.razor similarity index 98% rename from Components/Pages/AdminUsers.razor rename to timetracker.Client/Components/Pages/AdminUsers.razor index cef5c86..5039fbc 100644 --- a/Components/Pages/AdminUsers.razor +++ b/timetracker.Client/Components/Pages/AdminUsers.razor @@ -1,10 +1,10 @@ @page "/admin/users" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize(Policy = "AdminOnly")] -@inject AuthService AuthService +@inject IAuthService AuthService @inject ISnackbar Snackbar @inject AuthenticationStateProvider AuthStateProvider -@inject timetracker.Data.UserNotificationService UserNotificationService +@inject IUserNotificationService UserNotificationService @implements IDisposable Benutzerverwaltung – Timetracker diff --git a/Components/Pages/Changelog.razor b/timetracker.Client/Components/Pages/Changelog.razor similarity index 83% rename from Components/Pages/Changelog.razor rename to timetracker.Client/Components/Pages/Changelog.razor index d806a7e..afe9fc6 100644 --- a/Components/Pages/Changelog.razor +++ b/timetracker.Client/Components/Pages/Changelog.razor @@ -1,5 +1,5 @@ @page "/changelog" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] Changelog – Timetracker @@ -68,7 +68,15 @@ private readonly List _releases = [ - new("1.1", "08.06.2026", true, + new("1.2", "08.06.2026", true, + [ + new("Upgrade", "Architektur-Migration auf Hosted Blazor WebAssembly (.NET 10) mit sauberer Projektstruktur (Client, Server, Shared)"), + new("Neu", "Unterstützung für PostgreSQL-Datenbanken als produktive und skalierbare Alternative zu SQLite"), + new("Neu", "Dynamische DB-Provider-Weiche (SQLite vs. PostgreSQL) über Konfigurations- und Umgebungsvariablen"), + new("Neu", "Docker-Compose-Konfiguration inklusive PostgreSQL-Container für vereinfachten Deployment-Betrieb"), + new("Neu", "Lokales Ausführungsskript (run-local.sh) für einfaches Testen auf dem Entwicklungsrechner"), + ]), + new("1.1", "08.06.2026", false, [ new("Neu", "Versionsnummer in der Navbar mit Link zum Changelog"), new("Neu", "Changelog-Seite"), diff --git a/Components/Pages/Feiertage.razor b/timetracker.Client/Components/Pages/Feiertage.razor similarity index 99% rename from Components/Pages/Feiertage.razor rename to timetracker.Client/Components/Pages/Feiertage.razor index bd061c0..7e2debc 100644 --- a/Components/Pages/Feiertage.razor +++ b/timetracker.Client/Components/Pages/Feiertage.razor @@ -1,8 +1,8 @@ @page "/feiertage" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] -@inject HolidayService HolidayService -@inject TimetrackerService TrackerService +@inject IHolidayService HolidayService +@inject ITimetrackerService TrackerService @inject AuthenticationStateProvider AuthStateProvider Feiertage – Timetracker diff --git a/Components/Pages/Home.razor b/timetracker.Client/Components/Pages/Home.razor similarity index 99% rename from Components/Pages/Home.razor rename to timetracker.Client/Components/Pages/Home.razor index 72bd5cc..b7da6e6 100644 --- a/Components/Pages/Home.razor +++ b/timetracker.Client/Components/Pages/Home.razor @@ -1,8 +1,8 @@ @page "/" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] -@inject TimetrackerService TrackerService -@inject HolidayService HolidayService +@inject ITimetrackerService TrackerService +@inject IHolidayService HolidayService @inject ISnackbar Snackbar @inject AuthenticationStateProvider AuthStateProvider diff --git a/Components/Pages/Login.razor b/timetracker.Client/Components/Pages/Login.razor similarity index 64% rename from Components/Pages/Login.razor rename to timetracker.Client/Components/Pages/Login.razor index d0783e8..c7ff18f 100644 --- a/Components/Pages/Login.razor +++ b/timetracker.Client/Components/Pages/Login.razor @@ -1,5 +1,7 @@ @page "/login" +@rendermode InteractiveWebAssembly @attribute [AllowAnonymous] +@inject IAuthService AuthService @inject NavigationManager Nav Anmelden – Timetracker @@ -15,15 +17,15 @@ - @* ── Static Tab Navigation ── *@ + @* ── Tab Navigation ── *@ - Anmelden - @@ -41,14 +43,14 @@ { @_error } -
+ + Class="mt-2" + Disabled="_loading"> + @if (_loading) + { + + } Anmelden - +
} else @@ -80,14 +87,14 @@ { @_error } -
+ + Class="mt-2" + Disabled="_loading"> + @if (_loading) + { + + } Konto erstellen - +
}
@@ -119,6 +131,10 @@ @code { private int _activeTab = 0; private string? _error; + private bool _loading; + + private readonly AuthModel _loginModel = new(); + private readonly AuthModel _registerModel = new(); [SupplyParameterFromQuery(Name = "error")] public string? ErrorParam { get; set; } @@ -136,5 +152,68 @@ }; _activeTab = TabParam == "register" ? 1 : 0; } -} + private void SetTab(int tab) + { + _activeTab = tab; + _error = null; + } + + private async Task HandleLogin() + { + _loading = true; + _error = null; + try + { + var user = await AuthService.LoginAsync(_loginModel.Username, _loginModel.Password); + if (user != null) + { + Nav.NavigateTo("/", forceLoad: true); // forceLoad forces state update/re-render of the root app + } + else + { + _error = "Benutzername oder Passwort falsch."; + } + } + catch (Exception ex) + { + _error = $"Login Fehler: {ex.Message}"; + } + finally + { + _loading = false; + } + } + + private async Task HandleRegister() + { + _loading = true; + _error = null; + try + { + var (user, error) = await AuthService.RegisterAsync(_registerModel.Username, _registerModel.Password); + if (user != null) + { + Nav.NavigateTo("/", forceLoad: true); + } + else + { + _error = error ?? "Registrierung fehlgeschlagen."; + } + } + catch (Exception ex) + { + _error = $"Registrierungs Fehler: {ex.Message}"; + } + finally + { + _loading = false; + } + } + + private class AuthModel + { + public string Username { get; set; } = ""; + public string Password { get; set; } = ""; + } +} diff --git a/Components/Pages/Month.razor b/timetracker.Client/Components/Pages/Month.razor similarity index 99% rename from Components/Pages/Month.razor rename to timetracker.Client/Components/Pages/Month.razor index 2f602c9..31c03e2 100644 --- a/Components/Pages/Month.razor +++ b/timetracker.Client/Components/Pages/Month.razor @@ -1,8 +1,8 @@ @page "/month" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] -@inject TimetrackerService TrackerService -@inject HolidayService HolidayService +@inject ITimetrackerService TrackerService +@inject IHolidayService HolidayService @inject AuthenticationStateProvider AuthStateProvider @_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker diff --git a/Components/Pages/NotFound.razor b/timetracker.Client/Components/Pages/NotFound.razor similarity index 100% rename from Components/Pages/NotFound.razor rename to timetracker.Client/Components/Pages/NotFound.razor diff --git a/Components/Pages/Settings.razor b/timetracker.Client/Components/Pages/Settings.razor similarity index 99% rename from Components/Pages/Settings.razor rename to timetracker.Client/Components/Pages/Settings.razor index 336cdee..117c489 100644 --- a/Components/Pages/Settings.razor +++ b/timetracker.Client/Components/Pages/Settings.razor @@ -1,8 +1,8 @@ @page "/settings" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] -@inject TimetrackerService TrackerService -@inject HolidayService HolidayService +@inject ITimetrackerService TrackerService +@inject IHolidayService HolidayService @inject ISnackbar Snackbar @inject AuthenticationStateProvider AuthStateProvider diff --git a/Components/Pages/UrlaubsMaximizer.razor b/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor similarity index 99% rename from Components/Pages/UrlaubsMaximizer.razor rename to timetracker.Client/Components/Pages/UrlaubsMaximizer.razor index e0ef586..11e01ad 100644 --- a/Components/Pages/UrlaubsMaximizer.razor +++ b/timetracker.Client/Components/Pages/UrlaubsMaximizer.razor @@ -1,8 +1,8 @@ @page "/urlaub-maximizer" -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly @attribute [Authorize] -@inject TimetrackerService TrackerService -@inject HolidayService HolidayService +@inject ITimetrackerService TrackerService +@inject IHolidayService HolidayService @inject ISnackbar Snackbar @inject AuthenticationStateProvider AuthStateProvider diff --git a/Components/RedirectToLogin.razor b/timetracker.Client/Components/RedirectToLogin.razor similarity index 100% rename from Components/RedirectToLogin.razor rename to timetracker.Client/Components/RedirectToLogin.razor diff --git a/Components/Routes.razor b/timetracker.Client/Components/Routes.razor similarity index 54% rename from Components/Routes.razor rename to timetracker.Client/Components/Routes.razor index ae21609..e8f40fd 100644 --- a/Components/Routes.razor +++ b/timetracker.Client/Components/Routes.razor @@ -1,10 +1,10 @@ -@rendermode InteractiveServer +@rendermode InteractiveWebAssembly - + - + - + diff --git a/timetracker.Client/Components/_Imports.razor b/timetracker.Client/Components/_Imports.razor new file mode 100644 index 0000000..903abf5 --- /dev/null +++ b/timetracker.Client/Components/_Imports.razor @@ -0,0 +1,17 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using System.Security.Claims +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using timetracker.Client +@using timetracker.Client.Components +@using timetracker.Client.Components.Layout +@using timetracker.Client.Services +@using timetracker.Shared +@using MudBlazor diff --git a/timetracker.Client/Program.cs b/timetracker.Client/Program.cs new file mode 100644 index 0000000..d36d72e --- /dev/null +++ b/timetracker.Client/Program.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using timetracker.Client.Services; +using timetracker.Shared; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +builder.Services.AddMudServices(); + +builder.Services.AddAuthorizationCore(options => +{ + options.AddPolicy("AdminOnly", policy => + policy.RequireClaim(System.Security.Claims.ClaimTypes.Name, "marc")); +}); + +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => sp.GetRequiredService()); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +await builder.Build().RunAsync(); diff --git a/timetracker.Client/Properties/launchSettings.json b/timetracker.Client/Properties/launchSettings.json new file mode 100644 index 0000000..316e152 --- /dev/null +++ b/timetracker.Client/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5016", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7270;http://localhost:5016", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/timetracker.Client/Services/ClientAuthService.cs b/timetracker.Client/Services/ClientAuthService.cs new file mode 100644 index 0000000..ceaade3 --- /dev/null +++ b/timetracker.Client/Services/ClientAuthService.cs @@ -0,0 +1,74 @@ +using System.Net.Http.Json; +using Microsoft.AspNetCore.Components.Authorization; +using timetracker.Shared; + +namespace timetracker.Client.Services; + +public class ClientAuthService : IAuthService +{ + private readonly HttpClient _http; + private readonly AuthenticationStateProvider _authStateProvider; + + public ClientAuthService(HttpClient http, AuthenticationStateProvider authStateProvider) + { + _http = http; + _authStateProvider = authStateProvider; + } + + private HostAuthenticationStateProvider HostProvider => (HostAuthenticationStateProvider)_authStateProvider; + + public async Task LoginAsync(string username, string password) + { + var response = await _http.PostAsJsonAsync("api/auth/login", new { Username = username, Password = password }); + if (response.IsSuccessStatusCode) + { + var userInfo = await response.Content.ReadFromJsonAsync(); + if (userInfo != null) + { + HostProvider.NotifyUserChanged(userInfo); + return new User { Id = userInfo.Id, Username = userInfo.Username }; + } + } + return null; + } + + public async Task<(User? User, string? Error)> RegisterAsync(string username, string password) + { + var response = await _http.PostAsJsonAsync("api/auth/register", new { Username = username, Password = password }); + if (response.IsSuccessStatusCode) + { + var userInfo = await response.Content.ReadFromJsonAsync(); + if (userInfo != null) + { + HostProvider.NotifyUserChanged(userInfo); + return (new User { Id = userInfo.Id, Username = userInfo.Username }, null); + } + } + else + { + var error = await response.Content.ReadAsStringAsync(); + return (null, error); + } + return (null, "Registrierung fehlgeschlagen."); + } + + public async Task> GetAllUsersAsync() + { + return await _http.GetFromJsonAsync>("api/users") ?? []; + } + + public async Task DeleteUserAsync(int userId) + { + await _http.DeleteAsync($"api/users/{userId}"); + } + + public async Task RenameUserAsync(int userId, string newUsername) + { + var response = await _http.PutAsJsonAsync($"api/users/{userId}/rename", new { Username = newUsername }); + if (!response.IsSuccessStatusCode) + { + return await response.Content.ReadAsStringAsync(); + } + return null; + } +} diff --git a/timetracker.Client/Services/ClientHolidayService.cs b/timetracker.Client/Services/ClientHolidayService.cs new file mode 100644 index 0000000..4acdaf0 --- /dev/null +++ b/timetracker.Client/Services/ClientHolidayService.cs @@ -0,0 +1,49 @@ +using System.Net.Http.Json; +using timetracker.Shared; + +namespace timetracker.Client.Services; + +public class ClientHolidayService : IHolidayService +{ + private readonly HttpClient _http; + + public ClientHolidayService(HttpClient http) + { + _http = http; + } + + public async Task> GetHolidaysAsync(int year, string? stateCode = null) + { + var url = $"api/holidays?year={year}"; + if (!string.IsNullOrEmpty(stateCode)) + { + url += $"&stateCode={stateCode}"; + } + return await _http.GetFromJsonAsync>(url) ?? []; + } + + public async Task<(bool Success, string Message)> FetchAndStoreAsync(int year) + { + var response = await _http.PostAsync($"api/holidays/fetch/{year}", null); + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(); + if (result != null) + { + return (result.Success, result.Message); + } + } + return (false, "Fehler beim Abrufen der Feiertage."); + } + + public async Task DeleteAsync(int id) + { + await _http.DeleteAsync($"api/holidays/{id}"); + } + + private class FetchResponse + { + public bool Success { get; set; } + public string Message { get; set; } = ""; + } +} diff --git a/timetracker.Client/Services/ClientTimetrackerService.cs b/timetracker.Client/Services/ClientTimetrackerService.cs new file mode 100644 index 0000000..cd35d5d --- /dev/null +++ b/timetracker.Client/Services/ClientTimetrackerService.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Json; +using timetracker.Shared; + +namespace timetracker.Client.Services; + +public class ClientTimetrackerService : ITimetrackerService +{ + private readonly HttpClient _http; + + public ClientTimetrackerService(HttpClient http) + { + _http = http; + } + + public async Task> GetWeekAsync(int userId, DateOnly monday) + { + return await _http.GetFromJsonAsync>($"api/tracker/week?userId={userId}&monday={monday:yyyy-MM-dd}") ?? []; + } + + public async Task UpsertWorkDayAsync(WorkDay workDay) + { + await _http.PostAsJsonAsync("api/tracker/workday", workDay); + } + + public async Task GetSettingsAsync(int userId) + { + return await _http.GetFromJsonAsync($"api/tracker/settings/{userId}") ?? new AppSettings { UserId = userId }; + } + + public async Task SaveSettingsAsync(AppSettings settings) + { + await _http.PostAsJsonAsync("api/tracker/settings", settings); + } + + public async Task> GetVacationDaysAsync(int userId, int year) + { + return await _http.GetFromJsonAsync>($"api/tracker/vacation/{userId}/{year}") ?? []; + } + + public async Task AddVacationDayAsync(VacationDay vacationDay) + { + await _http.PostAsJsonAsync("api/tracker/vacation", vacationDay); + } + + public async Task RemoveVacationDayAsync(int userId, int id) + { + await _http.DeleteAsync($"api/tracker/vacation/{userId}/{id}"); + } + + public async Task GetTotalOvertimeAsync(int userId, AppSettings settings) + { + var response = await _http.PostAsJsonAsync($"api/tracker/overtime/{userId}", settings); + if (response.IsSuccessStatusCode) + { + var hours = await response.Content.ReadFromJsonAsync(); + return TimeSpan.FromHours(hours); + } + return TimeSpan.Zero; + } + + public async Task> GetMonthAsync(int userId, int year, int month) + { + return await _http.GetFromJsonAsync>($"api/tracker/month?userId={userId}&year={year}&month={month}") ?? []; + } +} diff --git a/timetracker.Client/Services/HostAuthenticationStateProvider.cs b/timetracker.Client/Services/HostAuthenticationStateProvider.cs new file mode 100644 index 0000000..655f30a --- /dev/null +++ b/timetracker.Client/Services/HostAuthenticationStateProvider.cs @@ -0,0 +1,73 @@ +using System.Security.Claims; +using System.Net.Http.Json; +using Microsoft.AspNetCore.Components.Authorization; +using timetracker.Shared; + +namespace timetracker.Client.Services; + +public class HostAuthenticationStateProvider : AuthenticationStateProvider +{ + private readonly HttpClient _http; + private static readonly ClaimsPrincipal Anonymous = new(new ClaimsIdentity()); + private ClaimsPrincipal? _currentUser; + + public HostAuthenticationStateProvider(HttpClient http) + { + _http = http; + } + + public override async Task GetAuthenticationStateAsync() + { + if (_currentUser != null) + { + return new AuthenticationState(_currentUser); + } + + try + { + var response = await _http.GetAsync("api/auth/me"); + if (response.IsSuccessStatusCode) + { + var userInfo = await response.Content.ReadFromJsonAsync(); + if (userInfo != null) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString()), + new Claim(ClaimTypes.Name, userInfo.Username) + }; + var identity = new ClaimsIdentity(claims, "Cookie"); + _currentUser = new ClaimsPrincipal(identity); + return new AuthenticationState(_currentUser); + } + } + } + catch + { + // Ignore error and fall back to anonymous (e.g. server offline or network issues) + } + + _currentUser = Anonymous; + return new AuthenticationState(_currentUser); + } + + public void NotifyUserChanged(UserInfo? userInfo) + { + if (userInfo != null) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userInfo.Id.ToString()), + new Claim(ClaimTypes.Name, userInfo.Username) + }; + var identity = new ClaimsIdentity(claims, "Cookie"); + _currentUser = new ClaimsPrincipal(identity); + } + else + { + _currentUser = Anonymous; + } + + NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_currentUser))); + } +} diff --git a/timetracker.Client/Services/UserNotificationService.cs b/timetracker.Client/Services/UserNotificationService.cs new file mode 100644 index 0000000..69e088e --- /dev/null +++ b/timetracker.Client/Services/UserNotificationService.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.SignalR.Client; +using timetracker.Shared; + +namespace timetracker.Client.Services; + +public class UserNotificationService : IUserNotificationService, IAsyncDisposable +{ + private readonly HubConnection _hubConnection; + + public event Func? OnUsersChanged; + public event Func? OnUserDeleted; + + public UserNotificationService(NavigationManager navigationManager) + { + _hubConnection = new HubConnectionBuilder() + .WithUrl(navigationManager.ToAbsoluteUri("/hubs/notifications")) + .WithAutomaticReconnect() + .Build(); + + _hubConnection.On("UsersChanged", async () => + { + if (OnUsersChanged != null) + await OnUsersChanged.Invoke(); + }); + + _hubConnection.On("UserDeleted", async (userId) => + { + if (OnUserDeleted != null) + await OnUserDeleted.Invoke(userId); + }); + + // Start connection asynchronously + _ = StartHubConnectionAsync(); + } + + private async Task StartHubConnectionAsync() + { + try + { + await _hubConnection.StartAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"SignalR Connection Error: {ex.Message}"); + } + } + + public async ValueTask DisposeAsync() + { + if (_hubConnection != null) + { + await _hubConnection.DisposeAsync(); + } + } +} diff --git a/timetracker.Client/_Imports.razor b/timetracker.Client/_Imports.razor new file mode 100644 index 0000000..71848dc --- /dev/null +++ b/timetracker.Client/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using timetracker.Client +@using timetracker.Client.Components +@using timetracker.Client.Components.Layout +@using timetracker.Shared +@using MudBlazor diff --git a/timetracker.Client/timetracker.Client.csproj b/timetracker.Client/timetracker.Client.csproj new file mode 100644 index 0000000..06712b5 --- /dev/null +++ b/timetracker.Client/timetracker.Client.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/timetracker.Client/wwwroot/icon-192.png b/timetracker.Client/wwwroot/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..166f56da7612ea74df6a297154c8d281a4f28a14 GIT binary patch literal 2626 zcmV-I3cdA-P)v0A9xRwxP|bki~~&uFk>U z#P+PQh zyZ;-jwXKqnKbb6)@RaxQz@vm={%t~VbaZrdbaZrdbaeEeXj>~BG?&`J0XrqR#sSlO zg~N5iUk*15JibvlR1f^^1czzNKWvoJtc!Sj*G37QXbZ8LeD{Fzxgdv#Q{x}ytfZ5q z+^k#NaEp>zX_8~aSaZ`O%B9C&YLHb(mNtgGD&Kezd5S@&C=n~Uy1NWHM`t07VQP^MopUXki{2^#ryd94>UJMYW|(#4qV`kb7eD)Q=~NN zaVIRi@|TJ!Rni8J=5DOutQ#bEyMVr8*;HU|)MEKmVC+IOiDi9y)vz=rdtAUHW$yjt zrj3B7v(>exU=IrzC<+?AE=2vI;%fafM}#ShGDZx=0Nus5QHKdyb9pw&4>4XCpa-o?P(Gnco1CGX|U> z$f+_tA3+V~<{MU^A%eP!8R*-sD9y<>Jc7A(;aC5hVbs;kX9&Sa$JMG!W_BLFQa*hM zri__C@0i0U1X#?)Y=)>JpvTnY6^s;fu#I}K9u>OldV}m!Ch`d1Vs@v9 zb}w(!TvOmSzmMBa9gYvD4xocL2r0ds6%Hs>Z& z#7#o9PGHDmfG%JQq`O5~dt|MAQN@2wyJw_@``7Giyy(yyk(m8U*kk5$X1^;3$a3}N^Lp6hE5!#8l z#~NYHmKAs6IAe&A;bvM8OochRmXN>`D`{N$%#dZCRxp4-dJ?*3P}}T`tYa3?zz5BA zTu7uE#GsDpZ$~j9q=Zq!LYjLbZPXFILZK4?S)C-zE1(dC2d<7nO4-nSCbV#9E|E1MM|V<9>i4h?WX*r*ul1 z5#k6;po8z=fdMiVVz*h+iaTlz#WOYmU^SX5#97H~B32s-#4wk<1NTN#g?LrYieCu> zF7pbOLR;q2D#Q`^t%QcY06*X-jM+ei7%ZuanUTH#9Y%FBi*Z#22({_}3^=BboIsbg zR0#jJ>9QR8SnmtSS6x($?$}6$x+q)697#m${Z@G6Ujf=6iO^S}7P`q8DkH!IHd4lB zDzwxt3BHsPAcXFFY^Fj}(073>NL_$A%v2sUW(CRutd%{G`5ow?L`XYSO*Qu?x+Gzv zBtR}Y6`XF4xX7)Z04D+fH;TMapdQFFameUuHL34NN)r@aF4RO%x&NApeWGtr#mG~M z6sEIZS;Uj1HB1*0hh=O@0q1=Ia@L>-tETu-3n(op+97E z#&~2xggrl(LA|giII;RwBlX2^Q`B{_t}gxNL;iB11gEPC>v` zb4SJ;;BFOB!{chn>?cCeGDKuqI0+!skyWTn*k!WiPNBf=8rn;@y%( znhq%8fj2eAe?`A5mP;TE&iLEmQ^xV%-kmC-8mWao&EUK_^=GW-Y3z ksi~={si~={skwfB0gq6itke#r1ONa407*qoM6N<$g11Kq@c;k- literal 0 HcmV?d00001 diff --git a/Components/App.razor b/timetracker.Server/Components/App.razor similarity index 95% rename from Components/App.razor rename to timetracker.Server/Components/App.razor index 990baf9..8d383fd 100644 --- a/Components/App.razor +++ b/timetracker.Server/Components/App.razor @@ -1,4 +1,4 @@ - + @@ -18,7 +18,6 @@ - diff --git a/Components/Pages/Error.razor b/timetracker.Server/Components/Pages/Error.razor similarity index 100% rename from Components/Pages/Error.razor rename to timetracker.Server/Components/Pages/Error.razor diff --git a/Components/_Imports.razor b/timetracker.Server/Components/_Imports.razor similarity index 79% rename from Components/_Imports.razor rename to timetracker.Server/Components/_Imports.razor index 622292a..1cc17b6 100644 --- a/Components/_Imports.razor +++ b/timetracker.Server/Components/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using System.Net.Http.Json @using System.Security.Claims @using Microsoft.AspNetCore.Authorization @@ -10,7 +10,8 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop @using timetracker -@using timetracker.Components -@using timetracker.Components.Layout +@using timetracker.Client.Components +@using timetracker.Client.Components.Layout @using timetracker.Data +@using timetracker.Shared @using MudBlazor diff --git a/Data/AuthService.cs b/timetracker.Server/Data/AuthService.cs similarity index 97% rename from Data/AuthService.cs rename to timetracker.Server/Data/AuthService.cs index 7b91746..3f4e1a0 100644 --- a/Data/AuthService.cs +++ b/timetracker.Server/Data/AuthService.cs @@ -1,10 +1,11 @@ using System.Security.Cryptography; using System.Text; using Microsoft.EntityFrameworkCore; +using timetracker.Shared; namespace timetracker.Data; -public class AuthService(IDbContextFactory factory, UserNotificationService notifier) +public class AuthService(IDbContextFactory factory, UserNotificationService notifier) : IAuthService { public async Task LoginAsync(string username, string password) { diff --git a/Data/HolidayService.cs b/timetracker.Server/Data/HolidayService.cs similarity index 97% rename from Data/HolidayService.cs rename to timetracker.Server/Data/HolidayService.cs index 0f611e8..dbcbfa0 100644 --- a/Data/HolidayService.cs +++ b/timetracker.Server/Data/HolidayService.cs @@ -1,10 +1,11 @@ using Microsoft.EntityFrameworkCore; using System.Net.Http.Json; using System.Text.Json.Serialization; +using timetracker.Shared; namespace timetracker.Data; -public class HolidayService(IDbContextFactory factory, HttpClient http) +public class HolidayService(IDbContextFactory factory, HttpClient http) : IHolidayService { private const string ApiUrl = "https://date.nager.at/api/v3/PublicHolidays/{0}/DE"; diff --git a/Data/Migrations/20260520133634_Initial.Designer.cs b/timetracker.Server/Data/Migrations/20260520133634_Initial.Designer.cs similarity index 100% rename from Data/Migrations/20260520133634_Initial.Designer.cs rename to timetracker.Server/Data/Migrations/20260520133634_Initial.Designer.cs diff --git a/Data/Migrations/20260520133634_Initial.cs b/timetracker.Server/Data/Migrations/20260520133634_Initial.cs similarity index 100% rename from Data/Migrations/20260520133634_Initial.cs rename to timetracker.Server/Data/Migrations/20260520133634_Initial.cs diff --git a/Data/Migrations/20260520200000_AddPublicHolidays.Designer.cs b/timetracker.Server/Data/Migrations/20260520200000_AddPublicHolidays.Designer.cs similarity index 100% rename from Data/Migrations/20260520200000_AddPublicHolidays.Designer.cs rename to timetracker.Server/Data/Migrations/20260520200000_AddPublicHolidays.Designer.cs diff --git a/Data/Migrations/20260520200000_AddPublicHolidays.cs b/timetracker.Server/Data/Migrations/20260520200000_AddPublicHolidays.cs similarity index 100% rename from Data/Migrations/20260520200000_AddPublicHolidays.cs rename to timetracker.Server/Data/Migrations/20260520200000_AddPublicHolidays.cs diff --git a/Data/Migrations/20260522081459_AddMultiUser.Designer.cs b/timetracker.Server/Data/Migrations/20260522081459_AddMultiUser.Designer.cs similarity index 100% rename from Data/Migrations/20260522081459_AddMultiUser.Designer.cs rename to timetracker.Server/Data/Migrations/20260522081459_AddMultiUser.Designer.cs diff --git a/Data/Migrations/20260522081459_AddMultiUser.cs b/timetracker.Server/Data/Migrations/20260522081459_AddMultiUser.cs similarity index 100% rename from Data/Migrations/20260522081459_AddMultiUser.cs rename to timetracker.Server/Data/Migrations/20260522081459_AddMultiUser.cs diff --git a/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.Designer.cs b/timetracker.Server/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.Designer.cs similarity index 100% rename from Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.Designer.cs rename to timetracker.Server/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.Designer.cs diff --git a/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.cs b/timetracker.Server/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.cs similarity index 100% rename from Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.cs rename to timetracker.Server/Data/Migrations/20260607213215_AddFlexTimeAndHolidayState.cs diff --git a/Data/Migrations/TimetrackerDbContextModelSnapshot.cs b/timetracker.Server/Data/Migrations/TimetrackerDbContextModelSnapshot.cs similarity index 100% rename from Data/Migrations/TimetrackerDbContextModelSnapshot.cs rename to timetracker.Server/Data/Migrations/TimetrackerDbContextModelSnapshot.cs diff --git a/timetracker.Server/Data/NotificationHub.cs b/timetracker.Server/Data/NotificationHub.cs new file mode 100644 index 0000000..f2ab32c --- /dev/null +++ b/timetracker.Server/Data/NotificationHub.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.SignalR; + +namespace timetracker.Data; + +public class NotificationHub : Hub +{ +} diff --git a/Data/TimetrackerDbContext.cs b/timetracker.Server/Data/TimetrackerDbContext.cs similarity index 95% rename from Data/TimetrackerDbContext.cs rename to timetracker.Server/Data/TimetrackerDbContext.cs index bb04343..1d23e64 100644 --- a/Data/TimetrackerDbContext.cs +++ b/timetracker.Server/Data/TimetrackerDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using timetracker.Shared; namespace timetracker.Data; diff --git a/Data/TimetrackerService.cs b/timetracker.Server/Data/TimetrackerService.cs similarity index 99% rename from Data/TimetrackerService.cs rename to timetracker.Server/Data/TimetrackerService.cs index c008d24..349e754 100644 --- a/Data/TimetrackerService.cs +++ b/timetracker.Server/Data/TimetrackerService.cs @@ -1,8 +1,9 @@ using Microsoft.EntityFrameworkCore; +using timetracker.Shared; namespace timetracker.Data; -public class TimetrackerService(IDbContextFactory factory) +public class TimetrackerService(IDbContextFactory factory) : ITimetrackerService { public async Task> GetWeekAsync(int userId, DateOnly monday) { diff --git a/timetracker.Server/Data/UserNotificationService.cs b/timetracker.Server/Data/UserNotificationService.cs new file mode 100644 index 0000000..579a083 --- /dev/null +++ b/timetracker.Server/Data/UserNotificationService.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.SignalR; +using timetracker.Shared; + +namespace timetracker.Data; + +public class UserNotificationService : IUserNotificationService +{ + private readonly IHubContext _hubContext; + + public event Func? OnUsersChanged; + public event Func? OnUserDeleted; + + public UserNotificationService(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public async Task NotifyUsersChangedAsync() + { + // Broadcast via SignalR to all clients + await _hubContext.Clients.All.SendAsync("UsersChanged"); + + // Also trigger locally if there are server-side subscribers + if (OnUsersChanged != null) + await OnUsersChanged.Invoke(); + } + + public async Task NotifyUserDeletedAsync(int userId) + { + // Broadcast via SignalR to all clients + await _hubContext.Clients.All.SendAsync("UserDeleted", userId); + + // Also trigger locally if there are server-side subscribers + if (OnUserDeleted != null) + await OnUserDeleted.Invoke(userId); + } +} diff --git a/Dockerfile b/timetracker.Server/Dockerfile similarity index 57% rename from Dockerfile rename to timetracker.Server/Dockerfile index b37c6df..a14dace 100644 --- a/Dockerfile +++ b/timetracker.Server/Dockerfile @@ -2,11 +2,22 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Keine Unterordner mehr beim Kopieren! -COPY timetracker.csproj ./ -RUN dotnet restore timetracker.csproj +# Copy project files for restoring dependencies +COPY timetracker.sln ./ +COPY timetracker.Server/timetracker.Server.csproj timetracker.Server/ +COPY timetracker.Client/timetracker.Client.csproj timetracker.Client/ +COPY timetracker.Shared/timetracker.Shared.csproj timetracker.Shared/ -COPY . ./ +# Restore dependencies +RUN dotnet restore timetracker.sln + +# Copy the rest of the source code +COPY timetracker.Server/ timetracker.Server/ +COPY timetracker.Client/ timetracker.Client/ +COPY timetracker.Shared/ timetracker.Shared/ + +# Publish +WORKDIR /src/timetracker.Server RUN dotnet publish -c Release -o /app/publish # ── Runtime Stage ───────────────────────────────────────────────────────────── @@ -14,7 +25,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app COPY --from=build /app/publish . -# Verzeichnis für die SQLite-Datenbank +# Directory for SQLite database RUN mkdir -p /data ENV ASPNETCORE_HTTP_PORTS=8080 @@ -27,4 +38,4 @@ EXPOSE 8080 VOLUME ["/data"] -ENTRYPOINT ["dotnet", "timetracker.dll"] \ No newline at end of file +ENTRYPOINT ["dotnet", "timetracker.Server.dll"] \ No newline at end of file diff --git a/timetracker.Server/Program.cs b/timetracker.Server/Program.cs new file mode 100644 index 0000000..3a83529 --- /dev/null +++ b/timetracker.Server/Program.cs @@ -0,0 +1,305 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MudBlazor.Services; +using timetracker.Server.Components; +using timetracker.Data; +using timetracker.Shared; + +var builder = WebApplication.CreateBuilder(args); + +// Add Authentication +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/login"; + options.LogoutPath = "/auth/logout"; + options.ExpireTimeSpan = TimeSpan.FromDays(30); + options.SlidingExpiration = true; + // Cookie-Konfiguration für APIs & WASM + options.Events.OnRedirectToLogin = context => + { + if (context.Request.Path.StartsWithSegments("/api")) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; + }); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => + policy.RequireClaim(ClaimTypes.Name, "marc")); +}); + +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddHttpContextAccessor(); + +// Add SignalR +builder.Services.AddSignalR(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => sp.GetRequiredService()); + +// Register DB-backed services as the interfaces +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddMudServices(); +builder.Services.AddHttpClient(); + +var dbProvider = builder.Configuration["DB_PROVIDER"] ?? "SQLite"; + +if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase)) +{ + var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? builder.Configuration["ConnectionStrings:DefaultConnection"]; + builder.Services.AddDbContextFactory(options => + options.UseNpgsql(connectionString)); +} +else +{ + var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH") + ?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db"); + builder.Services.AddDbContextFactory(options => + options.UseSqlite($"Data Source={dbPath}")); +} + +var app = builder.Build(); + +// Migrate or Ensure Database Created +using (var scope = app.Services.CreateScope()) +{ + var factory = scope.ServiceProvider.GetRequiredService>(); + await using var db = await factory.CreateDbContextAsync(); + if (dbProvider.Equals("PostgreSQL", StringComparison.OrdinalIgnoreCase)) + { + await db.Database.EnsureCreatedAsync(); + } + else + { + await db.Database.MigrateAsync(); + } +} + +var forwardedHeadersOptions = new ForwardedHeadersOptions +{ + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto +}; +forwardedHeadersOptions.KnownProxies.Clear(); +forwardedHeadersOptions.KnownIPNetworks.Clear(); +app.UseForwardedHeaders(forwardedHeadersOptions); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} + +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelopment())) +{ + app.UseHttpsRedirection(); +} + +app.UseStaticFiles(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); + +app.MapStaticAssets(); + +// Map Hub +app.MapHub("/hubs/notifications"); + +// Map Blazor WASM +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(timetracker.Client._Imports).Assembly); + +// ── Auth-API-Endpoints ──────────────────────────────────────────────────────── +app.MapGet("/api/auth/me", (HttpContext ctx) => +{ + if (ctx.User.Identity?.IsAuthenticated == true) + { + var idClaim = ctx.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var name = ctx.User.Identity.Name ?? ""; + if (int.TryParse(idClaim, out var id)) + { + return Results.Ok(new UserInfo { Id = id, Username = name }); + } + } + return Results.Unauthorized(); +}); + +app.MapPost("/api/auth/login", async (HttpContext ctx, [FromBody] LoginRequest req, IAuthService authService) => +{ + var user = await authService.LoginAsync(req.Username, req.Password); + if (user == null) + return Results.BadRequest("Benutzername oder Passwort falsch."); + + var claims = new[] { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username) + }; + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + new AuthenticationProperties { IsPersistent = true }); + + return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username }); +}); + +app.MapPost("/api/auth/register", async (HttpContext ctx, [FromBody] LoginRequest req, IAuthService authService) => +{ + var (user, error) = await authService.RegisterAsync(req.Username, req.Password); + if (user == null) + return Results.BadRequest(error ?? "Registrierung fehlgeschlagen."); + + var claims = new[] { + new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), + new Claim(ClaimTypes.Name, user.Username) + }; + var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, + new ClaimsPrincipal(identity), + new AuthenticationProperties { IsPersistent = true }); + + return Results.Ok(new UserInfo { Id = user.Id, Username = user.Username }); +}); + +app.MapGet("/auth/logout", async (HttpContext ctx) => +{ + await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + return Results.Redirect("/login"); +}); + +// ── Admin-API-Endpoints (Protected) ─────────────────────────────────────────── +var usersApi = app.MapGroup("/api/users").RequireAuthorization("AdminOnly"); + +usersApi.MapGet("/", async (IAuthService authService) => +{ + var users = await authService.GetAllUsersAsync(); + return Results.Ok(users); +}); + +usersApi.MapDelete("/{userId:int}", async (int userId, IAuthService authService) => +{ + await authService.DeleteUserAsync(userId); + return Results.NoContent(); +}); + +usersApi.MapPut("/{userId:int}/rename", async (int userId, [FromBody] RenameRequest req, IAuthService authService) => +{ + var error = await authService.RenameUserAsync(userId, req.Username); + if (error != null) + { + return Results.BadRequest(error); + } + return Results.Ok(); +}); + +// ── Timetracker-API-Endpoints (Protected) ───────────────────────────────────── +var trackerApi = app.MapGroup("/api/tracker").RequireAuthorization(); + +trackerApi.MapGet("/week", async ([FromQuery] int userId, [FromQuery] string monday, ITimetrackerService trackerService) => +{ + if (DateOnly.TryParse(monday, out var date)) + { + var days = await trackerService.GetWeekAsync(userId, date); + return Results.Ok(days); + } + return Results.BadRequest("Ungültiges Datum."); +}); + +trackerApi.MapPost("/workday", async ([FromBody] WorkDay workDay, ITimetrackerService trackerService) => +{ + await trackerService.UpsertWorkDayAsync(workDay); + return Results.Ok(); +}); + +trackerApi.MapGet("/settings/{userId:int}", async (int userId, ITimetrackerService trackerService) => +{ + var settings = await trackerService.GetSettingsAsync(userId); + return Results.Ok(settings); +}); + +trackerApi.MapPost("/settings", async ([FromBody] AppSettings settings, ITimetrackerService trackerService) => +{ + await trackerService.SaveSettingsAsync(settings); + return Results.Ok(); +}); + +trackerApi.MapGet("/vacation/{userId:int}/{year:int}", async (int userId, int year, ITimetrackerService trackerService) => +{ + var days = await trackerService.GetVacationDaysAsync(userId, year); + return Results.Ok(days); +}); + +trackerApi.MapPost("/vacation", async ([FromBody] VacationDay vacationDay, ITimetrackerService trackerService) => +{ + await trackerService.AddVacationDayAsync(vacationDay); + return Results.Ok(); +}); + +trackerApi.MapDelete("/vacation/{userId:int}/{id:int}", async (int userId, int id, ITimetrackerService trackerService) => +{ + await trackerService.RemoveVacationDayAsync(userId, id); + return Results.NoContent(); +}); + +trackerApi.MapPost("/overtime/{userId:int}", async (int userId, [FromBody] AppSettings settings, ITimetrackerService trackerService) => +{ + var ts = await trackerService.GetTotalOvertimeAsync(userId, settings); + return Results.Ok(ts.TotalHours); +}); + +trackerApi.MapGet("/month", async ([FromQuery] int userId, [FromQuery] int year, [FromQuery] int month, ITimetrackerService trackerService) => +{ + var days = await trackerService.GetMonthAsync(userId, year, month); + return Results.Ok(days); +}); + +// ── Holiday-API-Endpoints (Protected) ───────────────────────────────────────── +var holidaysApi = app.MapGroup("/api/holidays").RequireAuthorization(); + +holidaysApi.MapGet("/", async ([FromQuery] int year, [FromQuery] string? stateCode, IHolidayService holidayService) => +{ + var list = await holidayService.GetHolidaysAsync(year, stateCode); + return Results.Ok(list); +}); + +holidaysApi.MapPost("/fetch/{year:int}", async (int year, IHolidayService holidayService) => +{ + var (success, message) = await holidayService.FetchAndStoreAsync(year); + return Results.Ok(new { Success = success, Message = message }); +}); + +holidaysApi.MapDelete("/{id:int}", async (int id, IHolidayService holidayService) => +{ + await holidayService.DeleteAsync(id); + return Results.NoContent(); +}); + +app.Run(); + +// ── Models for Request Bodies ────────────────────────────────────────────────── +public record LoginRequest(string Username, string Password); +public record RenameRequest(string Username); diff --git a/Properties/launchSettings.json b/timetracker.Server/Properties/launchSettings.json similarity index 100% rename from Properties/launchSettings.json rename to timetracker.Server/Properties/launchSettings.json diff --git a/appsettings.Development.json b/timetracker.Server/appsettings.Development.json similarity index 100% rename from appsettings.Development.json rename to timetracker.Server/appsettings.Development.json diff --git a/appsettings.json b/timetracker.Server/appsettings.json similarity index 100% rename from appsettings.json rename to timetracker.Server/appsettings.json diff --git a/timetracker.csproj b/timetracker.Server/timetracker.Server.csproj similarity index 59% rename from timetracker.csproj rename to timetracker.Server/timetracker.Server.csproj index f130a2d..930c641 100644 --- a/timetracker.csproj +++ b/timetracker.Server/timetracker.Server.csproj @@ -8,9 +8,16 @@ + + + + + + + diff --git a/wwwroot/app.css b/timetracker.Server/wwwroot/app.css similarity index 100% rename from wwwroot/app.css rename to timetracker.Server/wwwroot/app.css diff --git a/wwwroot/favicon.png b/timetracker.Server/wwwroot/favicon.png similarity index 100% rename from wwwroot/favicon.png rename to timetracker.Server/wwwroot/favicon.png diff --git a/wwwroot/favicon.svg b/timetracker.Server/wwwroot/favicon.svg similarity index 100% rename from wwwroot/favicon.svg rename to timetracker.Server/wwwroot/favicon.svg diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css diff --git a/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map similarity index 100% rename from wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map rename to timetracker.Server/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map diff --git a/Data/AppSettings.cs b/timetracker.Shared/AppSettings.cs similarity index 97% rename from Data/AppSettings.cs rename to timetracker.Shared/AppSettings.cs index 3f13378..a7a8ad0 100644 --- a/Data/AppSettings.cs +++ b/timetracker.Shared/AppSettings.cs @@ -1,4 +1,4 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class AppSettings { diff --git a/Data/BreakEntry.cs b/timetracker.Shared/BreakEntry.cs similarity index 75% rename from Data/BreakEntry.cs rename to timetracker.Shared/BreakEntry.cs index b32eb0d..99d93dd 100644 --- a/Data/BreakEntry.cs +++ b/timetracker.Shared/BreakEntry.cs @@ -1,9 +1,10 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class BreakEntry { public int Id { get; set; } public int WorkDayId { get; set; } + [System.Text.Json.Serialization.JsonIgnore] public WorkDay WorkDay { get; set; } = null!; public TimeOnly? StartTime { get; set; } public TimeOnly? EndTime { get; set; } diff --git a/timetracker.Shared/IAuthService.cs b/timetracker.Shared/IAuthService.cs new file mode 100644 index 0000000..e99f167 --- /dev/null +++ b/timetracker.Shared/IAuthService.cs @@ -0,0 +1,10 @@ +namespace timetracker.Shared; + +public interface IAuthService +{ + Task LoginAsync(string username, string password); + Task> GetAllUsersAsync(); + Task DeleteUserAsync(int userId); + Task RenameUserAsync(int userId, string newUsername); + Task<(User? User, string? Error)> RegisterAsync(string username, string password); +} diff --git a/timetracker.Shared/IHolidayService.cs b/timetracker.Shared/IHolidayService.cs new file mode 100644 index 0000000..031165b --- /dev/null +++ b/timetracker.Shared/IHolidayService.cs @@ -0,0 +1,8 @@ +namespace timetracker.Shared; + +public interface IHolidayService +{ + Task> GetHolidaysAsync(int year, string? stateCode = null); + Task<(bool Success, string Message)> FetchAndStoreAsync(int year); + Task DeleteAsync(int id); +} diff --git a/timetracker.Shared/ITimetrackerService.cs b/timetracker.Shared/ITimetrackerService.cs new file mode 100644 index 0000000..abaf9fe --- /dev/null +++ b/timetracker.Shared/ITimetrackerService.cs @@ -0,0 +1,14 @@ +namespace timetracker.Shared; + +public interface ITimetrackerService +{ + Task> GetWeekAsync(int userId, DateOnly monday); + Task UpsertWorkDayAsync(WorkDay workDay); + Task GetSettingsAsync(int userId); + Task SaveSettingsAsync(AppSettings settings); + Task> GetVacationDaysAsync(int userId, int year); + Task AddVacationDayAsync(VacationDay vacationDay); + Task RemoveVacationDayAsync(int userId, int id); + Task GetTotalOvertimeAsync(int userId, AppSettings settings); + Task> GetMonthAsync(int userId, int year, int month); +} diff --git a/timetracker.Shared/IUserNotificationService.cs b/timetracker.Shared/IUserNotificationService.cs new file mode 100644 index 0000000..f3be7a3 --- /dev/null +++ b/timetracker.Shared/IUserNotificationService.cs @@ -0,0 +1,7 @@ +namespace timetracker.Shared; + +public interface IUserNotificationService +{ + event Func? OnUsersChanged; + event Func? OnUserDeleted; +} diff --git a/Data/PublicHoliday.cs b/timetracker.Shared/PublicHoliday.cs similarity index 86% rename from Data/PublicHoliday.cs rename to timetracker.Shared/PublicHoliday.cs index 97262ac..2fd14c3 100644 --- a/Data/PublicHoliday.cs +++ b/timetracker.Shared/PublicHoliday.cs @@ -1,4 +1,4 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class PublicHoliday { diff --git a/Data/User.cs b/timetracker.Shared/User.cs similarity index 87% rename from Data/User.cs rename to timetracker.Shared/User.cs index 5b64987..a339b3f 100644 --- a/Data/User.cs +++ b/timetracker.Shared/User.cs @@ -1,4 +1,4 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class User { diff --git a/timetracker.Shared/UserInfo.cs b/timetracker.Shared/UserInfo.cs new file mode 100644 index 0000000..67ed616 --- /dev/null +++ b/timetracker.Shared/UserInfo.cs @@ -0,0 +1,7 @@ +namespace timetracker.Shared; + +public class UserInfo +{ + public int Id { get; set; } + public string Username { get; set; } = ""; +} diff --git a/Data/VacationDay.cs b/timetracker.Shared/VacationDay.cs similarity index 85% rename from Data/VacationDay.cs rename to timetracker.Shared/VacationDay.cs index 6dd019c..781d256 100644 --- a/Data/VacationDay.cs +++ b/timetracker.Shared/VacationDay.cs @@ -1,4 +1,4 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class VacationDay { diff --git a/Data/WorkDay.cs b/timetracker.Shared/WorkDay.cs similarity index 90% rename from Data/WorkDay.cs rename to timetracker.Shared/WorkDay.cs index dcaa71c..1330ffc 100644 --- a/Data/WorkDay.cs +++ b/timetracker.Shared/WorkDay.cs @@ -1,4 +1,4 @@ -namespace timetracker.Data; +namespace timetracker.Shared; public class WorkDay { diff --git a/timetracker.Shared/timetracker.Shared.csproj b/timetracker.Shared/timetracker.Shared.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/timetracker.Shared/timetracker.Shared.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/timetracker.db b/timetracker.db deleted file mode 100644 index 5ed71f7cdbd4f60eccba8fad5db32a00f00445fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 zcmeI5Yit|Yb;n8Z{Tw^4C9iF*#`L;tMW#K&H@vNoH| z1;0V~?JUP&#o74)?{?GruvM4s^2a7ao`fyncw}Q<2#3SpaXboq*8h2T!uxS=FnBdI zHLNJviNOB{0w2BN^o*T9Z~x5;ny^zBa|Z`&qNYi5RW+@;W>@pMa-J>cW{P<>V)8`T ziy=P?OQcYVu!T}Ne=WbtmX^zG>1MIW&MueM%BwlBvyqj>ZA-zG)xuJ4b(3ApZ?d_Y z<>f*NYFWyc%9q+1=LD&Kzbq(KQCn!X6g9#^D~jFZov=K^BaZ$ z`rV+b8KEx7yW*fN%68LU#>Ce|*el48){=7a;fFn*vGH;Ht4q3KaU;#a*6h4{ty=Av zpH8T~Qcs}LUh~?r?}$w&f@sV{+Tza3l|IiC5})+FZTrWu$w~VoQSbjsYo{*lE;Q?? z2-W&2U}a%-J{^(%vOf=(lF&oaMi%llwRn!T!;6I#Y9`dEIB_X?8sw zO>4;`S@v2wbYOcrt+f?u9-ikH2Ld;dkFfL0tNFsUl0IZx_`%3LpI^ z9;;5wuBX$9wd9dHF^|4|V^OKu8>V2-{G@};ipxDVHD%w_yQ|)*!_oS-D&A>{FogGg zbXz*z=e#Jxcm5KToMaNV>v+KN8+Y}brb7bL>JuGsJlywta3Fm3xSq*6LRmk(n+8JI z^UXF}vIe7`X}>Y~@OHp6Ha2E&OAmL=s{RtRaM$C=j)2u#; ztq7`mx2aSX1hsZV$eK{s`a)n#c(^g*8B3?_kCycfy1kvBUy`bde!!+KNUGLU?hiCN ztOy3i^pJFO;-QUksM799|MBXOL?%$3UHyvEth9EuTd+~!=&~;@bgkfUk9{z0x9R(~ z_IE>QX-)m}_P~0!`GICU2lnD%^e|`ljPbnvQFH*UVsm%jS{yASkK=>^JxBu&`^@_b zIGjIRfki|eHh7$)YC_}SFqRN@1kUW4uj2|WM1Tko0U|&IhyW2F0z`la5CI}U1c<=9 zi@Y7;#lfLaWM`TjI!QvV}wrF9ocRu zACITviS2?cX_8Pk0{qY;C!^Y!e`EfV`7Er^LIj8a5g-CYfCvx)B0vO)01+SpM1Tmq z69l4;ZTk~n7jRBEW{v2o-XT1u5fB&~L-?qUIEku9_ z5CI}U1c(3;AOb{y2oM1xKm>@uyPUwNJ!tdT?LJ(*-j|*=y7TOLJpXqyuiKdaV!p+E zmHBh#58(qXM1Tko0U|&IhyW2F0z`la5CI}U1c<;dlYr0d7_;k7hjhKi<(RZv@A1DK zb~?`cOi%4R=5SoFn;%(v=GX{W%n!Rg6|_6X?d|8;ssTLz4~D;MWB!BrI`amc{eOyi z!0a&BnHY1L83}(k{MX^H{4zUCbrAs~Km>>Y5g-CYfCvx)B0vO)01>bf7(M2+of|hE zaF>Yk6KCM#72{(v8jIaj>OyNrjV=lIq=xiBR7Ou*;<<7S{=1bRS4N*P+qrDle#&Cc zbnRmnd%9~Mwb)Z#`$>yE-nIXV#UAV0Pgv}H*Z!WKJsyqs?8hzkZoiH@!G6L35`$*| zuI)?59Trm(Od;kqTOfSL7XC5(_5Y8;Ut&JQY%y8pRQRp%Z-bc@B0vO)01+SpM1Tko z0U|&IhyW2F0zVr87XFdvx$$v*I=b;>dM-7c@u6A&0Z<+9nw#~nQ1zOE26PX0$v>UN*W4*!*p<3>;Db`RS1iv$p_+mGpX zH`?_Rd9dUAXh^ra;L-@E+u;%kmKzPW?fi5MuT;Pl5&g2oC|s_9_KaX2{VE0I!7B&& z{C^MgmW_Fvd5ifW^Pg}X;9r@4eu55C2oWFxM1Tko0U|&IhyW2F0z`la5CJ0ae@wvT zu-hG{@YQz`Up*)A)qM>|?T2PU7em3|4}xC{em?lAU@N#8Ob1T~eh~PFz?TBQ z6Sxy729kl}{%`xg>i=W^@A>chH~dNenD1@hH+_HVd)@bnZ{0WTJMI0E_wT)5@&2y2 z;a&5ly=OiD>-i_opLssx`G{x7llPo;|Bw4y?!R!q=DzE`?S8?1!u1o^H(YPHKIeMq z+I7vl-s|!@zvq0z`TNd~Irp41&;eSA01SzYZ}`BZh~`Su?FSUyrgaxx&tf8ss?hLGVA{ z@pF8xUKf>`DAlE^D35;toKuFAgNaY8;$Bl}2%6NCvHs_O4csqw++4gYRM`qNa!zb% z>TXSx*}Nn}F73=#w-PQ3jae7#+P)-1EM#Ff!O81cIOuJ1Nl>IWKgTXejfPm?6KaZf zdIKUZ7!fImm{*!|U6gi#MB~hQFLf%)C0F(&xvFZ7rVOdi-U8=jry!STJJqI)-g7s> z8#lZ#Z!I}nQzTWB%r&1a_Yx$d9N+0-PT7HOJ+lVh^F{)g2bW$({6@{ESHab8GPqK@ zt6lG@8{j+FEtXgmlsyH~0nioj7(lsrl*<%kNGR*P1=uX?ydtzjY)=Oa>HdCY6~Kwg0_PxQEr1(A=sq&x0m_;OCn_reEC(wq4-vSTAR-x! za~5UI^-_a8IIFT|!HLS!3-Z}MWzB#Wm8E;R>>y?3%n7)DWxWJmR2C$FAKt927s1tT zGEPRcDC_;;LuEmkcr=zZDeEeD44_9S>jelzWns0$l=VJ{L1h^+U1d#!8I=V$jt*B= zww>Py8mz1g#GtYuhL7^uL&{2nAC;y1`<0afCn^h^gOrs7H-gaJk5g8nTgd=r#leZn zf&pD3%nr8LCTuA3?Voj zR}bMboiS@h#8IXtSIOeu_v56;b}Jbmy$Nul^wLA6_bf!z8&iHOO8C)5Q zvT)WnD$6LAGARq^eH%cJP!>)EM`b~Zp~}KZ;ixPlrmHNR6OPJ)FY)j}4JU|Wej{kG zvT(9EDhpyjS(!u1!b$b0EZyI)EC!sYEN~7|Rv6p}LU%t-S)pzv1C$j6Cn^iZq#=V^ z03vWRK?M9HEXwltQsdE-Rari8qA;KoIQtL8yx>J;>0UlDNLe0pf%GS^M5<@X&dut zCKmqMJA3*-*@yrUAOb{y2oM1xKm>>Y5g-CYfC&5o2~^BT~g-<&BaqHsgJmMFfWsFjtqYq=W-Q`M{Lt(~P(u3D=J>P&Y2ZlcOf zW#;bRTqr5)g@ZJ=%+1281k?5}h~hq^zpvJW-F;EHn^104q-)FR{FV7qej~n^s%Go5 zQfaIo>}0Cdtyo!HOXMo?Y&>x_QPj3p=Ni}Nr>1UYTXi+PQMk1+n@g*6#SQ6(c)hf> zd$3$kZnakTuL|&#f|H`y5R~0}w{FbOWwovK{EY*ld2Ro}9cW{5^OgIv_Zkn1`+Ik? z>+0-o;laJ@nX7Zv(%tpd>b$u5O14p4zJ7hP_41vXSX + + + +