- Deleted the `POST /resources/get/{dataFolder?}` and `GET /resources/get-installer` endpoints as part of the architectural shift towards simplified resource management.
- Removed associated methods and configurations, including `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, and related properties in `ResourcesConfig`.
- Cleaned up environment variables and configuration files to reflect the removal of installer-related settings.
- Eliminated the `GetResourceRequest` DTO and its validator, along with the `WrongResourceName` error code.
- Updated documentation to clarify the changes in resource handling and the retirement of per-user file encryption.
Co-authored-by: Cursor <cursoragent@cursor.com>
5.9 KiB
Module: Azaion.Services.UserService
Purpose
Core business logic for user management: registration (web users + provisioned devices), authentication, role management, and account lifecycle.
Cycle 1 (2026-05-13) note — hardware-binding methods (
UpdateHardware,CheckHardwareHash, privateUpdateLastLoginDate) and the boundIUserServicedeclarations were removed by AZ-197 (admin-side hardware-binding cleanup). Device auto-provisioning (RegisterDevice) was added by AZ-196. Post-cycle-1 (security audit F-3):RegisterDevicewas refactored to delegate the row insert toRegisterUser, andRegisterUseritself now relies on the newusers_email_uidxUNIQUE INDEX (env/db/06_users_email_unique.sql) — the check-then-insert race is gone;Npgsql.PostgresException(SqlState=23505)is translated toBusinessException(EmailExists). See_docs/03_implementation/batch_05_report.mdandbatch_06_report.md.
Public Interface
IUserService
| Method | Signature | Description |
|---|---|---|
RegisterUser |
Task RegisterUser(RegisterUserRequest request, CancellationToken ct) |
Creates a new user with hashed password |
RegisterDevice |
Task<RegisterDeviceResponse> RegisterDevice(CancellationToken ct) |
Creates a new CompanionPC user with auto-assigned azj-NNNN serial / email and a 32-char hex password (returned plaintext exactly once) |
ValidateUser |
Task<User> ValidateUser(LoginRequest request, CancellationToken ct) |
Validates email + password, returns user. Throws NoEmailFound, WrongPassword, or UserDisabled |
GetByEmail |
Task<User?> GetByEmail(string? email, CancellationToken ct) |
Cached user lookup by email |
UpdateQueueOffsets |
Task UpdateQueueOffsets(string email, UserQueueOffsets offsets, CancellationToken ct) |
Updates user's annotation queue offsets |
GetUsers |
Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken ct) |
Lists users with optional email/role filters |
ChangeRole |
Task ChangeRole(string email, RoleEnum newRole, CancellationToken ct) |
Changes a user's role |
SetEnableStatus |
Task SetEnableStatus(string email, bool isEnabled, CancellationToken ct) |
Enables or disables a user account |
RemoveUser |
Task RemoveUser(string email, CancellationToken ct) |
Permanently deletes a user |
Internal Logic
- RegisterUser: hashes password via
Security.ToHash, inserts viaRunAdmin. CatchesNpgsql.PostgresExceptionwithSqlState == PostgresErrorCodes.UniqueViolation(23505) on theusers_email_uidxUNIQUE INDEX and rethrows asBusinessException(EmailExists). The previous check-then-insert pattern was removed (race-prone before the index existed; redundant after). - RegisterDevice: calls private
NextDeviceIdentity(read-only) to compute the nextazj-NNNNserial + matching email, generates a 32-char hex password fromRandomNumberGenerator.GetBytes(16), then delegates the row insert toRegisterUser(so any future change to user-creation policy applies here too). Returns{Serial, Email, Password}(plaintext password exposed exactly once at provisioning time). On a serial-allocation race, the second caller's insert hits the UNIQUE INDEX and surfacesBusinessException(EmailExists); the caller can retry. - NextDeviceIdentity (private): queries the most recent
RoleEnum.CompanionPCuser viadbFactory.Run(read connection), parses theazj-NNNNsuffix (chars[SerialNumberStart, SerialNumberLength)of the email, constants on the class), increments by 1, returns(serial, email). - ValidateUser: finds user by email, compares password hash. Throws
NoEmailFound,WrongPassword, orUserDisabled. - GetByEmail: uses
ICache.GetFromCacheAsyncwith keyUser.{email}. - UpdateQueueOffsets: writes via
RunAdmin, then invalidates the user cache. - GetUsers: uses
WhereIffor optional filter predicates.
Private constants (device provisioning):
DeviceEmailPrefix = "azj-",DeviceEmailDomain = "@azaion.com",SerialNumberStart = 4,SerialNumberLength = 4,DevicePasswordBytes = 16.
Dependencies
IDbFactory(database access)ICache(user caching)Security(hashing —ToHash)System.Security.Cryptography.RandomNumberGenerator(device password entropy)Npgsql(PostgresException,PostgresErrorCodes.UniqueViolation— used to translate UNIQUE-INDEX violations toBusinessException(EmailExists))BusinessException(domain errors)QueryableExtensions.WhereIfUser,UserConfig,UserQueueOffsets,RoleEnumRegisterUserRequest,LoginRequest,RegisterDeviceResponse
Consumers
Program.cs—/users/*endpoints delegate toIUserServiceProgram.cs—POST /devicescallsRegisterDevice(added by AZ-196)AuthService.GetCurrentUser— callsGetByEmail
Data Models
Operates on User entity via AzaionDb.Users table. The User.Hardware column is left in place (nullable, unused) per AZ-197 — see the entity doc.
Configuration
None.
External Integrations
PostgreSQL via IDbFactory.
Security
- Passwords hashed with SHA-384 (via
Security.ToHash) before storage. - Device passwords are returned plaintext to the caller exactly once at provisioning; the persisted form is the SHA-384 hash. The plaintext is never re-derivable.
- Read operations use the read-only DB connection; writes use the admin connection.
Tests
e2e/Azaion.E2E/Tests/DeviceTests.cs— e2e for AZ-196 device-provisioning ACse2e/Azaion.E2E/Tests/UserManagementTests.csandLoginTests.cs— e2e coverage for the rest of the user lifecycle (login, register, role change, enable/disable, delete, queue offsets)
(Unit-test coverage in Azaion.Test/UserServiceTest.cs was removed earlier with the AZ-197 hardware-binding cleanup; the Azaion.Test project itself was removed from the solution in cycle 2 once its only remaining file — SecurityTest.cs — was deleted with the encrypted-download stack.)