Compare commits

...
2 Commits
Author SHA1 Message Date
Natlinux 3f0d23dab2 Merge pull request 'implement Clean Architecture and user authentication' (#1) from feature/users into develop
Reviewed-on: #1
2026-08-25 22:50:37 +00:00
Natlinux b12e557e1a implement Clean Architecture and user authentication
- Add LoopFit.Domain with entities (ApplicationUser, Workout, CalorieEntry, VitalData), enums (WorkoutType), and interfaces (IJwtTokenGenerator, IUserRepository, IWorkoutRepository)
- Add LoopFit.Application with auth DTOs, service interfaces, and DI setup
- Add LoopFit.Infrastructure with EF Core DbContext (PostgreSQL), ASP.NET Core Identity, JWT token generation, and repository implementations
- Add AuthController with register/login/me endpoints and JWT Bearer auth
- Add MAUI auth services (AuthService, TokenStorageService) and Login/Register Blazor pages
2026-08-26 00:50:02 +02:00
69 changed files with 1253 additions and 146 deletions
-17
View File
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0"/>
</ItemGroup>
</Project>
-14
View File
@@ -1,14 +0,0 @@
@page "/"
@using LoopFit.Shared.Services
@inject IFormFactor FormFactor
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app running on <em>@factor</em> using <em>@platform</em>.
@code {
private string factor => FormFactor.GetFormFactor();
private string platform => FormFactor.GetPlatform();
}
-5
View File
@@ -1,5 +0,0 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
-7
View File
@@ -1,7 +0,0 @@
namespace LoopFit.Shared.Services;
public interface IFormFactor
{
public string GetFormFactor();
public string GetPlatform();
}
-11
View File
@@ -1,11 +0,0 @@
@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 static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using LoopFit
@using LoopFit.Shared
@using LoopFit.Shared.Layout
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/> <base href="/"/>
<ResourcePreloader/> <ResourcePreloader/>
<link rel="stylesheet" href="_content/LoopFit.Shared/lib/bootstrap/dist/css/bootstrap.min.css"/> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="_content/LoopFit.Shared/app.css"/> <link rel="stylesheet" href="app.css"/>
<link rel="stylesheet" href="LoopFit.Web.styles.css"/> <link rel="stylesheet" href="LoopFit.Web.styles.css"/>
<ImportMap/> <ImportMap/>
<link rel="icon" type="image/png" href="_content/LoopFit.Shared/favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet @rendermode="InteractiveServer"/> <HeadOutlet @rendermode="InteractiveServer"/>
</head> </head>
@@ -1,4 +1,4 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<div class="page"> <div class="page">
<div class="sidebar"> <div class="sidebar">
@@ -1,4 +1,4 @@
<div class="top-row ps-3 navbar navbar-dark"> <div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="">LoopFit</a> <a class="navbar-brand" href="">LoopFit</a>
</div> </div>
@@ -13,7 +13,5 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink> </NavLink>
</div> </div>
</nav> </nav>
</div> </div>
@@ -38,14 +38,6 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
} }
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item { .nav-item {
font-size: 0.9rem; font-size: 0.9rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@@ -95,10 +87,7 @@
} }
.nav-scrollable { .nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block; display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem); height: calc(100vh - 3.5rem);
overflow-y: auto; overflow-y: auto;
} }
+7
View File
@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>LoopFit</h1>
<p>Willkommen bei LoopFit - dein Fitness-Tracker.</p>
@@ -0,0 +1,5 @@
@page "/not-found"
@layout Layout.MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
@@ -1,4 +1,4 @@
<Router AppAssembly="typeof(Layout.MainLayout).Assembly" NotFoundPage="typeof(Pages.NotFound)"> <Router AppAssembly="typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
<FocusOnNavigate RouteData="routeData" Selector="h1"/> <FocusOnNavigate RouteData="routeData" Selector="h1"/>
-2
View File
@@ -6,7 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using LoopFit.Shared
@using LoopFit.Shared.Layout
@using LoopFit.Web.Components @using LoopFit.Web.Components
@using LoopFit.Web.Components.Layout @using LoopFit.Web.Components.Layout
+58
View File
@@ -0,0 +1,58 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using LoopFit.Application.DTOs.Auth;
using LoopFit.Application.Interfaces;
namespace LoopFit.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("register")]
public async Task<ActionResult<AuthResponse>> Register([FromBody] RegisterRequest request)
{
var result = await _authService.RegisterAsync(request);
if (result is null)
return BadRequest(new { message = "Registrierung fehlgeschlagen. Email existiert bereits oder Passwort ist nicht gültig." });
return Ok(result);
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login([FromBody] LoginRequest request)
{
var result = await _authService.LoginAsync(request);
if (result is null)
return Unauthorized(new { message = "Email oder Passwort falsch." });
return Ok(result);
}
[Authorize]
[HttpGet("me")]
public async Task<ActionResult<AuthResponse>> GetCurrentUser()
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null)
return Unauthorized();
var result = await _authService.GetCurrentUserAsync(Guid.Parse(userId));
if (result is null)
return NotFound();
return Ok(result);
}
}
+2 -1
View File
@@ -7,7 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LoopFit.Shared\LoopFit.Shared.csproj"/> <ProjectReference Include="..\src\LoopFit.Application\LoopFit.Application.csproj"/>
<ProjectReference Include="..\src\LoopFit.Infrastructure\LoopFit.Infrastructure.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+34 -10
View File
@@ -1,36 +1,60 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using LoopFit.Application;
using LoopFit.Infrastructure;
using LoopFit.Web.Components; using LoopFit.Web.Components;
using LoopFit.Shared.Services;
using LoopFit.Web.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var jwtSettings = builder.Configuration.GetSection("Jwt");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtSettings["Key"]!))
};
});
builder.Services.AddAuthorization();
builder.Services.AddRazorComponents() builder.Services.AddRazorComponents()
.AddInteractiveServerComponents(); .AddInteractiveServerComponents();
// Add device-specific services used by the LoopFit.Shared project builder.Services.AddControllers();
builder.Services.AddSingleton<IFormFactor, FormFactor>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Error", createScopeForErrors: true); 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.UseHsts();
} }
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery(); app.UseAntiforgery();
app.MapStaticAssets(); app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode() .AddInteractiveServerRenderMode()
.AddAdditionalAssemblies( .AddAdditionalAssemblies(typeof(App).Assembly);
typeof(LoopFit.Shared._Imports).Assembly);
app.Run(); app.Run();
+10 -1
View File
@@ -5,5 +5,14 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=loopfit;Username=postgres;Password=postgres"
},
"Jwt": {
"Key": "LoopFitSuperSecretKeyThatIsAtLeast32CharactersLong!",
"Issuer": "LoopFit",
"Audience": "LoopFitApp",
"ExpirationMinutes": 60
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

+1
View File
@@ -0,0 +1 @@
404: Not Found
+72 -6
View File
@@ -2,27 +2,93 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit", "LoopFit\LoopFit.csproj", "{230BF2FE-83B3-443B-B561-E33ED9307B71}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit", "LoopFit\LoopFit.csproj", "{230BF2FE-83B3-443B-B561-E33ED9307B71}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Shared", "LoopFit.Shared\LoopFit.Shared.csproj", "{784878BE-3611-4C20-B91F-9724F150F78E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Web", "LoopFit.Web\LoopFit.Web.csproj", "{0011D403-B7FB-4ED1-9885-B6A594872B91}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Web", "LoopFit.Web\LoopFit.Web.csproj", "{0011D403-B7FB-4ED1-9885-B6A594872B91}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Domain", "src\LoopFit.Domain\LoopFit.Domain.csproj", "{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Application", "src\LoopFit.Application\LoopFit.Application.csproj", "{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoopFit.Infrastructure", "src\LoopFit.Infrastructure\LoopFit.Infrastructure.csproj", "{C91B39DF-E06E-4EAF-AD2B-32641A15E558}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|Any CPU.Build.0 = Debug|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|x64.ActiveCfg = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|x64.Build.0 = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|x86.ActiveCfg = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Debug|x86.Build.0 = Debug|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|Any CPU.ActiveCfg = Release|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|Any CPU.Build.0 = Release|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|Any CPU.Build.0 = Release|Any CPU
{784878BE-3611-4C20-B91F-9724F150F78E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|x64.ActiveCfg = Release|Any CPU
{784878BE-3611-4C20-B91F-9724F150F78E}.Debug|Any CPU.Build.0 = Debug|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|x64.Build.0 = Release|Any CPU
{784878BE-3611-4C20-B91F-9724F150F78E}.Release|Any CPU.ActiveCfg = Release|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|x86.ActiveCfg = Release|Any CPU
{784878BE-3611-4C20-B91F-9724F150F78E}.Release|Any CPU.Build.0 = Release|Any CPU {230BF2FE-83B3-443B-B561-E33ED9307B71}.Release|x86.Build.0 = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|Any CPU.Build.0 = Debug|Any CPU {0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|x64.ActiveCfg = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|x64.Build.0 = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|x86.ActiveCfg = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Debug|x86.Build.0 = Debug|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|Any CPU.ActiveCfg = Release|Any CPU {0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|Any CPU.Build.0 = Release|Any CPU {0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|Any CPU.Build.0 = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|x64.ActiveCfg = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|x64.Build.0 = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|x86.ActiveCfg = Release|Any CPU
{0011D403-B7FB-4ED1-9885-B6A594872B91}.Release|x86.Build.0 = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|x64.ActiveCfg = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|x64.Build.0 = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|x86.ActiveCfg = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Debug|x86.Build.0 = Debug|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|Any CPU.Build.0 = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|x64.ActiveCfg = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|x64.Build.0 = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|x86.ActiveCfg = Release|Any CPU
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3}.Release|x86.Build.0 = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|x64.ActiveCfg = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|x64.Build.0 = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|x86.ActiveCfg = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Debug|x86.Build.0 = Debug|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|Any CPU.Build.0 = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|x64.ActiveCfg = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|x64.Build.0 = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|x86.ActiveCfg = Release|Any CPU
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8}.Release|x86.Build.0 = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|x64.ActiveCfg = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|x64.Build.0 = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|x86.ActiveCfg = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Debug|x86.Build.0 = Debug|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|Any CPU.Build.0 = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|x64.ActiveCfg = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|x64.Build.0 = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|x86.ActiveCfg = Release|Any CPU
{C91B39DF-E06E-4EAF-AD2B-32641A15E558}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A7CC92FF-09E8-4331-94B2-24FC1B0D17A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{CF4C1F14-B82D-4023-BED6-F750F97CE9C8} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C91B39DF-E06E-4EAF-AD2B-32641A15E558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+1 -1
View File
@@ -1,6 +1,6 @@
namespace LoopFit; namespace LoopFit;
public partial class App : Application public partial class App : Microsoft.Maui.Controls.Application
{ {
public App() public App()
{ {
@@ -0,0 +1,34 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main>
<div class="top-row px-4">
<a href="" @onclick="Logout" @onclick:preventDefault>Abmelden</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
[Inject] private Services.AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager Navigation { get; set; } = default!;
private async Task Logout()
{
await AuthService.LogoutAsync();
Navigation.NavigateTo("/login", forceLoad: true);
}
}
@@ -0,0 +1,87 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
+17
View File
@@ -0,0 +1,17 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">LoopFit</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
</nav>
</div>
@@ -0,0 +1,94 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
display: block;
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
+8
View File
@@ -0,0 +1,8 @@
@page "/"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
<PageTitle>Home</PageTitle>
<h1>LoopFit</h1>
<p>Willkommen bei LoopFit - dein Fitness-Tracker.</p>
+78
View File
@@ -0,0 +1,78 @@
@page "/login"
<PageTitle>Login</PageTitle>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Anmelden</h2>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger">@_errorMessage</div>
}
<EditForm Model="_loginRequest" OnValidSubmit="HandleLogin" FormName="loginForm">
<div class="mb-3">
<label class="form-label">Email</label>
<InputText class="form-control" @bind-Value="_loginRequest.Email" type="email" placeholder="name@beispiel.de"/>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<InputText class="form-control" @bind-Value="_loginRequest.Password" type="password" placeholder="Passwort"/>
</div>
<button type="submit" class="btn btn-primary w-100" disabled="@_isLoading">
@if (_isLoading)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
Anmelden
</button>
</EditForm>
<div class="text-center mt-3">
<a href="/register">Noch kein Konto? Registrieren</a>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
[Inject] private Services.AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager Navigation { get; set; } = default!;
private LoopFit.Application.DTOs.Auth.LoginRequest _loginRequest = new();
private bool _isLoading;
private string? _errorMessage;
protected override async Task OnInitializedAsync()
{
if (await AuthService.IsAuthenticatedAsync())
{
Navigation.NavigateTo("/", forceLoad: true);
}
}
private async Task HandleLogin()
{
_isLoading = true;
_errorMessage = null;
var result = await AuthService.LoginAsync(_loginRequest);
if (result is not null)
{
Navigation.NavigateTo("/", forceLoad: true);
}
else
{
_errorMessage = "Email oder Passwort falsch.";
}
_isLoading = false;
}
}
+5
View File
@@ -0,0 +1,5 @@
@page "/not-found"
@layout Layout.MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
+86
View File
@@ -0,0 +1,86 @@
@page "/register"
<PageTitle>Registrieren</PageTitle>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Registrieren</h2>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger">@_errorMessage</div>
}
<EditForm Model="_registerRequest" OnValidSubmit="HandleRegister" FormName="registerForm">
<div class="mb-3">
<label class="form-label">Vorname</label>
<InputText class="form-control" @bind-Value="_registerRequest.FirstName" placeholder="Vorname"/>
</div>
<div class="mb-3">
<label class="form-label">Nachname</label>
<InputText class="form-control" @bind-Value="_registerRequest.LastName" placeholder="Nachname"/>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<InputText class="form-control" @bind-Value="_registerRequest.Email" type="email" placeholder="name@beispiel.de"/>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<InputText class="form-control" @bind-Value="_registerRequest.Password" type="password" placeholder="Mind. 8 Zeichen"/>
</div>
<button type="submit" class="btn btn-primary w-100" disabled="@_isLoading">
@if (_isLoading)
{
<span class="spinner-border spinner-border-sm me-2"></span>
}
Registrieren
</button>
</EditForm>
<div class="text-center mt-3">
<a href="/login">Bereits ein Konto? Anmelden</a>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
[Inject] private Services.AuthService AuthService { get; set; } = default!;
[Inject] private NavigationManager Navigation { get; set; } = default!;
private LoopFit.Application.DTOs.Auth.RegisterRequest _registerRequest = new();
private bool _isLoading;
private string? _errorMessage;
protected override async Task OnInitializedAsync()
{
if (await AuthService.IsAuthenticatedAsync())
{
Navigation.NavigateTo("/", forceLoad: true);
}
}
private async Task HandleRegister()
{
_isLoading = true;
_errorMessage = null;
var result = await AuthService.RegisterAsync(_registerRequest);
if (result is not null)
{
Navigation.NavigateTo("/", forceLoad: true);
}
else
{
_errorMessage = "Registrierung fehlgeschlagen. Bitte Eingaben prüfen.";
}
_isLoading = false;
}
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
</Found>
</Router>
+4 -1
View File
@@ -3,8 +3,11 @@
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using LoopFit @using LoopFit
@using LoopFit.Components @using LoopFit.Components
@using LoopFit.Shared @using LoopFit.Components.Layout
@using LoopFit.Application.DTOs.Auth
@using LoopFit.Application.Interfaces
+5 -28
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net10.0-android</TargetFrameworks> <TargetFramework>net10.0-android</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RootNamespace>LoopFit</RootNamespace> <RootNamespace>LoopFit</RootNamespace>
@@ -10,57 +10,34 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCssItems>false</EnableDefaultCssItems> <EnableDefaultCssItems>false</EnableDefaultCssItems>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Enable XAML source generation for faster build times and improved performance.
This generates C# code from XAML at compile time instead of runtime inflation.
To disable, remove this line.
For individual files, you can override by setting Inflator metadata:
<MauiXaml Update="MyPage.xaml" Inflator="Default" /> (reverts to defaults: Runtime for Debug, XamlC for Release)
<MauiXaml Update="MyPage.xaml" Inflator="Runtime" /> (force runtime inflation) -->
<MauiXamlInflator>SourceGen</MauiXamlInflator> <MauiXamlInflator>SourceGen</MauiXamlInflator>
<!-- Display name -->
<ApplicationTitle>LoopFit</ApplicationTitle> <ApplicationTitle>LoopFit</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.loopfit</ApplicationId> <ApplicationId>com.companyname.loopfit</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/> <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4"/>
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/> <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128"/>
<!-- Images -->
<MauiImage Include="Resources\Images\*"/> <MauiImage Include="Resources\Images\*"/>
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208"/> <MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208"/>
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*"/> <MauiFont Include="Resources\Fonts\*"/>
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/> <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)"/> <PackageReference Include="Microsoft.Maui.Controls" Version="10.0.20"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.20"/>
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0"/> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LoopFit.Shared\LoopFit.Shared.csproj"/> <ProjectReference Include="..\src\LoopFit.Application\LoopFit.Application.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+2 -2
View File
@@ -2,13 +2,13 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LoopFit" xmlns:local="clr-namespace:LoopFit"
xmlns:shared="clr-namespace:LoopFit.Shared;assembly=LoopFit.Shared" xmlns:components="clr-namespace:LoopFit.Components"
x:Class="LoopFit.MainPage" x:Class="LoopFit.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}"> BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html"> <BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView.RootComponents> <BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type shared:Routes}" /> <RootComponent Selector="#app" ComponentType="{x:Type components:Routes}" />
</BlazorWebView.RootComponents> </BlazorWebView.RootComponents>
</BlazorWebView> </BlazorWebView>
+9 -2
View File
@@ -1,5 +1,5 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LoopFit.Shared.Services; using LoopFit.Application.Interfaces;
using LoopFit.Services; using LoopFit.Services;
namespace LoopFit; namespace LoopFit;
@@ -13,10 +13,17 @@ public static class MauiProgram
.UseMauiApp<App>() .UseMauiApp<App>()
.ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); });
// Add device-specific services used by the LoopFit.Shared project builder.Services.AddSingleton<TokenStorageService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddSingleton<IFormFactor, FormFactor>(); builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddHttpClient<AuthService>(client =>
{
client.BaseAddress = new Uri("http://10.0.2.2:5000");
});
builder.Services.AddMauiBlazorWebView(); builder.Services.AddMauiBlazorWebView();
builder.Services.AddAuthorizationCore();
#if DEBUG #if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); builder.Services.AddBlazorWebViewDeveloperTools();
+76
View File
@@ -0,0 +1,76 @@
using System.Net.Http.Json;
using LoopFit.Application.DTOs.Auth;
using LoopFit.Application.Interfaces;
namespace LoopFit.Services;
public class AuthService : IAuthService
{
private readonly HttpClient _httpClient;
private readonly TokenStorageService _tokenStorage;
public AuthService(HttpClient httpClient, TokenStorageService tokenStorage)
{
_httpClient = httpClient;
_tokenStorage = tokenStorage;
}
public async Task<AuthResponse?> RegisterAsync(RegisterRequest request)
{
var response = await _httpClient.PostAsJsonAsync("api/auth/register", request);
if (!response.IsSuccessStatusCode)
return null;
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
if (result is not null)
await _tokenStorage.SaveTokenAsync(result.Token);
return result;
}
public async Task<AuthResponse?> LoginAsync(LoginRequest request)
{
var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
if (!response.IsSuccessStatusCode)
return null;
var result = await response.Content.ReadFromJsonAsync<AuthResponse>();
if (result is not null)
await _tokenStorage.SaveTokenAsync(result.Token);
return result;
}
public async Task<AuthResponse?> GetCurrentUserAsync(Guid userId)
{
var token = await _tokenStorage.GetTokenAsync();
if (string.IsNullOrEmpty(token))
return null;
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.GetAsync("api/auth/me");
if (!response.IsSuccessStatusCode)
return null;
return await response.Content.ReadFromJsonAsync<AuthResponse>();
}
public async Task<bool> IsAuthenticatedAsync()
{
var token = await _tokenStorage.GetTokenAsync();
return !string.IsNullOrEmpty(token);
}
public async Task LogoutAsync()
{
await _tokenStorage.RemoveTokenAsync();
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
using LoopFit.Shared.Services; using LoopFit.Application.Interfaces;
namespace LoopFit.Services; namespace LoopFit.Services;
+29
View File
@@ -0,0 +1,29 @@
namespace LoopFit.Services;
public class TokenStorageService
{
private const string TokenKey = "loopfit_jwt_token";
public async Task SaveTokenAsync(string token)
{
await SecureStorage.Default.SetAsync(TokenKey, token);
}
public async Task<string?> GetTokenAsync()
{
try
{
return await SecureStorage.Default.GetAsync(TokenKey);
}
catch
{
return null;
}
}
public Task RemoveTokenAsync()
{
SecureStorage.Default.Remove(TokenKey);
return Task.CompletedTask;
}
}
+38
View File
@@ -1,3 +1,41 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.status-bar-safe-area { .status-bar-safe-area {
display: none; display: none;
} }
+1 -2
View File
@@ -5,8 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>LoopFit</title> <title>LoopFit</title>
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="_content/LoopFit.Shared/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="_content/LoopFit.Shared/app.css" />
<link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="LoopFit.styles.css" /> <link rel="stylesheet" href="LoopFit.styles.css" />
<link rel="icon" href="data:,"> <link rel="icon" href="data:,">
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
namespace LoopFit.Application.DTOs.Auth;
public class AuthResponse
{
public string Token { get; set; } = string.Empty;
public DateTime Expiration { get; set; }
public string UserName { get; set; } = string.Empty;
}
@@ -0,0 +1,7 @@
namespace LoopFit.Application.DTOs.Auth;
public class LoginRequest
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
@@ -0,0 +1,9 @@
namespace LoopFit.Application.DTOs.Auth;
public class RegisterRequest
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
@@ -0,0 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using LoopFit.Application.Interfaces;
namespace LoopFit.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<IAuthService>(sp =>
throw new InvalidOperationException("IAuthService muss in Infrastructure registriert werden."));
return services;
}
}
@@ -0,0 +1,10 @@
using LoopFit.Application.DTOs.Auth;
namespace LoopFit.Application.Interfaces;
public interface IAuthService
{
Task<AuthResponse?> RegisterAsync(RegisterRequest request);
Task<AuthResponse?> LoginAsync(LoginRequest request);
Task<AuthResponse?> GetCurrentUserAsync(Guid userId);
}
@@ -0,0 +1,7 @@
namespace LoopFit.Application.Interfaces;
public interface IFormFactor
{
string GetFormFactor();
string GetPlatform();
}
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.11"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LoopFit.Domain\LoopFit.Domain.csproj"/>
</ItemGroup>
</Project>
@@ -0,0 +1,10 @@
namespace LoopFit.Domain.Entities;
public class ApplicationUser
{
public Guid Id { get; set; }
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,11 @@
namespace LoopFit.Domain.Entities;
public class CalorieEntry
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public int Calories { get; set; }
public DateTime Date { get; set; } = DateTime.UtcNow;
public string? Notes { get; set; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace LoopFit.Domain.Entities;
public class VitalData
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public int? HeartRate { get; set; }
public int? Steps { get; set; }
public double? SleepHours { get; set; }
public int? CaloriesBurned { get; set; }
public DateTime Date { get; set; } = DateTime.UtcNow;
public string? Source { get; set; }
}
+15
View File
@@ -0,0 +1,15 @@
using LoopFit.Domain.Enums;
namespace LoopFit.Domain.Entities;
public class Workout
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string Name { get; set; } = string.Empty;
public WorkoutType Type { get; set; }
public int DurationMinutes { get; set; }
public int CaloriesBurned { get; set; }
public DateTime Date { get; set; } = DateTime.UtcNow;
public string? Notes { get; set; }
}
+10
View File
@@ -0,0 +1,10 @@
namespace LoopFit.Domain.Enums;
public enum WorkoutType
{
Strength,
Cardio,
Endurance,
Flexibility,
Other
}
@@ -0,0 +1,6 @@
namespace LoopFit.Domain.Interfaces;
public interface IJwtTokenGenerator
{
string GenerateToken(Guid userId, string email);
}
@@ -0,0 +1,9 @@
using LoopFit.Domain.Entities;
namespace LoopFit.Domain.Interfaces;
public interface IUserRepository
{
Task<ApplicationUser?> GetByIdAsync(Guid id);
Task<ApplicationUser?> GetByEmailAsync(string email);
}
@@ -0,0 +1,10 @@
using LoopFit.Domain.Entities;
namespace LoopFit.Domain.Interfaces;
public interface IWorkoutRepository
{
Task<Workout?> GetByIdAsync(Guid id);
Task<IEnumerable<Workout>> GetByUserIdAsync(Guid userId);
Task AddAsync(Workout workout);
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using LoopFit.Domain.Entities;
namespace LoopFit.Infrastructure.Data;
public class AppDbContext : IdentityDbContext<IdentityUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<ApplicationUser> ApplicationUsers => Set<ApplicationUser>();
public DbSet<Workout> Workouts => Set<Workout>();
public DbSet<CalorieEntry> CalorieEntries => Set<CalorieEntry>();
public DbSet<VitalData> VitalData => Set<VitalData>();
}
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using LoopFit.Application.Interfaces;
using LoopFit.Domain.Interfaces;
using LoopFit.Infrastructure.Data;
using LoopFit.Infrastructure.Identity;
using LoopFit.Infrastructure.Repositories;
using LoopFit.Infrastructure.Services;
namespace LoopFit.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<IdentityUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<IJwtTokenGenerator, JwtTokenGenerator>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IFormFactor, FormFactor>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IWorkoutRepository, WorkoutRepository>();
return services;
}
}
@@ -0,0 +1,104 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using LoopFit.Application.DTOs.Auth;
using LoopFit.Application.Interfaces;
using LoopFit.Domain.Entities;
using LoopFit.Domain.Interfaces;
using LoopFit.Infrastructure.Data;
namespace LoopFit.Infrastructure.Identity;
public class AuthService : IAuthService
{
private readonly UserManager<IdentityUser> _userManager;
private readonly IJwtTokenGenerator _jwtTokenGenerator;
private readonly AppDbContext _dbContext;
public AuthService(
UserManager<IdentityUser> userManager,
IJwtTokenGenerator jwtTokenGenerator,
AppDbContext dbContext)
{
_userManager = userManager;
_jwtTokenGenerator = jwtTokenGenerator;
_dbContext = dbContext;
}
public async Task<AuthResponse?> RegisterAsync(RegisterRequest request)
{
var identityUser = new IdentityUser
{
UserName = request.Email,
Email = request.Email
};
var result = await _userManager.CreateAsync(identityUser, request.Password);
if (!result.Succeeded)
return null;
var appUser = new ApplicationUser
{
Id = Guid.Parse(identityUser.Id),
Email = request.Email,
FirstName = request.FirstName,
LastName = request.LastName
};
_dbContext.ApplicationUsers.Add(appUser);
await _dbContext.SaveChangesAsync();
var token = _jwtTokenGenerator.GenerateToken(appUser.Id, appUser.Email);
return new AuthResponse
{
Token = token,
Expiration = DateTime.UtcNow.AddMinutes(60),
UserName = $"{appUser.FirstName} {appUser.LastName}"
};
}
public async Task<AuthResponse?> LoginAsync(LoginRequest request)
{
var identityUser = await _userManager.FindByEmailAsync(request.Email);
if (identityUser is null)
return null;
var isValidPassword = await _userManager.CheckPasswordAsync(identityUser, request.Password);
if (!isValidPassword)
return null;
var appUser = await _dbContext.ApplicationUsers
.FirstOrDefaultAsync(u => u.Id == Guid.Parse(identityUser.Id));
var userId = Guid.Parse(identityUser.Id);
var token = _jwtTokenGenerator.GenerateToken(userId, identityUser.Email!);
return new AuthResponse
{
Token = token,
Expiration = DateTime.UtcNow.AddMinutes(60),
UserName = appUser != null ? $"{appUser.FirstName} {appUser.LastName}" : identityUser.Email!
};
}
public async Task<AuthResponse?> GetCurrentUserAsync(Guid userId)
{
var appUser = await _dbContext.ApplicationUsers
.FirstOrDefaultAsync(u => u.Id == userId);
if (appUser is null)
return null;
var token = _jwtTokenGenerator.GenerateToken(appUser.Id, appUser.Email);
return new AuthResponse
{
Token = token,
Expiration = DateTime.UtcNow.AddMinutes(60),
UserName = $"{appUser.FirstName} {appUser.LastName}"
};
}
}
@@ -0,0 +1,41 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using LoopFit.Domain.Interfaces;
namespace LoopFit.Infrastructure.Identity;
public class JwtTokenGenerator : IJwtTokenGenerator
{
private readonly IConfiguration _configuration;
public JwtTokenGenerator(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateToken(Guid userId, string email)
{
var jwtSettings = _configuration.GetSection("Jwt");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]!));
var expiration = DateTime.UtcNow.AddMinutes(Convert.ToDouble(jwtSettings["ExpirationMinutes"]));
var claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, userId.ToString()),
new(ClaimTypes.Email, email),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires: expiration,
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.11"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.11"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.22.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LoopFit.Application\LoopFit.Application.csproj"/>
</ItemGroup>
</Project>
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using LoopFit.Domain.Entities;
using LoopFit.Domain.Interfaces;
using LoopFit.Infrastructure.Data;
namespace LoopFit.Infrastructure.Repositories;
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context)
{
_context = context;
}
public async Task<ApplicationUser?> GetByIdAsync(Guid id)
{
return await _context.ApplicationUsers.FindAsync(id);
}
public async Task<ApplicationUser?> GetByEmailAsync(string email)
{
return await _context.ApplicationUsers
.FirstOrDefaultAsync(u => u.Email == email);
}
}
@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using LoopFit.Domain.Entities;
using LoopFit.Domain.Interfaces;
using LoopFit.Infrastructure.Data;
namespace LoopFit.Infrastructure.Repositories;
public class WorkoutRepository : IWorkoutRepository
{
private readonly AppDbContext _context;
public WorkoutRepository(AppDbContext context)
{
_context = context;
}
public async Task<Workout?> GetByIdAsync(Guid id)
{
return await _context.Workouts.FindAsync(id);
}
public async Task<IEnumerable<Workout>> GetByUserIdAsync(Guid userId)
{
return await _context.Workouts
.Where(w => w.UserId == userId)
.OrderByDescending(w => w.Date)
.ToListAsync();
}
public async Task AddAsync(Workout workout)
{
_context.Workouts.Add(workout);
await _context.SaveChangesAsync();
}
}
@@ -1,6 +1,6 @@
using LoopFit.Shared.Services; using LoopFit.Application.Interfaces;
namespace LoopFit.Web.Services; namespace LoopFit.Infrastructure.Services;
public class FormFactor : IFormFactor public class FormFactor : IFormFactor
{ {