Users¶
Purpose¶
UserService owns the lifecycle of a user account: deactivation (soft delete with 30-day grace), reactivation on next login, and the eventual hard delete that purges every owned row. It is intentionally separate from AuthService — auth issues credentials, users owns the account record itself, and they call into each other only at well-defined seams (AuthService.DeleteAccount → UserService.DeactivateUser).
Profile updates, password changes, avatar uploads, and phone/email changes also live on AuthService (because they piggyback on the same userRepo), but the GDPR-grade cleanup paths are all here.
Responsibilities¶
DeactivateUser— setdeactivated_at = now(), revoke every session/refresh token viaSessionManager.RevokeAllUserTokensReactivateUser— cleardeactivated_at(called fromAuthService.finalizeLoginwhen the user logs back in during the grace window)PurgeDeactivatedAccounts— find every user withdeactivated_at <= now() - 30dand callDeleteUserDeleteUser— hard delete with full cascade cleanup:- Revoke sessions
- Anonymise messages in group chats (
user_id = NULL) - Hard-delete messages, room, and participants for direct chats
- Delete friendships, event participations, refresh tokens, sessions, login history, WebAuthn credentials, API keys, audit logs
- Delete avatar from object storage
- Delete moment media files from S3 (DB rows are CASCADE'd via FK)
Unscoped().Deletethe user row itself (true hard delete, not the GORM soft-delete column)- Trigger
EventLifecycleService.CleanupUserEventsso orphaned events the user hosted are dropped
HTTP endpoints¶
UserService is not directly exposed over HTTP. Its surface is reached through:
| Method | Path | Auth | Handler → Service path |
|---|---|---|---|
| DELETE | /api/v1/auth/profile |
JWT | AuthHandler.DeleteAccount → AuthService.DeleteAccount → UserService.DeactivateUser |
| POST | /api/v1/auth/profile/avatar |
JWT | AuthHandler.UploadAvatar → AuthService.UpdateAvatar |
| PUT | /api/v1/auth/profile/password |
JWT | AuthHandler.UpdatePassword → AuthService.UpdatePassword |
| PATCH | /api/v1/auth/profile |
JWT | AuthHandler.UpdateProfile → AuthService.UpdateProfile |
| POST | /api/v1/auth/phone |
JWT | AuthHandler.UpdatePhone → AuthService.UpdatePhone |
| POST | /api/v1/auth/email/change + /confirm |
JWT | OTP-driven email change flow on AuthService |
The deactivated_acct_purge cron (24h interval, see internal/scheduler/manager.go) calls PurgeDeactivatedAccounts directly.
Key types¶
type UserService interface {
DeactivateUser(ctx context.Context, userID uuid.UUID) error
ReactivateUser(ctx context.Context, userID uuid.UUID) error
PurgeDeactivatedAccounts(ctx context.Context) error
DeleteUser(ctx context.Context, userID uuid.UUID) error
GetUser(ctx context.Context, userID uuid.UUID) (*models.User, error)
}
type SessionManager interface {
RevokeAllUserTokens(userID uuid.UUID) error
}
Data model¶
DeleteUser writes (deletes) across nearly every table:
users(Unscoped, hard delete)chat_messages,chat_rooms,chat_room_participants(direct vs. group treated differently)friendshipsevent_participants(events themselves cleaned up viaEventLifecycleService.CleanupUserEvents)refresh_tokens,sessions,login_historyweb_authn_credentials,api_keys,audit_logsmoments(rows via FK CASCADE; media files on S3 deleted explicitly first)
Dependencies¶
*gorm.DB— direct SQL for batch deletes / anonymisationUserRepo— minimalFindByIDinterface (kept narrow to avoid full repo coupling)EventLifecycleService—CleanupUserEventsafter deleteSessionManager(typicallySessionService) —RevokeAllUserTokensstorage.FileStorage— delete avatar + moment media files
Notable behavior¶
Two-stage delete
A DELETE /auth/profile request does not hard-delete immediately. It sets deactivated_at and revokes sessions. The actual destruction happens 30 days later in the deactivated_acct_purge worker. Within those 30 days a login reactivates the account transparently (see Auth — finalizeLogin).
Group chat message anonymisation
When a user is hard-deleted, deleteUserChatMessages sets user_id = NULL on their messages in group rooms so conversation history is preserved for the remaining members. Direct (1:1) rooms are deleted wholesale instead because the other participant has nothing to preserve.
S3 cleanup is best-effort
Failures to delete avatar or moment media are logged at WARN but do not block the user-row delete. The orphaned objects can be reclaimed by a separate bucket-lifecycle policy.
Where to look¶
backend/internal/services/user_service.gobackend/internal/services/auth_service.go— profile/password/avatar/phone update methodsbackend/internal/scheduler/manager.go—deactivated_acct_purgecron registrationbackend/internal/scheduler/handlers.go— purge handler invocation