From 2a208e81b2295c8427f0fab0aff20c711d1929b4 Mon Sep 17 00:00:00 2001 From: Assistant builder Date: Thu, 11 Jun 2026 18:34:07 +0000 Subject: [PATCH] Add /api/auth/{register,login,logout,me} with first-user-is-admin rule --- backend.tests/AuthTests.cs | 50 +++++++++++++++++++++++++++ backend/Auth/AuthEndpoints.cs | 64 +++++++++++++++++++++++++++++++++++ backend/Program.cs | 2 ++ 3 files changed, 116 insertions(+) create mode 100644 backend.tests/AuthTests.cs create mode 100644 backend/Auth/AuthEndpoints.cs diff --git a/backend.tests/AuthTests.cs b/backend.tests/AuthTests.cs new file mode 100644 index 0000000..9221b55 --- /dev/null +++ b/backend.tests/AuthTests.cs @@ -0,0 +1,50 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using Xunit; + +namespace backend.tests; + +public class AuthTests(ApiFactory factory) : IClassFixture +{ + private readonly ApiFactory _factory = factory; + + [Fact] + public async Task Register_then_login_then_me_returns_user() + { + var client = _factory.CreateClient(); + + var reg = await client.PostAsJsonAsync("/api/auth/register", + new { email = "first@example.com", password = "Passw0rd!" }); + reg.StatusCode.Should().Be(HttpStatusCode.OK); + + var login = await client.PostAsJsonAsync("/api/auth/login", + new { email = "first@example.com", password = "Passw0rd!" }); + login.StatusCode.Should().Be(HttpStatusCode.OK); + + var me = await client.GetAsync("/api/auth/me"); + me.StatusCode.Should().Be(HttpStatusCode.OK); + (await me.Content.ReadAsStringAsync()).Should().Contain("first@example.com"); + } + + [Fact] + public async Task Me_without_login_returns_401() + { + var client = _factory.CreateClient(); + var res = await client.GetAsync("/api/auth/me"); + res.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task First_user_is_admin() + { + var client = _factory.CreateClient(); + await client.PostAsJsonAsync("/api/auth/register", + new { email = "first@example.com", password = "Passw0rd!" }); + await client.PostAsJsonAsync("/api/auth/login", + new { email = "first@example.com", password = "Passw0rd!" }); + + var me = await client.GetAsync("/api/auth/me"); + (await me.Content.ReadAsStringAsync()).Should().Contain("\"isAdmin\":true"); + } +} diff --git a/backend/Auth/AuthEndpoints.cs b/backend/Auth/AuthEndpoints.cs new file mode 100644 index 0000000..673b512 --- /dev/null +++ b/backend/Auth/AuthEndpoints.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace backend.Auth; + +public record RegisterRequest(string Email, string Password); +public record LoginRequest(string Email, string Password); + +public static class AuthEndpoints +{ + public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder app) + { + var grp = app.MapGroup("/api/auth"); + + grp.MapPost("/register", async ( + [FromBody] RegisterRequest req, + UserManager users, + RoleManager> roles) => + { + var user = new ApplicationUser { UserName = req.Email, Email = req.Email }; + var create = await users.CreateAsync(user, req.Password); + if (!create.Succeeded) + return Results.BadRequest(new { errors = create.Errors.Select(e => e.Description) }); + + var anyAdmin = await users.GetUsersInRoleAsync(Roles.Admin); + var role = anyAdmin.Count == 0 ? Roles.Admin : Roles.User; + await users.AddToRoleAsync(user, role); + return Results.Ok(new { id = user.Id, email = user.Email, role }); + }); + + grp.MapPost("/login", async ( + [FromBody] LoginRequest req, + SignInManager signIn) => + { + var res = await signIn.PasswordSignInAsync(req.Email, req.Password, + isPersistent: true, lockoutOnFailure: false); + return res.Succeeded ? Results.Ok() : Results.Unauthorized(); + }); + + grp.MapPost("/logout", async (SignInManager signIn) => + { + await signIn.SignOutAsync(); + return Results.Ok(); + }).RequireAuthorization(); + + grp.MapGet("/me", async ( + UserManager users, + HttpContext http) => + { + var user = await users.GetUserAsync(http.User); + if (user is null) return Results.Unauthorized(); + var rolesList = await users.GetRolesAsync(user); + return Results.Ok(new + { + id = user.Id, + email = user.Email, + isAdmin = rolesList.Contains(Roles.Admin), + }); + }).RequireAuthorization(); + + return app; + } +} diff --git a/backend/Program.cs b/backend/Program.cs index 7c8fbbd..3dcfb4e 100644 --- a/backend/Program.cs +++ b/backend/Program.cs @@ -22,6 +22,8 @@ using (var scope = app.Services.CreateScope()) app.UseAuthentication(); app.UseAuthorization(); +app.MapAuthEndpoints(); + app.MapGet("/health", () => Results.Ok(new { ok = true })); app.Run();