Files
SNote/Server/Endpoints/AuthEndpoints.cs
T
2026-06-01 06:38:20 +10:00

158 lines
6.5 KiB
C#

using System;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using SNote.Models;
using SNote.Server.Data;
using SNote.Server.Security;
namespace SNote.Server.Endpoints;
public static class AuthEndpoints
{
public static void MapAuthEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/auth");
group.MapPost("/register", async (RegisterRequest req, HttpContext context, ServerDbContext db, PeerCache peerCache, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient) =>
{
if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password) || string.IsNullOrWhiteSpace(req.EncryptedKey))
{
return Results.BadRequest("Username, password, and encrypted key are required.");
}
var existing = await db.Users.AnyAsync(u => u.Username.ToLower() == req.Username.ToLower());
if (existing)
{
return Results.Conflict("Username already exists.");
}
var user = new User
{
Username = req.Username,
PasswordHash = PasswordHasher.HashPassword(req.Password),
EncryptedKey = req.EncryptedKey
};
db.Users.Add(user);
await db.SaveChangesAsync();
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
await SyncEndpoints.BroadcastUserChangeAsync(user.Username, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting user registration: {ex.Message}");
}
});
return Results.Ok(new { message = "Registration successful." });
});
group.MapPost("/change-password", async (ChangePasswordRequest req, HttpContext context, ServerDbContext db, CertificateManager certManager, PeerCache peerCache, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient) =>
{
var username = AuthHelper.GetAuthenticatedUser(context, certManager);
if (string.IsNullOrEmpty(username)) return Results.Unauthorized();
if (string.IsNullOrWhiteSpace(req.OldPassword) || string.IsNullOrWhiteSpace(req.NewPassword) || string.IsNullOrWhiteSpace(req.NewEncryptedKey))
{
return Results.BadRequest("Old password, new password, and new encrypted key are required.");
}
var user = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
if (user == null) return Results.NotFound("User not found.");
// Verify old password
if (!PasswordHasher.VerifyPassword(user.PasswordHash, req.OldPassword))
{
return Results.BadRequest("Incorrect old password.");
}
// Update user properties
user.PasswordHash = PasswordHasher.HashPassword(req.NewPassword);
user.EncryptedKey = req.NewEncryptedKey;
await db.SaveChangesAsync();
// Broadcast the user password change!
_ = Task.Run(async () =>
{
try
{
using var scope = context.RequestServices.CreateScope();
var scopeDb = scope.ServiceProvider.GetRequiredService<ServerDbContext>();
await SyncEndpoints.BroadcastUserChangeAsync(user.Username, scopeDb, peerCache, rsaKeyManager, configuration, httpClient);
}
catch (Exception ex)
{
Console.WriteLine($"Error broadcasting password change: {ex.Message}");
}
});
return Results.Ok(new { message = "Password changed successfully." });
});
group.MapPost("/login", async (LoginRequest req, ServerDbContext db, CertificateManager certManager) =>
{
if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password))
{
return Results.BadRequest("Username and password are required.");
}
var user = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == req.Username.ToLower());
if (user == null || !PasswordHasher.VerifyPassword(user.PasswordHash, req.Password))
{
return Results.Unauthorized();
}
// Generate JWT Token signed by shared certificate
var cert = certManager.GetCertificate();
var privateKey = new X509SecurityKey(cert);
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, "User")
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
return Results.Ok(new LoginResponse(tokenString, user.EncryptedKey));
});
group.MapGet("/encrypted-key/{username}", async (string username, ServerDbContext db) =>
{
var user = await db.Users.FirstOrDefaultAsync(u => u.Username.ToLower() == username.ToLower());
if (user == null)
{
return Results.NotFound("User not found.");
}
return Results.Ok(new { encryptedKey = user.EncryptedKey });
});
}
}
public record RegisterRequest(string Username, string Password, string EncryptedKey);
public record LoginRequest(string Username, string Password);
public record LoginResponse(string Token, string EncryptedKey);
public record ChangePasswordRequest(string OldPassword, string NewPassword, string NewEncryptedKey);