Files
admin/_docs/02_document/modules/services_user_service.md
T
Oleksandr Bezdieniezhnykh c7b297de83
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
refactor: remove deploy.cmd and update Dockerfile for health checks
- Deleted the deploy.cmd script as it was no longer needed.
- Updated Dockerfile to include curl for health checks and added a non-root user for improved security.
- Modified health check command to use curl for better reliability.
- Adjusted docker-compose.test.yml to reflect changes in health check configuration.
- Cleaned up appsettings.json and removed unused configuration properties.
- Removed Resource entity and related requests from the codebase as part of the architectural shift.
- Updated documentation to reflect the removal of hardware binding and related endpoints.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 08:47:21 +03:00

5.6 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, private UpdateLastLoginDate) and the bound IUserService declarations 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): RegisterDevice was refactored to delegate the row insert to RegisterUser, and RegisterUser itself now relies on the new users_email_uidx UNIQUE INDEX (env/db/06_users_email_unique.sql) — the check-then-insert race is gone; Npgsql.PostgresException(SqlState=23505) is translated to BusinessException(EmailExists). See _docs/03_implementation/batch_05_report.md and batch_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 via RunAdmin. Catches Npgsql.PostgresException with SqlState == PostgresErrorCodes.UniqueViolation (23505) on the users_email_uidx UNIQUE INDEX and rethrows as BusinessException(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 next azj-NNNN serial + matching email, generates a 32-char hex password from RandomNumberGenerator.GetBytes(16), then delegates the row insert to RegisterUser (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 surfaces BusinessException(EmailExists); the caller can retry.
  • NextDeviceIdentity (private): queries the most recent RoleEnum.CompanionPC user via dbFactory.Run (read connection), parses the azj-NNNN suffix (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, or UserDisabled.
  • GetByEmail: uses ICache.GetFromCacheAsync with key User.{email}.
  • UpdateQueueOffsets: writes via RunAdmin, then invalidates the user cache.
  • GetUsers: uses WhereIf for 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 to BusinessException(EmailExists))
  • BusinessException (domain errors)
  • QueryableExtensions.WhereIf
  • User, UserConfig, UserQueueOffsets, RoleEnum
  • RegisterUserRequest, LoginRequest, RegisterDeviceResponse

Consumers

  • Program.cs/users/* endpoints delegate to IUserService
  • Program.csPOST /devices calls RegisterDevice (added by AZ-196)
  • AuthService.GetCurrentUser — calls GetByEmail

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

  • Azaion.Test/UserServiceTest.cs — unit/integration tests against the live test database (hardware-binding tests removed by AZ-197)
  • e2e/Azaion.E2E/Tests/DeviceTests.cs — e2e for AZ-196 device-provisioning ACs