Files
nexus/backend/Repositories/UserRepository.cs
T
devops e4091eee80
CI - Build & Test / Backend (.NET) (push) Successful in 35s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 20s
CI - Build & Test / Security Check (push) Successful in 4s
feat: Multi-User/Admin usermanagement + Galaxy Login/Settings + Task detail improvements
- Backend: NEW AdminController with user CRUD (GET/POST/DELETE /api/v1/admin/users)
- Backend: NEW GET /api/dashboard/tasks/{id} single task endpoint
- Backend: NEW POST /api/dashboard/tasks/{id}/activity comment endpoint
- Backend: IUserRepository + UserRepository extended with GetAllAsync, DeleteAsync
- Backend: Admin DTOs (AdminUserInfo, AdminCreateUserRequest, AdminUpdateRoleRequest)
- Frontend: NEW TaskDetailView.vue — URL-based (/tasks/:id) Galaxy-themed task detail
  with subtask create/edit/delete, activity with comments, property sidebar
- Frontend: LoginView.vue — полностью Galaxy theme redesign with GalaxyBackground,
  glass-morphism card, password toggle, consistent brand
- Frontend: SettingsView.vue — Galaxy theme redesign with glass cards,
  admin user management section (create/list users, visible only to owner role)
- Frontend: TaskBoardView.vue — added "Full View" link to URL-based detail page
- Frontend: Router — added /tasks/:id route for TaskDetailView
- Frontend: App.vue — added TaskDetail to standaloneViews whitelist
- Frontend: tasks store — stable

Auth: Admin creates accounts, users log in with existing /api/v1/auth/login.
Login/Settings deliver visible Galaxy-consistent design with nexus-tokens.css tokens.
2026-06-20 14:24:40 +02:00

101 lines
3.6 KiB
C#

using Microsoft.EntityFrameworkCore;
using Nexus.Api.Data;
namespace Nexus.Api.Repositories;
public sealed class UserRepository(NexusDbContext db) : IUserRepository
{
public ValueTask<NexusUser?> GetByIdAsync(Guid userId, CancellationToken ct = default)
=> db.Users.FindAsync([userId], ct);
public Task<NexusUser?> GetByEmailAsync(string normalizedEmail, CancellationToken ct = default)
=> db.Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, ct);
public Task<List<NexusUser>> GetAllAsync(CancellationToken ct = default)
=> db.Users.OrderBy(u => u.CreatedAt).ToListAsync(ct);
public Task<bool> AnyUsersAsync(CancellationToken ct = default)
=> db.Users.AnyAsync(ct);
public async Task<NexusUser> AddAsync(NexusUser user, CancellationToken ct = default)
{
db.Users.Add(user);
await db.SaveChangesAsync(ct);
return user;
}
public Task UpdateAsync(NexusUser user, CancellationToken ct = default)
=> db.SaveChangesAsync(ct);
public async Task DeleteAsync(NexusUser user, CancellationToken ct = default)
{
// Remove refresh tokens first
var tokens = await db.RefreshTokens
.Where(r => r.UserId == user.Id)
.ToListAsync(ct);
db.RefreshTokens.RemoveRange(tokens);
db.Users.Remove(user);
await db.SaveChangesAsync(ct);
}
public Task<RefreshToken?> GetRefreshTokenByHashAsync(string tokenHash, CancellationToken ct = default)
=> db.RefreshTokens
.Include(r => r.User)
.FirstOrDefaultAsync(r => r.TokenHash == tokenHash, ct);
public Task<List<RefreshToken>> GetActiveTokensByFamilyAsync(Guid familyId, CancellationToken ct = default)
=> db.RefreshTokens
.Where(r => r.FamilyId == familyId && r.RevokedAt == null)
.ToListAsync(ct);
public async Task AddRefreshTokenAsync(RefreshToken token, CancellationToken ct = default)
{
db.RefreshTokens.Add(token);
await db.SaveChangesAsync(ct);
}
public Task UpdateRefreshTokenAsync(RefreshToken token, CancellationToken ct = default)
=> db.SaveChangesAsync(ct);
public async Task RevokeTokenAsync(string tokenHash, CancellationToken ct = default)
{
var token = await db.RefreshTokens.FirstOrDefaultAsync(r => r.TokenHash == tokenHash, ct);
if (token is null || token.RevokedAt is not null) return;
token.RevokedAt = DateTimeOffset.UtcNow;
token.ConcurrencyStamp = Guid.NewGuid();
await db.SaveChangesAsync(ct);
}
public async Task RevokeFamilyAsync(Guid familyId, CancellationToken ct = default)
{
var activeTokens = await db.RefreshTokens
.Where(r => r.FamilyId == familyId && r.RevokedAt == null)
.ToListAsync(ct);
if (activeTokens.Count == 0) return;
var now = DateTimeOffset.UtcNow;
foreach (var token in activeTokens)
{
token.RevokedAt = now;
token.ConcurrencyStamp = Guid.NewGuid();
}
await db.SaveChangesAsync(ct);
}
public async Task RemoveExpiredTokensAsync(Guid userId, CancellationToken ct = default)
{
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
var oldTokens = await db.RefreshTokens
.Where(r => r.UserId == userId && (r.ExpiresAt < DateTimeOffset.UtcNow || r.RevokedAt < cutoff))
.ToListAsync(ct);
if (oldTokens.Count > 0)
{
db.RefreshTokens.RemoveRange(oldTokens);
await db.SaveChangesAsync(ct);
}
}
}