femto-backend/Femto.Modules.Auth/Application/AuthService.cs
2025-06-21 11:41:53 +02:00

237 lines
6.6 KiB
C#

using Dapper;
using Femto.Common.Domain;
using Femto.Common.Infrastructure.DbConnection;
using Femto.Modules.Auth.Application.Dto;
using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Infrastructure;
using Femto.Modules.Auth.Models;
using Microsoft.EntityFrameworkCore;
namespace Femto.Modules.Auth.Application;
internal class AuthService(
AuthContext context,
SessionStorage storage,
IDbConnectionFactory connectionFactory
) : IAuthService
{
public async Task<UserAndSession?> AuthenticateUserCredentials(
string username,
string password,
CancellationToken cancellationToken = default
)
{
var user = await context
.Users.Where(u => u.Username == username)
.SingleOrDefaultAsync(cancellationToken);
if (user is null)
return null;
if (!user.HasPassword(password))
return null;
var session = new Session(user.Id, true);
await storage.AddSession(session);
return new(
new UserInfo(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()),
session
);
}
public Task<UserInfo?> GetUserWithId(Guid? userId, CancellationToken cancellationToken)
{
return context
.Users.Where(u => u.Id == userId)
.Select(u => new UserInfo(u.Id, u.Username, u.Roles.Select(r => r.Role).ToList()))
.SingleOrDefaultAsync(cancellationToken);
}
public async Task<Session> CreateNewSession(Guid userId)
{
var session = new Session(userId, true);
await storage.AddSession(session);
return session;
}
public async Task<Session> CreateWeakSession(Guid userId)
{
var session = new Session(userId, false);
await storage.AddSession(session);
return session;
}
public Task<Session?> GetSession(string sessionId)
{
return storage.GetSession(sessionId);
}
public async Task DeleteSession(string sessionId)
{
await storage.DeleteSession(sessionId);
}
public async Task<UserAndSession> CreateUserWithCredentials(
string username,
string password,
string signupCode,
CancellationToken cancellationToken = default
)
{
var now = DateTimeOffset.UtcNow;
var code = await context
.SignupCodes.Where(c => c.Code == signupCode)
.Where(c => c.ExpiresAt == null || c.ExpiresAt > now)
.Where(c => c.RedeemingUserId == null)
.SingleOrDefaultAsync(cancellationToken);
if (code is null)
throw new DomainError("invalid signup code");
var usernameTaken = await context.Users.AnyAsync(
u => u.Username == username,
cancellationToken
);
if (usernameTaken)
throw new DomainError("username taken");
var user = new UserIdentity(username);
await context.AddAsync(user, cancellationToken);
user.SetPassword(password);
code.Redeem(user.Id);
var session = new Session(user.Id, true);
await storage.AddSession(session);
await context.SaveChangesAsync(cancellationToken);
return new(new UserInfo(user), session);
}
public async Task AddSignupCode(
string code,
string recipientName,
CancellationToken cancellationToken
)
{
await context.SignupCodes.AddAsync(
new SignupCode("", recipientName, code),
cancellationToken
);
await context.SaveChangesAsync(cancellationToken);
}
public async Task<ICollection<SignupCodeDto>> GetSignupCodes(
CancellationToken cancellationToken = default
)
{
using var conn = connectionFactory.GetConnection();
// lang=sql
const string sql = """
SELECT
sc.code as Code,
sc.recipient_email as Email,
sc.recipient_name as Name,
sc.redeeming_user_id as RedeemedByUserId,
u.username as RedeemedByUsername,
sc.expires_at as ExpiresOn
FROM authn.signup_code sc
LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id
ORDER BY sc.created_at DESC
""";
var result = await conn.QueryAsync<GetSignupCodesQueryResultRow>(sql, cancellationToken);
return result
.Select(row => new SignupCodeDto(
row.Code,
row.Email,
row.Name,
row.RedeemedByUserId,
row.RedeemedByUsername,
row.ExpiresOn
))
.ToList();
}
public async Task<NewRememberMeToken> CreateRememberMeToken(Guid userId)
{
var (rememberMeToken, verifier) = LongTermSession.Create(userId);
await context.AddAsync(rememberMeToken);
await context.SaveChangesAsync();
return new(rememberMeToken.Selector, verifier, rememberMeToken.Expires);
}
public async Task<(UserInfo?, NewRememberMeToken?)> GetUserWithRememberMeToken(
RememberMeToken rememberMeToken
)
{
var token = await context.LongTermSessions.SingleOrDefaultAsync(t =>
t.Selector == rememberMeToken.Selector
);
if (token is null)
return (null, null);
if (!token.Validate(rememberMeToken.Verifier))
return (null, null);
var user = await context.Users.SingleOrDefaultAsync(u => u.Id == token.UserId);
if (user is null)
return (null, null);
if (token.ExpiresSoon)
{
var (newToken, verifier) = LongTermSession.Create(user.Id);
await context.AddAsync(newToken);
await context.SaveChangesAsync();
return (new(user), new(newToken.Selector, verifier, newToken.Expires));
}
return (new(user), null);
}
public async Task DeleteRememberMeToken(RememberMeToken rememberMeToken)
{
var session = await context.LongTermSessions.SingleOrDefaultAsync(s =>
s.Selector == rememberMeToken.Selector
);
if (session is null)
return;
if (!session.Validate(rememberMeToken.Verifier))
return;
context.Remove(session);
await context.SaveChangesAsync();
}
private class GetSignupCodesQueryResultRow
{
public string Code { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public Guid? RedeemedByUserId { get; set; }
public string? RedeemedByUsername { get; set; }
public DateTimeOffset? ExpiresOn { get; set; }
}
}