From 3a118a9b34b15eeeb086b09db1e4a085d01c33ae Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Tue, 4 Mar 2025 13:56:38 +0100 Subject: [PATCH] Added backend functions --- .gitlab-ci.yml | 8 +- WorkTime.sln | 14 +++ src/WorkTime.Api/AuthHandler.cs | 60 ++++++++++++ .../Controllers/AuthController.cs | 13 +++ .../Controllers/EntryController.cs | 35 +++++++ src/WorkTime.Api/DatabaseContext.cs | 10 ++ src/WorkTime.Api/Dockerfile | 23 +++++ src/WorkTime.Api/Logic/EntryLogic.cs | 57 +++++++++++ src/WorkTime.Api/Program.cs | 56 +++++++++++ .../Properties/launchSettings.json | 23 +++++ .../Repositories/EntryRepository.cs | 39 ++++++++ src/WorkTime.Api/Services/AuthService.cs | 29 ++++++ src/WorkTime.Api/WorkTime.Api.csproj | 28 ++++++ src/WorkTime.Api/appsettings.Development.json | 8 ++ src/WorkTime.Api/appsettings.json | 9 ++ src/WorkTime.Host/Program.cs | 4 +- src/WorkTime.Host/WorkTime.Host.csproj | 4 + .../Results/LogicResult.cs | 98 +++++++++++++++++++ src/WorkTime.Shared/Models/TimeEntry.cs | 24 +++++ .../Repositories/IEntryRepository.cs | 17 ++++ src/WorkTime.Shared/Services/IAuthService.cs | 11 +++ src/WorkTime.Shared/WorkTime.Shared.csproj | 13 +++ 22 files changed, 578 insertions(+), 5 deletions(-) create mode 100644 src/WorkTime.Api/AuthHandler.cs create mode 100644 src/WorkTime.Api/Controllers/AuthController.cs create mode 100644 src/WorkTime.Api/Controllers/EntryController.cs create mode 100644 src/WorkTime.Api/DatabaseContext.cs create mode 100644 src/WorkTime.Api/Dockerfile create mode 100644 src/WorkTime.Api/Logic/EntryLogic.cs create mode 100644 src/WorkTime.Api/Program.cs create mode 100644 src/WorkTime.Api/Properties/launchSettings.json create mode 100644 src/WorkTime.Api/Repositories/EntryRepository.cs create mode 100644 src/WorkTime.Api/Services/AuthService.cs create mode 100644 src/WorkTime.Api/WorkTime.Api.csproj create mode 100644 src/WorkTime.Api/appsettings.Development.json create mode 100644 src/WorkTime.Api/appsettings.json create mode 100644 src/WorkTime.ServiceDefaults/Results/LogicResult.cs create mode 100644 src/WorkTime.Shared/Models/TimeEntry.cs create mode 100644 src/WorkTime.Shared/Repositories/IEntryRepository.cs create mode 100644 src/WorkTime.Shared/Services/IAuthService.cs create mode 100644 src/WorkTime.Shared/WorkTime.Shared.csproj diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0c08a91..aeb86ea 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ install-mobile: stage: install image: node:lts-alpine before_script: - - cd src/WorkTime.Mobile + - cd src/WorkTime.WebMobile script: - npm install --prefer-offline cache: @@ -30,7 +30,7 @@ lint-mobile: image: node:lts-alpine needs: ["install-mobile"] before_script: - - cd src/WorkTime.Mobile + - cd src/WorkTime.WebMobile script: - npm run lint cache: @@ -46,7 +46,7 @@ build-mobile: image: node:lts-alpine needs: ["lint-mobile"] before_script: - - cd src/WorkTime.Mobile + - cd src/WorkTime.WebMobile script: - npm run build artifacts: @@ -90,7 +90,7 @@ publish-mobile: - name: docker:dind alias: docker before_script: - - cd src/WorkTime.Mobile + - cd src/WorkTime.WebMobile script: - export VERSION=$(echo $CI_COMMIT_TAG | sed 's/^v//') - docker login -u leon.hoppe -p ${CI_REGISTRY_PASSWORD} registry.leon-hoppe.de diff --git a/WorkTime.sln b/WorkTime.sln index 1fe24b9..48f1361 100644 --- a/WorkTime.sln +++ b/WorkTime.sln @@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Host", "src\WorkTi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.ServiceDefaults", "src\WorkTime.ServiceDefaults\WorkTime.ServiceDefaults.csproj", "{B66AA463-03D5-4814-B1D4-71663804248C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Shared", "src\WorkTime.Shared\WorkTime.Shared.csproj", "{E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkTime.Api", "src\WorkTime.Api\WorkTime.Api.csproj", "{CED653D6-A0B6-432B-9C36-FBB58EEA8229}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,6 +18,8 @@ Global GlobalSection(NestedProjects) = preSolution {6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382} {B66AA463-03D5-4814-B1D4-71663804248C} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382} + {E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382} + {CED653D6-A0B6-432B-9C36-FBB58EEA8229} = {25C5A6B2-A1F9-4244-9538-18E3FE76D382} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F5D4D47-1484-44EA-A5DD-D00AAD2F2F68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -24,5 +30,13 @@ Global {B66AA463-03D5-4814-B1D4-71663804248C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B66AA463-03D5-4814-B1D4-71663804248C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B66AA463-03D5-4814-B1D4-71663804248C}.Release|Any CPU.Build.0 = Release|Any CPU + {E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6A73E21-39B8-4FA7-8D6D-D5DDB8FB8AF0}.Release|Any CPU.Build.0 = Release|Any CPU + {CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CED653D6-A0B6-432B-9C36-FBB58EEA8229}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/WorkTime.Api/AuthHandler.cs b/src/WorkTime.Api/AuthHandler.cs new file mode 100644 index 0000000..ab9dd08 --- /dev/null +++ b/src/WorkTime.Api/AuthHandler.cs @@ -0,0 +1,60 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using WorkTime.Shared.Services; + +namespace WorkTime.Api; + +public class AuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + IAuthService authService) + : AuthenticationHandler(options, logger, encoder) { + + protected override async Task HandleAuthenticateAsync() { + if (! await authService.IsAuthenticated()) { + return AuthenticateResult.Fail("Invalid or missing Guid."); + } + + var guid = await authService.GetCurrentUserId(); + + var claims = new[] { new Claim(ClaimTypes.NameIdentifier, guid.ToString()) }; + var identity = new ClaimsIdentity(claims, Scheme.Name); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, Scheme.Name); + + return AuthenticateResult.Success(ticket); + } + + public static Task ConfigureOpenApi(OpenApiDocument document, OpenApiDocumentTransformerContext transformerContext, CancellationToken token) { + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes ??= new Dictionary(); + document.SecurityRequirements ??= new List(); + + document.Components.SecuritySchemes.Add(nameof(AuthHandler), new OpenApiSecurityScheme { + Type = SecuritySchemeType.Http, + In = ParameterLocation.Header, + Name = IAuthService.HeaderName, + Scheme = "Bearer", + Description = "GUID Authorization header using a custom scheme.\nExample: \"Authorization: {GUID}\"" + }); + + //TODO: only add security requirement to authorized endpoints + document.SecurityRequirements.Add(new() { + { + new() { + Reference = new OpenApiReference { + Type = ReferenceType.SecurityScheme, + Id = nameof(AuthHandler) + } + }, [] + } + }); + + return Task.CompletedTask; + } +} diff --git a/src/WorkTime.Api/Controllers/AuthController.cs b/src/WorkTime.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..e332f74 --- /dev/null +++ b/src/WorkTime.Api/Controllers/AuthController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; + +namespace WorkTime.Api.Controllers; + +[ApiController, Route("auth")] +public class AuthController : ControllerBase { + + [HttpGet("register")] + public ActionResult Register() { + return Ok(Guid.NewGuid().ToString()); + } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/Controllers/EntryController.cs b/src/WorkTime.Api/Controllers/EntryController.cs new file mode 100644 index 0000000..955b61d --- /dev/null +++ b/src/WorkTime.Api/Controllers/EntryController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using WorkTime.Api.Logic; +using WorkTime.Shared.Models; + +namespace WorkTime.Api.Controllers; + +[ApiController, Route("entries"), Authorize] +public class EntryController(EntryLogic logic) : ControllerBase { + + [HttpGet("{id:guid}")] + public async Task GetAllEntries(Guid id) { + var result = await logic.GetAllEntries(id); + return result.MapResult(); + } + + [HttpGet("{id:guid}/{day:datetime}")] + public async Task GetEntries(Guid id, DateTime day) { + var result = await logic.GetEntries(id, DateOnly.FromDateTime(day)); + return result.MapResult(); + } + + [HttpPost] + public async Task AddEntry(TimeEntry entry) { + var result = await logic.AddEntry(entry); + return result.MapResult(); + } + + [HttpDelete("{id:int}")] + public async Task DeleteEntry(int id) { + var result = await logic.DeleteEntry(id); + return result.MapResult(); + } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/DatabaseContext.cs b/src/WorkTime.Api/DatabaseContext.cs new file mode 100644 index 0000000..61f5c39 --- /dev/null +++ b/src/WorkTime.Api/DatabaseContext.cs @@ -0,0 +1,10 @@ +using Microsoft.EntityFrameworkCore; +using WorkTime.Shared.Models; + +namespace WorkTime.Api; + +public class DatabaseContext(DbContextOptions options) : DbContext(options) { + + public DbSet Entries { get; set; } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/Dockerfile b/src/WorkTime.Api/Dockerfile new file mode 100644 index 0000000..5e8e0d2 --- /dev/null +++ b/src/WorkTime.Api/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["src/WorkTime.Api/WorkTime.Api.csproj", "src/WorkTime.Api/"] +RUN dotnet restore "src/WorkTime.Api/WorkTime.Api.csproj" +COPY . . +WORKDIR "/src/src/WorkTime.Api" +RUN dotnet build "WorkTime.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "WorkTime.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "WorkTime.Api.dll"] diff --git a/src/WorkTime.Api/Logic/EntryLogic.cs b/src/WorkTime.Api/Logic/EntryLogic.cs new file mode 100644 index 0000000..cab4f20 --- /dev/null +++ b/src/WorkTime.Api/Logic/EntryLogic.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http.HttpResults; +using WorkTime.ServiceDefaults.Results; +using WorkTime.Shared.Models; +using WorkTime.Shared.Repositories; +using WorkTime.Shared.Services; + +namespace WorkTime.Api.Logic; + +public class EntryLogic(IEntryRepository entryRepository, IAuthService authService) { + + public async Task> GetAllEntries(Guid owner) { + if (!await authService.IsAuthenticated()) + return new(Results.Unauthorized()); + + if (await authService.GetCurrentUserId() != owner) + return new(Results.Unauthorized()); + + return await entryRepository.GetAllEntries(owner); + } + + public async Task> GetEntries(Guid owner, DateOnly date) { + if (!await authService.IsAuthenticated()) + return new(Results.Unauthorized()); + + if (await authService.GetCurrentUserId() != owner) + return new(Results.Unauthorized()); + + return await entryRepository.GetEntries(owner, date); + } + + public async Task AddEntry(TimeEntry entry) { + if (!await authService.IsAuthenticated()) + return new(Results.Unauthorized()); + + if (await authService.GetCurrentUserId() != entry.Owner) + return new(Results.Unauthorized()); + + await entryRepository.AddEntry(entry); + return new(); + } + + public async Task DeleteEntry(int id) { + if (!await authService.IsAuthenticated()) + return new(Results.Unauthorized()); + + var entry = await entryRepository.GetEntry(id); + if (entry is null) + return new(Results.NotFound()); + + if (await authService.GetCurrentUserId() != entry.Owner) + return new(Results.Unauthorized()); + + await entryRepository.DeleteEntry(entry); + return new(); + } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/Program.cs b/src/WorkTime.Api/Program.cs new file mode 100644 index 0000000..e436a8a --- /dev/null +++ b/src/WorkTime.Api/Program.cs @@ -0,0 +1,56 @@ +using MartinCostello.OpenApi; +using Microsoft.AspNetCore.Authentication; +using WorkTime.Api; +using WorkTime.Api.Logic; +using WorkTime.Api.Repositories; +using WorkTime.Api.Services; +using WorkTime.Shared.Repositories; +using WorkTime.Shared.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(options => { + options.AddDocumentTransformer(AuthHandler.ConfigureOpenApi); +}); +builder.Services.AddOpenApiExtensions(options => { + options.AddServerUrls = true; +}); + +builder.AddServiceDefaults(); +builder.Services.AddControllers(); +builder.Services.AddProblemDetails(); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddNpgsql(builder.Configuration.GetConnectionString("data")); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); + +builder.Services.AddAuthentication(nameof(AuthHandler)) + .AddScheme(nameof(AuthHandler), _ => { }); +builder.Services.AddAuthorization(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { + app.MapOpenApi(); + app.UseSwaggerUI(options => { + options.SwaggerEndpoint("/openapi/v1.json", "WorkTime.Api"); + }); + + await using var scope = app.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); +} + +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/src/WorkTime.Api/Properties/launchSettings.json b/src/WorkTime.Api/Properties/launchSettings.json new file mode 100644 index 0000000..89dd186 --- /dev/null +++ b/src/WorkTime.Api/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5212", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7091;http://localhost:5212", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/WorkTime.Api/Repositories/EntryRepository.cs b/src/WorkTime.Api/Repositories/EntryRepository.cs new file mode 100644 index 0000000..735df05 --- /dev/null +++ b/src/WorkTime.Api/Repositories/EntryRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using WorkTime.Shared.Models; +using WorkTime.Shared.Repositories; + +namespace WorkTime.Api.Repositories; + +internal class EntryRepository(DatabaseContext context) : IEntryRepository { + + public async Task GetAllEntries(Guid owner) { + return await context.Entries + .Where(entry => entry.Owner == owner) + .OrderBy(entry => entry.Timestamp) + .ToArrayAsync(); + } + + public async Task GetEntries(Guid owner, DateOnly date) { + return await context.Entries + .Where(entry => entry.Owner == owner) + .Where(entry => DateOnly.FromDateTime(entry.Timestamp) == date) + .OrderBy(entry => entry.Timestamp) + .ToArrayAsync(); + } + + public async Task GetEntry(int id) { + return await context.Entries + .FindAsync(id); + } + + public async Task AddEntry(TimeEntry entry) { + await context.Entries.AddAsync(entry); + await context.SaveChangesAsync(); + } + + public async Task DeleteEntry(TimeEntry entry) { + context.Entries.Remove(entry); + await context.SaveChangesAsync(); + } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/Services/AuthService.cs b/src/WorkTime.Api/Services/AuthService.cs new file mode 100644 index 0000000..c0a0b46 --- /dev/null +++ b/src/WorkTime.Api/Services/AuthService.cs @@ -0,0 +1,29 @@ +using WorkTime.Shared.Services; + +namespace WorkTime.Api.Services; + +internal class AuthService(IHttpContextAccessor accessor) : IAuthService { + + public Task IsAuthenticated() { + var header = accessor.HttpContext?.Request.Headers[IAuthService.HeaderName]; + if (header is not { Count: 1 }) + return Task.FromResult(false); + + var value = header.Value[0]!.Replace("Bearer ", ""); + if (Guid.TryParse(value, out var guid)) + return Task.FromResult(false); + + return Task.FromResult(guid != Guid.Empty); + } + + public async Task GetCurrentUserId() { + if (!await IsAuthenticated()) + return Guid.Empty; + + var header = accessor.HttpContext?.Request.Headers[IAuthService.HeaderName]!; + var value = header.Value[0]!.Replace("Bearer ", ""); + + return Guid.Parse(value); + } + +} \ No newline at end of file diff --git a/src/WorkTime.Api/WorkTime.Api.csproj b/src/WorkTime.Api/WorkTime.Api.csproj new file mode 100644 index 0000000..64214d6 --- /dev/null +++ b/src/WorkTime.Api/WorkTime.Api.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + Linux + + + + + + + + + + + + .dockerignore + + + + + + + + + diff --git a/src/WorkTime.Api/appsettings.Development.json b/src/WorkTime.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/WorkTime.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/WorkTime.Api/appsettings.json b/src/WorkTime.Api/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/WorkTime.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/WorkTime.Host/Program.cs b/src/WorkTime.Host/Program.cs index 654783c..4978648 100644 --- a/src/WorkTime.Host/Program.cs +++ b/src/WorkTime.Host/Program.cs @@ -3,6 +3,8 @@ var builder = DistributedApplication.CreateBuilder(args); var db = builder.AddPostgres("db") .WithDataVolume(); - +builder.AddProject("api") + .WithReference(db.AddDatabase("data")) + .WaitFor(db); builder.Build().Run(); \ No newline at end of file diff --git a/src/WorkTime.Host/WorkTime.Host.csproj b/src/WorkTime.Host/WorkTime.Host.csproj index db10b2a..2cc959f 100644 --- a/src/WorkTime.Host/WorkTime.Host.csproj +++ b/src/WorkTime.Host/WorkTime.Host.csproj @@ -16,4 +16,8 @@ + + + + diff --git a/src/WorkTime.ServiceDefaults/Results/LogicResult.cs b/src/WorkTime.ServiceDefaults/Results/LogicResult.cs new file mode 100644 index 0000000..0cfdf25 --- /dev/null +++ b/src/WorkTime.ServiceDefaults/Results/LogicResult.cs @@ -0,0 +1,98 @@ +using Microsoft.AspNetCore.Http; + +namespace WorkTime.ServiceDefaults.Results; + +public interface ILogicResult { + + public TResult? Result { get; init; } + + public TError? Error { get; init; } + + public bool IsSuccessful => Result is not null && Error is null; + + IResult MapResult(); + +} + +public interface ILogicResult : ILogicResult; + +public interface ILogicResult : ILogicResult; + +public readonly struct LogicResult : ILogicResult { + + public TResult? Result { get; init; } + public TError? Error { get; init; } + + public bool IsSuccessful => Result is not null && Error is null; + + public static implicit operator LogicResult(TResult result) { + return new LogicResult { + Result = result + }; + } + + public static implicit operator LogicResult(TError error) { + return new LogicResult { + Error = error + }; + } + + public IResult MapResult() { + if (!IsSuccessful) + return Microsoft.AspNetCore.Http.Results.Problem(); + + return Microsoft.AspNetCore.Http.Results.Ok(Result); + } + +} + +public readonly struct LogicResult : ILogicResult { + + public TResult? Result { get; init; } + public IResult? Error { get; init; } + + public bool IsSuccessful => Error is null; + + public static implicit operator LogicResult(TResult result) { + return new LogicResult { + Result = result + }; + } + + public LogicResult() { } + + public LogicResult(IResult error) { + Error = error; + } + + public IResult MapResult() { + if (!IsSuccessful) { + return Error!; + } + + return Microsoft.AspNetCore.Http.Results.Ok(Result); + } + +} + +public readonly struct LogicResult : ILogicResult { + + public object? Result { get; init; } + public IResult? Error { get; init; } + + public bool IsSuccessful => Error is null; + + public LogicResult() { } + + public LogicResult(IResult error) { + Error = error; + } + + public IResult MapResult() { + if (!IsSuccessful) { + return Error!; + } + + return Microsoft.AspNetCore.Http.Results.Ok(Result); + } +} diff --git a/src/WorkTime.Shared/Models/TimeEntry.cs b/src/WorkTime.Shared/Models/TimeEntry.cs new file mode 100644 index 0000000..1f613f0 --- /dev/null +++ b/src/WorkTime.Shared/Models/TimeEntry.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace WorkTime.Shared.Models; + +public class TimeEntry { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public required Guid Owner { get; set; } + + public required TimeEntryType Type { get; set; } + + public DateTime Timestamp { get; set; } = DateTime.Now; + + public bool IsMoba { get; set; } +} + +public enum TimeEntryType { + Login, + Logout, + LoginDrive, + LogoutDrive +} diff --git a/src/WorkTime.Shared/Repositories/IEntryRepository.cs b/src/WorkTime.Shared/Repositories/IEntryRepository.cs new file mode 100644 index 0000000..c977175 --- /dev/null +++ b/src/WorkTime.Shared/Repositories/IEntryRepository.cs @@ -0,0 +1,17 @@ +using WorkTime.Shared.Models; + +namespace WorkTime.Shared.Repositories; + +public interface IEntryRepository { + + Task GetAllEntries(Guid owner); + + Task GetEntries(Guid owner, DateOnly date); + + Task GetEntry(int id); + + Task AddEntry(TimeEntry entry); + + Task DeleteEntry(TimeEntry entry); + +} \ No newline at end of file diff --git a/src/WorkTime.Shared/Services/IAuthService.cs b/src/WorkTime.Shared/Services/IAuthService.cs new file mode 100644 index 0000000..67723fa --- /dev/null +++ b/src/WorkTime.Shared/Services/IAuthService.cs @@ -0,0 +1,11 @@ +namespace WorkTime.Shared.Services; + +public interface IAuthService { + + public const string HeaderName = "Authentication"; + + public Task IsAuthenticated(); + + public Task GetCurrentUserId(); + +} \ No newline at end of file diff --git a/src/WorkTime.Shared/WorkTime.Shared.csproj b/src/WorkTime.Shared/WorkTime.Shared.csproj new file mode 100644 index 0000000..e65e200 --- /dev/null +++ b/src/WorkTime.Shared/WorkTime.Shared.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + +