e4091eee80
- 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.
101 lines
3.6 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|