Skip to content

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.DeleteAccountUserService.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 — set deactivated_at = now(), revoke every session/refresh token via SessionManager.RevokeAllUserTokens
  • ReactivateUser — clear deactivated_at (called from AuthService.finalizeLogin when the user logs back in during the grace window)
  • PurgeDeactivatedAccounts — find every user with deactivated_at <= now() - 30d and call DeleteUser
  • DeleteUser — 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().Delete the user row itself (true hard delete, not the GORM soft-delete column)
  • Trigger EventLifecycleService.CleanupUserEvents so 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.DeleteAccountAuthService.DeleteAccountUserService.DeactivateUser
POST /api/v1/auth/profile/avatar JWT AuthHandler.UploadAvatarAuthService.UpdateAvatar
PUT /api/v1/auth/profile/password JWT AuthHandler.UpdatePasswordAuthService.UpdatePassword
PATCH /api/v1/auth/profile JWT AuthHandler.UpdateProfileAuthService.UpdateProfile
POST /api/v1/auth/phone JWT AuthHandler.UpdatePhoneAuthService.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)
  • friendships
  • event_participants (events themselves cleaned up via EventLifecycleService.CleanupUserEvents)
  • refresh_tokens, sessions, login_history
  • web_authn_credentials, api_keys, audit_logs
  • moments (rows via FK CASCADE; media files on S3 deleted explicitly first)

Dependencies

  • *gorm.DB — direct SQL for batch deletes / anonymisation
  • UserRepo — minimal FindByID interface (kept narrow to avoid full repo coupling)
  • EventLifecycleServiceCleanupUserEvents after delete
  • SessionManager (typically SessionService) — RevokeAllUserTokens
  • storage.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 AuthfinalizeLogin).

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