Initial commit: base slice (auth, roles, users, admin) scaffold
Backend: .NET 10 Clean Architecture + LiteCqrs.Net + EF Core/PostgreSQL + Identity/JWT. Frontend: React 19 + Vite + TanStack Query/Router + Tailwind v4 with a retro CRT theme. Docker/compose deployment mirroring PnvPanel's conventions, scoped down to the current base feature set.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Api.Common;
|
||||
using TeleWave.Application.Auth;
|
||||
using TeleWave.Application.Auth.ChangePassword;
|
||||
using TeleWave.Application.Auth.ChangeUserName;
|
||||
using TeleWave.Application.Auth.DeleteMyAccount;
|
||||
using TeleWave.Application.Auth.Login;
|
||||
using TeleWave.Application.Auth.Logout;
|
||||
using TeleWave.Application.Auth.Me;
|
||||
using TeleWave.Application.Auth.Refresh;
|
||||
using TeleWave.Application.Auth.Register;
|
||||
|
||||
namespace TeleWave.Api.Endpoints;
|
||||
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
private const string RefreshCookieName = "tw_refresh_token";
|
||||
|
||||
public static IEndpointRouteBuilder MapAuthEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/auth")
|
||||
.WithTags("Auth")
|
||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||
|
||||
group.MapPost("/register", Register).Produces<AuthResponseDto>();
|
||||
group.MapPost("/login", Login).Produces<AuthResponseDto>();
|
||||
group.MapPost("/refresh", Refresh).Produces<AuthResponseDto>();
|
||||
group
|
||||
.MapPost("/logout", Logout)
|
||||
.RequireAuthorization()
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
group
|
||||
.MapPost("/change-password", ChangePassword)
|
||||
.RequireAuthorization()
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
group
|
||||
.MapPost("/change-username", ChangeUserName)
|
||||
.RequireAuthorization()
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
group.MapGet("/me", Me).RequireAuthorization().Produces<CurrentUserDto>();
|
||||
group
|
||||
.MapDelete("/me", DeleteMe)
|
||||
.RequireAuthorization()
|
||||
.Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<IResult> Register(
|
||||
RegisterCommand command,
|
||||
ISender sender,
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToHttpResult();
|
||||
|
||||
SetRefreshCookie(
|
||||
request,
|
||||
response,
|
||||
result.Value.RefreshToken,
|
||||
result.Value.RefreshTokenExpiresAt
|
||||
);
|
||||
return Results.Ok(ToLoginResponse(result.Value));
|
||||
}
|
||||
|
||||
private static async Task<IResult> Login(
|
||||
LoginCommand command,
|
||||
ISender sender,
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
return result.ToHttpResult();
|
||||
|
||||
SetRefreshCookie(
|
||||
request,
|
||||
response,
|
||||
result.Value.RefreshToken,
|
||||
result.Value.RefreshTokenExpiresAt
|
||||
);
|
||||
return Results.Ok(ToLoginResponse(result.Value));
|
||||
}
|
||||
|
||||
private static async Task<IResult> Refresh(
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (
|
||||
!request.Cookies.TryGetValue(RefreshCookieName, out var rawToken)
|
||||
|| string.IsNullOrEmpty(rawToken)
|
||||
)
|
||||
return Results.Unauthorized();
|
||||
|
||||
var result = await sender.Send(new RefreshCommand(rawToken), cancellationToken);
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(request));
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
SetRefreshCookie(
|
||||
request,
|
||||
response,
|
||||
result.Value.RefreshToken,
|
||||
result.Value.RefreshTokenExpiresAt
|
||||
);
|
||||
return Results.Ok(ToLoginResponse(result.Value));
|
||||
}
|
||||
|
||||
internal static AuthResponseDto ToLoginResponse(AuthResult auth) =>
|
||||
new(auth.AccessToken, auth.AccessTokenExpiresAt, auth.User);
|
||||
|
||||
private static async Task<IResult> Logout(
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (
|
||||
request.Cookies.TryGetValue(RefreshCookieName, out var rawToken)
|
||||
&& !string.IsNullOrEmpty(rawToken)
|
||||
)
|
||||
await sender.Send(new LogoutCommand(rawToken), cancellationToken);
|
||||
|
||||
response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(request));
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ChangePassword(
|
||||
ChangePasswordCommand command,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> ChangeUserName(
|
||||
ChangeUserNameCommand command,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> Me(ISender sender, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await sender.Send(new GetCurrentUserQuery(), cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> DeleteMe(
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
ISender sender,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var result = await sender.Send(new DeleteMyAccountCommand(), cancellationToken);
|
||||
response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(request));
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static void SetRefreshCookie(
|
||||
HttpRequest request,
|
||||
HttpResponse response,
|
||||
string rawToken,
|
||||
DateTimeOffset expiresAt
|
||||
)
|
||||
{
|
||||
var options = BuildCookieOptions(request);
|
||||
options.Expires = expiresAt;
|
||||
response.Cookies.Append(RefreshCookieName, rawToken, options);
|
||||
}
|
||||
|
||||
private static CookieOptions BuildCookieOptions(HttpRequest request) =>
|
||||
new()
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = request.IsHttps,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Path = "/api/auth",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record AuthResponseDto(
|
||||
string AccessToken,
|
||||
DateTimeOffset ExpiresAt,
|
||||
CurrentUserDto User
|
||||
);
|
||||
Reference in New Issue
Block a user