Skip to content

API Reference

User module for user account management and authentication.

This module provides comprehensive user management including account creation, profile updates, authentication, MFA support, email verification, and admin approval workflows.

Exports
  • CRUD: get_all_users, get_users_number, get_users_with_pagination, get_user_by_username, get_user_by_email, get_user_by_id, get_users_admin, create_user, create_signup_user, edit_user, approve_user, verify_user_email, update_user_photo, delete_user
  • Schemas: UsersBase, Users, UsersRead, UsersMe, UsersSignup, UsersCreate, UsersEditPassword, UsersListResponse
  • Models: Users (ORM model)
  • Enums: Gender, Language, WeekDay, UserAccessType
  • Utils: get_user_by_id_or_404, get_admin_users_or_404, check_user_is_active, create_user_default_data, save_user_image_file, delete_user_photo_filesystem

Gender

Bases: Enum

User gender enumeration.

Attributes:

Name Type Description
MALE

Male gender.

FEMALE

Female gender.

UNSPECIFIED

Unspecified or undisclosed gender.

Source code in backend/app/users/users/schema.py
20
21
22
23
24
25
26
27
28
29
30
31
32
class Gender(Enum):
    """
    User gender enumeration.

    Attributes:
        MALE: Male gender.
        FEMALE: Female gender.
        UNSPECIFIED: Unspecified or undisclosed gender.
    """

    MALE = "male"
    FEMALE = "female"
    UNSPECIFIED = "unspecified"

Language

Bases: Enum

Supported application languages.

Attributes:

Name Type Description
CATALAN

Catalan (ca).

CHINESE_SIMPLIFIED

Simplified Chinese (cn).

CHINESE_TRADITIONAL

Traditional Chinese (tw).

GERMAN

German (de).

FRENCH

French (fr).

GALICIAN

Galician (gl).

ITALIAN

Italian (it).

DUTCH

Dutch (nl).

PORTUGUESE

Portuguese (pt).

SLOVENIAN

Slovenian (sl).

SWEDISH

Swedish (sv).

SPANISH

Spanish (es).

ENGLISH_USA

US English (us).

Source code in backend/app/users/users/schema.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Language(Enum):
    """
    Supported application languages.

    Attributes:
        CATALAN: Catalan (ca).
        CHINESE_SIMPLIFIED: Simplified Chinese (cn).
        CHINESE_TRADITIONAL: Traditional Chinese (tw).
        GERMAN: German (de).
        FRENCH: French (fr).
        GALICIAN: Galician (gl).
        ITALIAN: Italian (it).
        DUTCH: Dutch (nl).
        PORTUGUESE: Portuguese (pt).
        SLOVENIAN: Slovenian (sl).
        SWEDISH: Swedish (sv).
        SPANISH: Spanish (es).
        ENGLISH_USA: US English (us).
    """

    CATALAN = "ca"
    CHINESE_SIMPLIFIED = "cn"
    CHINESE_TRADITIONAL = "tw"
    GERMAN = "de"
    FRENCH = "fr"
    GALICIAN = "gl"
    ITALIAN = "it"
    DUTCH = "nl"
    PORTUGUESE = "pt"
    SLOVENIAN = "sl"
    SWEDISH = "sv"
    SPANISH = "es"
    ENGLISH_USA = "us"

UserAccessType

Bases: Enum

User access level enumeration.

Attributes:

Name Type Description
REGULAR

Standard user access.

ADMIN

Administrative access.

Source code in backend/app/users/users/schema.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
class UserAccessType(Enum):
    """
    User access level enumeration.

    Attributes:
        REGULAR: Standard user access.
        ADMIN: Administrative access.
    """

    REGULAR = "regular"
    ADMIN = "admin"

Users

Bases: UsersBase

Complete users schema with administrative fields.

Note

mfa_secret is intentionally NOT part of any API-facing schema. It lives only on the SQLAlchemy model and is accessed directly by MFA verification utilities. Including it in a Pydantic schema would risk leaking the encrypted seed through responses, exports, logs, or future model_dump callers.

Attributes:

Name Type Description
access_type UserAccessType

User access level.

photo_path StrictStr | None

Path to user's photo.

active StrictBool

Whether the user is active.

mfa_enabled StrictBool

Whether MFA is enabled.

email_verified StrictBool

Whether email is verified.

pending_admin_approval StrictBool

Whether pending admin approval.

Source code in backend/app/users/users/schema.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
class Users(UsersBase):
    """
    Complete users schema with administrative fields.

    Note:
        ``mfa_secret`` is intentionally NOT part of any API-facing
        schema. It lives only on the SQLAlchemy model and is
        accessed directly by MFA verification utilities. Including
        it in a Pydantic schema would risk leaking the encrypted
        seed through responses, exports, logs, or future
        ``model_dump`` callers.

    Attributes:
        access_type: User access level.
        photo_path: Path to user's photo.
        active: Whether the user is active.
        mfa_enabled: Whether MFA is enabled.
        email_verified: Whether email is verified.
        pending_admin_approval: Whether pending admin approval.
    """

    access_type: UserAccessType = Field(
        ...,
        description="User access level",
    )
    photo_path: StrictStr | None = Field(
        default=None,
        max_length=250,
        description="Path to user's photo",
    )
    active: StrictBool = Field(
        ...,
        description="Whether the user is active",
    )
    mfa_enabled: StrictBool = Field(
        default=False,
        description="Whether MFA is enabled",
    )
    email_verified: StrictBool = Field(
        default=False,
        description="Whether email is verified",
    )
    pending_admin_approval: StrictBool = Field(
        default=False,
        description="Whether pending admin approval",
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
        validate_assignment=True,
        use_enum_values=True,
    )

UsersBase

Bases: BaseModel

Base users schema with common fields.

Attributes:

Name Type Description
name StrictStr

User's full name (1-250 chars).

username StrictStr

Unique username (1-250 chars, alphanumeric and dots).

email EmailStr

User's email address (max 250 chars).

city StrictStr | None

User's city (max 250 chars).

birthdate date | None

User's birthdate.

preferred_language Language

Preferred language.

gender Gender

User's gender.

units Units

User units (metric, imperial).

height StrictInt | None

User's height in centimeters (1-300).

max_heart_rate StrictInt | None

Maximum heart rate in bpm (30-250).

first_day_of_week WeekDay

First day of the week.

currency Currency

User currency (euro, dollar, pound).

Source code in backend/app/users/users/schema.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class UsersBase(BaseModel):
    """
    Base users schema with common fields.

    Attributes:
        name: User's full name (1-250 chars).
        username: Unique username (1-250 chars, alphanumeric
            and dots).
        email: User's email address (max 250 chars).
        city: User's city (max 250 chars).
        birthdate: User's birthdate.
        preferred_language: Preferred language.
        gender: User's gender.
        units: User units (metric, imperial).
        height: User's height in centimeters (1-300).
        max_heart_rate: Maximum heart rate in bpm (30-250).
        first_day_of_week: First day of the week.
        currency: User currency (euro, dollar, pound).
    """

    name: StrictStr = Field(
        ...,
        min_length=1,
        max_length=250,
        description="User's full name",
    )
    username: StrictStr = Field(
        ...,
        min_length=1,
        max_length=250,
        pattern=r"^[a-zA-Z0-9._-]+$",
        description="Unique username (alphanumeric, dots, hyphen, underscore)",
    )
    email: EmailStr = Field(
        ...,
        max_length=250,
        description="User's email address",
    )
    city: StrictStr | None = Field(
        default=None,
        max_length=250,
        description="User's city",
    )
    birthdate: datetime_date | None = Field(
        default=None,
        description="User's birthdate",
    )
    preferred_language: Language = Field(
        default=Language.ENGLISH_USA,
        description="Preferred language",
    )
    gender: Gender = Field(
        default=Gender.UNSPECIFIED,
        description="User's gender",
    )
    units: server_settings_schema.Units = Field(
        default=server_settings_schema.Units.METRIC,
        description="User units (metric, imperial)",
    )
    height: StrictInt | None = Field(
        default=None,
        ge=1,
        le=300,
        description="Height in centimeters",
    )
    max_heart_rate: StrictInt | None = Field(
        default=None,
        ge=30,
        le=250,
        description="Maximum heart rate in bpm",
    )
    first_day_of_week: WeekDay = Field(
        default=WeekDay.MONDAY,
        description="First day of the week",
    )
    currency: server_settings_schema.Currency = Field(
        default=server_settings_schema.Currency.EURO,
        description="User currency (euro, dollar, pound)",
    )

    model_config = ConfigDict(use_enum_values=True)

    @field_validator("birthdate", mode="before")
    @classmethod
    def validate_birthdate(cls, value: datetime_date | str | None) -> str | None:
        """
        Convert birthdate to ISO format string.

        Args:
            value: Birthdate as date object, string, or None.

        Returns:
            ISO format date string or None.
        """
        if value is None:
            return None
        if isinstance(value, datetime_date):
            return value.isoformat()
        return value

validate_birthdate classmethod

validate_birthdate(value)

Convert birthdate to ISO format string.

Parameters:

Name Type Description Default
value date | str | None

Birthdate as date object, string, or None.

required

Returns:

Type Description
str | None

ISO format date string or None.

Source code in backend/app/users/users/schema.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
@field_validator("birthdate", mode="before")
@classmethod
def validate_birthdate(cls, value: datetime_date | str | None) -> str | None:
    """
    Convert birthdate to ISO format string.

    Args:
        value: Birthdate as date object, string, or None.

    Returns:
        ISO format date string or None.
    """
    if value is None:
        return None
    if isinstance(value, datetime_date):
        return value.isoformat()
    return value

UsersCreate

Bases: Users

Users schema for admin user creation.

Attributes:

Name Type Description
password StrictStr

User's password (min 8 chars).

Source code in backend/app/users/users/schema.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
class UsersCreate(Users):
    """
    Users schema for admin user creation.

    Attributes:
        password: User's password (min 8 chars).
    """

    password: StrictStr = Field(
        ...,
        min_length=8,
        max_length=250,
        description="User's password",
    )

UsersEditPassword

Bases: BaseModel

Schema for password update operations (self-service).

Requires the caller to prove possession of the current password — and an MFA code when MFA is enabled — before the new password is accepted. This prevents a stolen in-memory access token from being parlayed into permanent account takeover.

Attributes:

Name Type Description
current_password StrictStr

Caller's existing password.

password StrictStr

New password (min 8 chars).

mfa_code StrictStr | None

TOTP or backup code, required when MFA is enabled on the account.

Source code in backend/app/users/users/schema.py
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
class UsersEditPassword(BaseModel):
    """
    Schema for password update operations (self-service).

    Requires the caller to prove possession of the current
    password — and an MFA code when MFA is enabled — before
    the new password is accepted. This prevents a stolen
    in-memory access token from being parlayed into permanent
    account takeover.

    Attributes:
        current_password: Caller's existing password.
        password: New password (min 8 chars).
        mfa_code: TOTP or backup code, required when MFA is
            enabled on the account.
    """

    current_password: StrictStr = Field(
        ...,
        min_length=1,
        max_length=250,
        description="Current password (step-up verification)",
    )
    password: StrictStr = Field(
        ...,
        min_length=8,
        max_length=250,
        description="New password",
    )
    mfa_code: StrictStr | None = Field(
        default=None,
        max_length=32,
        description="TOTP or backup code, required when MFA is enabled",
    )
    revoke_other_sessions: StrictBool = Field(
        default=False,
        description=(
            "When true, revoke all of the user's other sessions "
            "(keeping the current one) after the password change. "
            "Use this to evict an attacker when changing a password "
            "because of a suspected compromise."
        ),
    )

    model_config = ConfigDict(
        extra="forbid",
        validate_assignment=True,
    )

UsersListResponse

Bases: BaseModel

Response model for paginated user listing.

Attributes:

Name Type Description
total StrictInt

Total number of user records.

num_records StrictInt | None

Number of records in this response.

page_number StrictInt | None

Current page number.

records list[UsersRead]

List of user records.

Source code in backend/app/users/users/schema.py
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
class UsersListResponse(BaseModel):
    """
    Response model for paginated user listing.

    Attributes:
        total: Total number of user records.
        num_records: Number of records in this response.
        page_number: Current page number.
        records: List of user records.
    """

    total: StrictInt = Field(
        ...,
        ge=0,
        description="Total number of user records",
    )
    num_records: StrictInt | None = Field(
        default=None,
        ge=0,
        description="Number of records in this response",
    )
    page_number: StrictInt | None = Field(
        default=None,
        ge=1,
        description="Current page number",
    )
    records: list[UsersRead] = Field(
        ...,
        description="List of user records",
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
        validate_assignment=True,
    )

UsersMe

Bases: UsersRead

Extended users schema for current user profile.

Includes privacy settings and integration status.

Attributes:

Name Type Description
is_strava_linked StrictInt | None

Strava integration status.

is_garminconnect_linked StrictInt | None

Garmin Connect status.

default_activity_visibility StrictStr | None

Default visibility level.

hide_activity_start_time StrictBool | None

Hide start time setting.

hide_activity_location StrictBool | None

Hide location setting.

hide_activity_map StrictBool | None

Hide map setting.

hide_activity_hr StrictBool | None

Hide heart rate setting.

hide_activity_power StrictBool | None

Hide power setting.

hide_activity_cadence StrictBool | None

Hide cadence setting.

hide_activity_elevation StrictBool | None

Hide elevation setting.

hide_activity_speed StrictBool | None

Hide speed setting.

hide_activity_pace StrictBool | None

Hide pace setting.

hide_activity_laps StrictBool | None

Hide laps setting.

hide_activity_workout_sets_steps StrictBool | None

Hide workout sets/steps.

hide_activity_gear StrictBool | None

Hide gear setting.

Source code in backend/app/users/users/schema.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
class UsersMe(UsersRead):
    """
    Extended users schema for current user profile.

    Includes privacy settings and integration status.

    Attributes:
        is_strava_linked: Strava integration status.
        is_garminconnect_linked: Garmin Connect status.
        default_activity_visibility: Default visibility level.
        hide_activity_start_time: Hide start time setting.
        hide_activity_location: Hide location setting.
        hide_activity_map: Hide map setting.
        hide_activity_hr: Hide heart rate setting.
        hide_activity_power: Hide power setting.
        hide_activity_cadence: Hide cadence setting.
        hide_activity_elevation: Hide elevation setting.
        hide_activity_speed: Hide speed setting.
        hide_activity_pace: Hide pace setting.
        hide_activity_laps: Hide laps setting.
        hide_activity_workout_sets_steps: Hide workout
            sets/steps.
        hide_activity_gear: Hide gear setting.
    """

    is_strava_linked: StrictInt | None = Field(default=None, description="Whether Strava is linked")
    is_garminconnect_linked: StrictInt | None = Field(default=None, description="Whether Garmin Connect is linked")
    default_activity_visibility: StrictStr | None = Field(default=None, description="Default activity visibility")
    hide_activity_start_time: StrictBool | None = Field(default=None, description="Hide activity start time")
    hide_activity_location: StrictBool | None = Field(default=None, description="Hide activity location")
    hide_activity_map: StrictBool | None = Field(default=None, description="Hide activity map")
    hide_activity_hr: StrictBool | None = Field(default=None, description="Hide activity heart rate")
    hide_activity_power: StrictBool | None = Field(default=None, description="Hide activity power")
    hide_activity_cadence: StrictBool | None = Field(default=None, description="Hide activity cadence")
    hide_activity_elevation: StrictBool | None = Field(default=None, description="Hide activity elevation")
    hide_activity_speed: StrictBool | None = Field(default=None, description="Hide activity speed")
    hide_activity_pace: StrictBool | None = Field(default=None, description="Hide activity pace")
    hide_activity_laps: StrictBool | None = Field(default=None, description="Hide activity laps")
    hide_activity_workout_sets_steps: StrictBool | None = Field(
        default=None, description="Hide activity workout sets and steps"
    )
    hide_activity_gear: StrictBool | None = Field(default=None, description="Hide activity gear")
    has_local_password: StrictBool | None = Field(
        default=None,
        description=(
            "Whether the account has a local password set. False"
            " indicates an SSO-only account, in which case"
            " step-up flows must skip the password factor. The"
            " raw password hash is never exposed; only this"
            " derived boolean is returned."
        ),
    )

UsersModel

Bases: Base

User account and profile information.

Attributes:

Name Type Description
id Mapped[int]

Primary key.

name Mapped[str]

User's real name (may include spaces).

username Mapped[str]

Unique username (letters, numbers, dots).

email Mapped[str]

Unique email address (max 250 characters).

city Mapped[str | None]

User's city.

birthdate Mapped[date | None]

User's birthdate.

preferred_language Mapped[str]

Preferred language code.

gender Mapped[str]

User's gender (male, female, unspecified).

units Mapped[str]

Measurement units (metric, imperial).

height Mapped[int | None]

User's height in centimeters.

max_heart_rate Mapped[int | None]

User maximum heart rate (bpm).

access_type Mapped[str]

User type (regular, admin).

photo_path Mapped[str | None]

Path to user's photo.

active Mapped[bool]

Whether the user is active.

first_day_of_week Mapped[str]

First day of the week (sunday, monday, etc.).

currency Mapped[str]

Currency preference (euro, dollar, pound).

email_verified Mapped[bool]

Whether the user's email address has been verified.

pending_admin_approval Mapped[bool]

Whether the user is pending admin approval for activation.

users_sessions Mapped[list[UsersSessions]]

List of session objects.

password_reset_tokens Mapped[list[PasswordResetToken]]

List of password reset tokens.

sign_up_tokens Mapped[list[SignUpToken]]

List of sign-up tokens.

users_integrations Mapped[list[UsersIntegrations]]

List of integrations.

users_default_gear Mapped[list[UsersDefaultGear]]

List of default gear.

users_privacy_settings Mapped[list[UsersPrivacySettings]]

List of privacy settings.

gear Mapped[list[Gear]]

List of gear owned by the user.

gear_components Mapped[list[GearComponents]]

List of gear components.

activities Mapped[list[Activity]]

List of activities performed.

followers Mapped[list[Follower]]

List of Follower objects representing users who follow this user.

following Mapped[list[Follower]]

List of Follower objects representing users this user is following.

health_sleep Mapped[list[HealthSleep]]

List of health sleep records.

health_weight Mapped[list[HealthWeight]]

List of health weight records.

health_steps Mapped[list[HealthSteps]]

List of health steps records.

health_targets Mapped[list[HealthTargets]]

List of health targets.

health_fasting Mapped[list[HealthFasting]]

List of health fasting records.

health_water Mapped[list[HealthWater]]

List of health water intake records.

health_poop Mapped[list[HealthPoop]]

List of health poop records.

notifications Mapped[list[Notification]]

List of notifications.

goals Mapped[list[UsersGoal]]

List of user goals.

user_identity_providers Mapped[list[IdentityLink]]

List of identity providers linked to the user.

oauth_states Mapped[list[OAuthState]]

List of OAuth states for the user.

mfa_backup_codes Mapped[list[MFABackupCode]]

List of MFA backup codes.

auth_mfa Mapped[UsersMFA]

1:1 MFA state row in users_mfa

idp_link_tokens Mapped[list[IdpLinkToken]]

List of short-lived IdP link tokens for the user.

local_credential Mapped[LocalCredential | None]

1:1 local password credential row in users_local_credentials (None for SSO-only accounts).

mfa_enabled bool

Computed property — True when auth_mfa.mfa_enabled is set.

Source code in backend/app/users/users/models.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
class Users(Base):
    """
    User account and profile information.

    Attributes:
        id: Primary key.
        name: User's real name (may include spaces).
        username: Unique username (letters, numbers, dots).
        email: Unique email address (max 250 characters).
        city: User's city.
        birthdate: User's birthdate.
        preferred_language: Preferred language code.
        gender: User's gender (male, female, unspecified).
        units: Measurement units (metric, imperial).
        height: User's height in centimeters.
        max_heart_rate: User maximum heart rate (bpm).
        access_type: User type (regular, admin).
        photo_path: Path to user's photo.
        active: Whether the user is active.
        first_day_of_week: First day of the week
            (sunday, monday, etc.).
        currency: Currency preference (euro, dollar, pound).
        email_verified: Whether the user's email address has
            been verified.
        pending_admin_approval: Whether the user is pending
            admin approval for activation.
        users_sessions: List of session objects.
        password_reset_tokens: List of password reset tokens.
        sign_up_tokens: List of sign-up tokens.
        users_integrations: List of integrations.
        users_default_gear: List of default gear.
        users_privacy_settings: List of privacy settings.
        gear: List of gear owned by the user.
        gear_components: List of gear components.
        activities: List of activities performed.
        followers: List of Follower objects representing users
            who follow this user.
        following: List of Follower objects representing users
            this user is following.
        health_sleep: List of health sleep records.
        health_weight: List of health weight records.
        health_steps: List of health steps records.
        health_targets: List of health targets.
        health_fasting: List of health fasting records.
        health_water: List of health water intake records.
        health_poop: List of health poop records.
        notifications: List of notifications.
        goals: List of user goals.
        user_identity_providers: List of identity providers
            linked to the user.
        oauth_states: List of OAuth states for the user.
        mfa_backup_codes: List of MFA backup codes.
        auth_mfa: 1:1 MFA state row in ``users_mfa``
        idp_link_tokens: List of short-lived IdP link tokens for the user.
        local_credential: 1:1 local password credential row in
            ``users_local_credentials`` (``None`` for SSO-only accounts).
        mfa_enabled: Computed property — ``True`` when
            ``auth_mfa.mfa_enabled`` is set.
    """

    __tablename__ = "users"

    id: Mapped[int] = mapped_column(
        primary_key=True,
        autoincrement=True,
    )
    name: Mapped[str] = mapped_column(
        String(250),
        nullable=False,
        comment="User real name (May include spaces)",
    )
    username: Mapped[str] = mapped_column(
        String(250),
        nullable=False,
        unique=True,
        index=True,
        comment="User username (letters, numbers, and dots allowed)",
    )
    email: Mapped[str] = mapped_column(
        String(250),
        nullable=False,
        unique=True,
        index=True,
        comment="User email (max 250 characters)",
    )
    city: Mapped[str | None] = mapped_column(
        String(250),
        nullable=True,
        comment="User city",
    )
    birthdate: Mapped[date_type | None] = mapped_column(
        nullable=True,
        comment="User birthdate (date)",
    )
    preferred_language: Mapped[str] = mapped_column(
        String(5),
        nullable=False,
        comment="User preferred language (en, pt, others)",
    )
    gender: Mapped[str] = mapped_column(
        String(20),
        default="male",
        nullable=False,
        comment="User gender (male, female, unspecified)",
    )
    units: Mapped[str] = mapped_column(
        String(20),
        default="metric",
        nullable=False,
        comment="User units (metric, imperial)",
    )
    height: Mapped[int | None] = mapped_column(
        nullable=True,
        comment="User height in centimeters",
    )
    max_heart_rate: Mapped[int | None] = mapped_column(
        nullable=True,
        comment="User maximum heart rate (bpm)",
    )
    access_type: Mapped[str] = mapped_column(
        String(20),
        nullable=False,
        comment="User type (regular, admin)",
    )
    photo_path: Mapped[str | None] = mapped_column(
        String(250),
        nullable=True,
        comment="User photo path",
    )
    active: Mapped[bool] = mapped_column(
        default=True,
        nullable=False,
        comment="Whether the user is active (true - yes, false - no)",
    )
    first_day_of_week: Mapped[str] = mapped_column(
        String(20),
        default="monday",
        nullable=False,
        comment="User first day of week (sunday, monday, etc.)",
    )
    currency: Mapped[str] = mapped_column(
        String(20),
        default="euro",
        nullable=False,
        comment="User currency (euro, dollar, pound)",
    )
    email_verified: Mapped[bool] = mapped_column(
        default=False,
        nullable=False,
        comment=("Whether the user's email address has been verified (true - yes, false - no)"),
    )
    pending_admin_approval: Mapped[bool] = mapped_column(
        default=False,
        nullable=False,
        comment=("Whether the user is pending admin approval for activation (true - yes, false - no)"),
    )

    # Relationships
    users_sessions: Mapped[list["UsersSessions"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    password_reset_tokens: Mapped[list["PasswordResetToken"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    sign_up_tokens: Mapped[list["SignUpToken"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    users_integrations: Mapped[list["UsersIntegrations"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    users_default_gear: Mapped[list["UsersDefaultGear"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    users_privacy_settings: Mapped[list["UsersPrivacySettings"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    gear: Mapped[list["Gear"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    gear_components: Mapped[list["GearComponents"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    activities: Mapped[list["Activity"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    followers: Mapped[list["Follower"]] = relationship(
        back_populates="following",
        cascade="all, delete-orphan",
        foreign_keys="Follower.following_id",
    )
    following: Mapped[list["Follower"]] = relationship(
        back_populates="follower",
        cascade="all, delete-orphan",
        foreign_keys="Follower.follower_id",
    )
    health_sleep: Mapped[list["HealthSleep"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_weight: Mapped[list["HealthWeight"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_steps: Mapped[list["HealthSteps"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_targets: Mapped[list["HealthTargets"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_fasting: Mapped[list["HealthFasting"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_water: Mapped[list["HealthWater"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    health_poop: Mapped[list["HealthPoop"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    notifications: Mapped[list["Notification"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    goals: Mapped[list["UsersGoal"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    user_identity_providers: Mapped[list["IdentityLink"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    oauth_states: Mapped[list["OAuthState"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    mfa_backup_codes: Mapped[list["MFABackupCode"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    auth_mfa: Mapped["UsersMFA"] = relationship(
        back_populates="users",
        uselist=False,
        cascade="all, delete-orphan",
    )
    users_api_keys: Mapped[list["UsersApiKeys"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    idp_link_tokens: Mapped[list["IdpLinkToken"]] = relationship(
        back_populates="users",
        cascade="all, delete-orphan",
    )
    local_credential: Mapped["LocalCredential | None"] = relationship(
        back_populates="users",
        uselist=False,
        cascade="all, delete-orphan",
    )

    @property
    def mfa_enabled(self) -> bool:
        """
        Return whether MFA is active for this user.

        Used by Pydantic schemas (``from_attributes=True``) and
        any caller that checks MFA status on the profile row.
        """
        return bool(self.auth_mfa and self.auth_mfa.mfa_enabled)

    @property
    def has_local_password(self) -> bool:
        """
        Return whether this user has a local (non-SSO) password set.

        A user has a local password when a row exists in the auth-owned
        ``users_local_credentials`` table. SSO-only accounts have no row.
        """
        return self.local_credential is not None

has_local_password property

has_local_password

Return whether this user has a local (non-SSO) password set.

A user has a local password when a row exists in the auth-owned users_local_credentials table. SSO-only accounts have no row.

mfa_enabled property

mfa_enabled

Return whether MFA is active for this user.

Used by Pydantic schemas (from_attributes=True) and any caller that checks MFA status on the profile row.

UsersRead

Bases: Users

Users schema for read operations.

Attributes:

Name Type Description
id StrictInt

User ID.

external_auth_count StrictInt

Number of external auth providers.

Source code in backend/app/users/users/schema.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class UsersRead(Users):
    """
    Users schema for read operations.

    Attributes:
        id: User ID.
        external_auth_count: Number of external auth providers.
    """

    id: StrictInt = Field(
        ...,
        ge=1,
        description="User ID",
    )
    external_auth_count: StrictInt = Field(
        default=0,
        ge=0,
        description="Number of external auth providers linked",
    )

UsersSignup

Bases: UsersBase

Users schema for signup operations.

Attributes:

Name Type Description
password StrictStr

User's password (min 8 chars).

Source code in backend/app/users/users/schema.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
class UsersSignup(UsersBase):
    """
    Users schema for signup operations.

    Attributes:
        password: User's password (min 8 chars).
    """

    password: StrictStr = Field(
        ...,
        min_length=8,
        max_length=250,
        description="User's password",
    )

WeekDay

Bases: Enum

Days of the week enumeration.

Attributes:

Name Type Description
SUNDAY

Sunday.

MONDAY

Monday.

TUESDAY

Tuesday.

WEDNESDAY

Wednesday.

THURSDAY

Thursday.

FRIDAY

Friday.

SATURDAY

Saturday.

Source code in backend/app/users/users/schema.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class WeekDay(Enum):
    """
    Days of the week enumeration.

    Attributes:
        SUNDAY: Sunday.
        MONDAY: Monday.
        TUESDAY: Tuesday.
        WEDNESDAY: Wednesday.
        THURSDAY: Thursday.
        FRIDAY: Friday.
        SATURDAY: Saturday.
    """

    SUNDAY = "sunday"
    MONDAY = "monday"
    TUESDAY = "tuesday"
    WEDNESDAY = "wednesday"
    THURSDAY = "thursday"
    FRIDAY = "friday"
    SATURDAY = "saturday"

approve_user

approve_user(user_id, db)

Approve a user by marking them as active.

Parameters:

Name Type Description Default
user_id int

ID of user to approve.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
None

None

Raises:

Type Description
HTTPException

404 if user not found.

HTTPException

400 if user email not verified.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
@core_decorators.handle_db_errors
def approve_user(user_id: int, db: Session) -> None:
    """
    Approve a user by marking them as active.

    Args:
        user_id: ID of user to approve.
        db: SQLAlchemy database session.

    Returns:
        None

    Raises:
        HTTPException: 404 if user not found.
        HTTPException: 400 if user email not verified.
        HTTPException: 500 if database error occurs.
    """
    # Get the user from the database
    db_user = users_utils.get_user_by_id_or_404(user_id, db)

    if not db_user.email_verified:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="User email is not verified",
        )

    db_user.pending_admin_approval = False
    db_user.active = True

    # Commit the transaction
    db.commit()
    db.refresh(db_user)

check_user_is_active

check_user_is_active(user)

Check if user is active and raise 403 if inactive.

Parameters:

Name Type Description Default
user Users | UsersRead

User object to check (User or UsersRead schema).

required

Returns:

Type Description
None

None

Raises:

Type Description
HTTPException

403 if user is not active.

Source code in backend/app/users/users/utils.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def check_user_is_active(
    user: users_models.Users | users_schema.UsersRead,
) -> None:
    """
    Check if user is active and raise 403 if inactive.

    Args:
        user: User object to check (User or UsersRead schema).

    Returns:
        None

    Raises:
        HTTPException: 403 if user is not active.
    """
    if not user.active:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Inactive user",
            headers={"WWW-Authenticate": "Bearer"},
        )

create_signup_user

create_signup_user(user, server_settings, identity_service, db, persist_credential=True)

Create a new user during signup process.

Parameters:

Name Type Description Default
user UsersSignup

User signup data.

required
server_settings ServerSettingsRead

Server config for signup requirements.

required
identity_service IdentityService

Identity service dependency.

required
db Session

SQLAlchemy database session.

required
persist_credential bool

When True (default), validate the supplied password and store its hash in the auth-owned credential table. SSO-created accounts pass False so they get no local credential row and remain SSO-only (has_local_password then correctly reports False).

True

Returns:

Type Description
Users

Created Users model.

Raises:

Type Description
HTTPException

409 if email/username already exists. Abstract message to reduce information leakage.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
@core_decorators.handle_db_errors
def create_signup_user(
    user: users_schema.UsersSignup,
    server_settings: server_settings_schema.ServerSettingsRead,
    identity_service: "auth_identity_service.IdentityService",
    db: Session,
    persist_credential: bool = True,
) -> users_models.Users:
    """
    Create a new user during signup process.

    Args:
        user: User signup data.
        server_settings: Server config for signup requirements.
        identity_service: Identity service dependency.
        db: SQLAlchemy database session.
        persist_credential: When ``True`` (default), validate the supplied
            password and store its hash in the auth-owned credential table.
            SSO-created accounts pass ``False`` so they get no local
            credential row and remain SSO-only (``has_local_password`` then
            correctly reports ``False``).

    Returns:
        Created Users model.

    Raises:
        HTTPException: 409 if email/username already exists. Abstract message
            to reduce information leakage.
        HTTPException: 500 if database error occurs.
    """
    try:
        # Determine user status based on server settings
        active = True
        email_verified = False
        pending_admin_approval = False

        if server_settings.signup_require_email_verification:
            email_verified = False
            active = False  # Inactive until email verified

        if server_settings.signup_require_admin_approval:
            pending_admin_approval = True
            active = False  # Inactive until approved

        # If both email verification and admin approval are disabled, user is immediately active
        if not server_settings.signup_require_email_verification and not server_settings.signup_require_admin_approval:
            active = True
            email_verified = True

        # Create a new user
        db_user = users_models.Users(
            **user.model_dump(
                exclude={
                    "username",
                    "email",
                    "access_type",
                    "active",
                    "email_verified",
                    "pending_admin_approval",
                    "password",
                }
            ),
            username=user.username.lower(),
            email=user.email.lower(),
            access_type=users_schema.UserAccessType.REGULAR.value,
            active=active,
            email_verified=email_verified,
            pending_admin_approval=pending_admin_approval,
        )

        # Hash the signup password with the configured policy. SSO-created
        # accounts opt out (persist_credential=False) so the supplied
        # placeholder password is never validated or hashed.
        hashed_password: str | None = None
        if persist_credential:
            hashed_password = auth_password_policy.validate_and_hash_for_user(
                identity_service,
                server_settings,
                users_schema.UserAccessType.REGULAR.value,
                user.password,
            )

        # Add the user to the database
        db.add(db_user)
        db.commit()
        db.refresh(db_user)

        # Persist the password hash in the auth-owned credential table. Skipped
        # for SSO-only accounts so that ``has_local_password`` stays a true row
        # existence check.
        if hashed_password is not None:
            identity_service.set_local_password_hash(db_user.id, hashed_password)

        # Return user
        return db_user
    except IntegrityError as integrity_error:
        # Rollback the transaction
        db.rollback()

        # Raise an HTTPException with a 409 Conflict status code
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=("Unable to create user."),
        ) from integrity_error

create_user

create_user(user, identity_service, db)

Create a new user with hashed password.

Parameters:

Name Type Description Default
user UsersCreate

User creation data with plain text password.

required
identity_service IdentityService

Identity service dependency.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
Users

Created Users model with hashed password.

Raises:

Type Description
HTTPException

409 if email/username already exists.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
@core_decorators.handle_db_errors
def create_user(
    user: users_schema.UsersCreate,
    identity_service: "auth_identity_service.IdentityService",
    db: Session,
) -> users_models.Users:
    """
    Create a new user with hashed password.

    Args:
        user: User creation data with plain text password.
        identity_service: Identity service dependency.
        db: SQLAlchemy database session.

    Returns:
        Created Users model with hashed password.

    Raises:
        HTTPException: 409 if email/username already exists.
        HTTPException: 500 if database error occurs.
    """
    try:
        user.username = user.username.lower()
        user.email = user.email.lower()

        # Get server settings to determine password policy
        server_settings = server_settings_utils.get_server_settings_or_404(db)

        # Normalize access_type to string value
        access_type_value = users_schema.normalize_access_type(user.access_type)

        # Hash the password with configurable policy and length
        hashed_password = auth_password_policy.validate_and_hash_for_user(
            identity_service,
            server_settings,
            access_type_value,
            user.password,
        )

        # Create a new user
        db_user = users_models.Users(
            **user.model_dump(exclude={"password", "access_type"}),
            access_type=access_type_value,
        )

        # Add the user to the database
        db.add(db_user)
        db.commit()
        db.refresh(db_user)

        # Persist the password hash in the auth-owned credential table.
        identity_service.set_local_password_hash(db_user.id, hashed_password)

        # Return user
        return db_user
    except HTTPException:
        # Rollback the transaction
        db.rollback()
        raise
    except IntegrityError as integrity_error:
        # Rollback the transaction
        db.rollback()

        # Raise an HTTPException with a 409 Conflict status code
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=("Duplicate entry error. Check if email and username are unique"),
        ) from integrity_error

create_user_default_data

create_user_default_data(user_id, identity_service, db)

Create default data for newly created user.

Parameters:

Name Type Description Default
user_id int

ID of user to create default data for.

required
identity_service IdentityService

Identity service used to initialise the auth-owned MFA row through the auth boundary.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
None

None

Source code in backend/app/users/users/utils.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def create_user_default_data(
    user_id: int,
    identity_service: "auth_identity_service.IdentityService",
    db: Session,
) -> None:
    """
    Create default data for newly created user.

    Args:
        user_id: ID of user to create default data for.
        identity_service: Identity service used to initialise the
            auth-owned MFA row through the auth boundary.
        db: SQLAlchemy database session.

    Returns:
        None
    """
    # Create the user integrations in the database
    user_integrations_crud.create_user_integrations(user_id, db)

    # Create the user privacy settings
    users_privacy_settings_crud.create_user_privacy_settings(user_id, db)

    # Create the user health targets
    health_targets_crud.create_health_targets(user_id, db)

    # Create the user default gear
    user_default_gear_crud.create_user_default_gear(user_id, db)

    # Create the user's MFA row (disabled by default) via the auth boundary.
    identity_service.initialize_user_mfa(user_id)

delete_user async

delete_user(user_id, db)

Delete a user from the database.

Parameters:

Name Type Description Default
user_id int

ID of user to delete.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
None

None

Raises:

Type Description
HTTPException

404 if user not found.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
@core_decorators.handle_db_errors
async def delete_user(user_id: int, db: Session) -> None:
    """
    Delete a user from the database.

    Args:
        user_id: ID of user to delete.
        db: SQLAlchemy database session.

    Returns:
        None

    Raises:
        HTTPException: 404 if user not found.
        HTTPException: 500 if database error occurs.
    """
    # Get the user from the database
    db_user = users_utils.get_user_by_id_or_404(user_id, db)

    # Delete the user
    db.delete(db_user)

    # Commit the transaction
    db.commit()

    # Delete the user photo in the filesystem
    await users_utils.delete_user_photo_filesystem(user_id)

delete_user_photo_filesystem async

delete_user_photo_filesystem(user_id)

Delete user photo files from filesystem.

Parameters:

Name Type Description Default
user_id int

ID of user whose photo files to delete.

required

Returns:

Type Description
None

None

Source code in backend/app/users/users/utils.py
181
182
183
184
185
186
187
188
189
190
191
async def delete_user_photo_filesystem(user_id: int) -> None:
    """
    Delete user photo files from filesystem.

    Args:
        user_id: ID of user whose photo files to delete.

    Returns:
        None
    """
    await core_file_uploads.delete_files_by_pattern(core_config.USER_IMAGES_DIR, f"{user_id}.*")

edit_user async

edit_user(user_id, user, db)

Update an existing user's information.

Note

This dynamic-assignment helper is intended for admin endpoints that legitimately need to set fields like access_type, active, or pending_admin_approval. Self-service profile updates MUST NOT call this — use :func:edit_profile_user instead, which enforces an explicit allow-list and prevents privilege escalation through mass assignment.

Parameters:

Name Type Description Default
user_id int

ID of user to update.

required
user UsersRead

User data to update with.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
Users

users_models.Users

Raises:

Type Description
HTTPException

404 if user not found.

HTTPException

409 if email/username conflict.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
@core_decorators.handle_db_errors
async def edit_user(user_id: int, user: users_schema.UsersRead, db: Session) -> users_models.Users:
    """
    Update an existing user's information.

    Note:
        This dynamic-assignment helper is intended for **admin**
        endpoints that legitimately need to set fields like
        ``access_type``, ``active``, or ``pending_admin_approval``.
        Self-service profile updates MUST NOT call this — use
        :func:`edit_profile_user` instead, which enforces an
        explicit allow-list and prevents privilege escalation
        through mass assignment.

    Args:
        user_id: ID of user to update.
        user: User data to update with.
        db: SQLAlchemy database session.

    Returns:
        users_models.Users

    Raises:
        HTTPException: 404 if user not found.
        HTTPException: 409 if email/username conflict.
        HTTPException: 500 if database error occurs.
    """
    try:
        # Get the user from the database
        db_user = users_utils.get_user_by_id_or_404(user_id, db)

        height_before = db_user.height

        # Check if the photo_path is being updated
        if user.photo_path:
            # Delete the user photo in the filesystem
            await users_utils.delete_user_photo_filesystem(db_user.id)

        user.username = user.username.lower()

        # Dictionary of the fields to update if they are not None
        user_data = user.model_dump(exclude_unset=True)
        # Iterate over the fields and update the db_user dynamically
        for key, value in user_data.items():
            setattr(db_user, key, value)

        # Commit the transaction
        db.commit()
        db.refresh(db_user)

        if height_before != db_user.height:
            # Update the user's health data
            health_weight_utils.calculate_bmi_all_user_entries(db_user.id, db)

        if db_user.photo_path is None:
            # Delete the user photo in the filesystem
            await users_utils.delete_user_photo_filesystem(db_user.id)

        return db_user
    except HTTPException:
        raise
    except IntegrityError as integrity_error:
        # Rollback the transaction
        db.rollback()

        # Raise an HTTPException with a 409 Conflict status code
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=("Duplicate entry error. Check if email and username are unique"),
        ) from integrity_error

get_admin_users_or_404

get_admin_users_or_404(db)

Retrieve all admin users from database or raise 404 error.

Parameters:

Name Type Description Default
db Session

SQLAlchemy database session.

required

Returns:

Type Description
list[Users]

List of all admin User models.

Raises:

Type Description
HTTPException

404 if no admin users found.

Source code in backend/app/users/users/utils.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def get_admin_users_or_404(db: Session) -> list[users_models.Users]:
    """
    Retrieve all admin users from database or raise 404 error.

    Args:
        db: SQLAlchemy database session.

    Returns:
        List of all admin User models.

    Raises:
        HTTPException: 404 if no admin users found.
    """
    admins = users_crud.get_users_admin(db)

    if not admins:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="No admin users found",
        )

    return admins

get_all_users

get_all_users(db)

Retrieve all users from the database.

Parameters:

Name Type Description Default
db Session

SQLAlchemy database session.

required

Returns:

Type Description
list[Users]

List of all User models.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@core_decorators.handle_db_errors
def get_all_users(db: Session) -> list[users_models.Users]:
    """
    Retrieve all users from the database.

    Args:
        db: SQLAlchemy database session.

    Returns:
        List of all User models.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    stmt = select(users_models.Users)
    return list(db.execute(stmt).scalars().all())

get_user_by_email

get_user_by_email(email, db)

Retrieve user by email address.

Parameters:

Name Type Description Default
email str

Email address to search for (case-insensitive).

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
Users | None

Users model if found, None otherwise.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
@core_decorators.handle_db_errors
def get_user_by_email(email: str, db: Session) -> users_models.Users | None:
    """
    Retrieve user by email address.

    Args:
        email: Email address to search for (case-insensitive).
        db: SQLAlchemy database session.

    Returns:
        Users model if found, None otherwise.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    stmt = select(users_models.Users).where(users_models.Users.email == email.lower())
    return db.execute(stmt).scalar_one_or_none()

get_user_by_id

get_user_by_id(user_id, db, public_check=False)

Retrieve user by ID.

Parameters:

Name Type Description Default
user_id int

User ID to search for.

required
db Session

SQLAlchemy database session.

required
public_check bool

If True, only returns user when public sharing is enabled in server settings.

False

Returns:

Type Description
Users | None

Users model if found (and public sharing enabled if

Users | None

public_check=True), None otherwise.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
@core_decorators.handle_db_errors
def get_user_by_id(user_id: int, db: Session, public_check: bool = False) -> users_models.Users | None:
    """
    Retrieve user by ID.

    Args:
        user_id: User ID to search for.
        db: SQLAlchemy database session.
        public_check: If True, only returns user when public sharing
                      is enabled in server settings.

    Returns:
        Users model if found (and public sharing enabled if
        public_check=True), None otherwise.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    if public_check:
        # Check if public sharable links are enabled in server settings
        server_settings = server_settings_utils.get_server_settings_or_404(db)

        # Return None if public sharable links are disabled
        if not server_settings.public_shareable_links or not server_settings.public_shareable_links_user_info:
            return None

    stmt = select(users_models.Users).where(users_models.Users.id == user_id)
    return db.execute(stmt).scalar_one_or_none()

get_user_by_id_or_404

get_user_by_id_or_404(user_id, db)

Retrieve user by ID or raise 404 error.

Parameters:

Name Type Description Default
user_id int

User ID to search for.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
Users

Users model (guaranteed non-None).

Raises:

Type Description
HTTPException

404 if user not found.

Source code in backend/app/users/users/utils.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def get_user_by_id_or_404(user_id: int, db: Session) -> users_models.Users:
    """
    Retrieve user by ID or raise 404 error.

    Args:
        user_id: User ID to search for.
        db: SQLAlchemy database session.

    Returns:
        Users model (guaranteed non-None).

    Raises:
        HTTPException: 404 if user not found.
    """
    # Get the user from the database
    db_user = users_crud.get_user_by_id(user_id, db)

    if db_user is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found",
            headers={"WWW-Authenticate": "Bearer"},
        )

    return db_user

get_user_by_username

get_user_by_username(username, db, contains=False)

Retrieve user by username.

Parameters:

Name Type Description Default
username str

Username to search for.

required
db Session

SQLAlchemy database session.

required
contains bool

If True, performs partial match search and returns list of matching users. If False, performs exact match and returns single user or None.

False

Returns:

Type Description
list[Users] | Users | None

If contains=False: Users model if found, None otherwise.

list[Users] | Users | None

If contains=True: List of User models matching the search.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@core_decorators.handle_db_errors
def get_user_by_username(
    username: str, db: Session, contains: bool = False
) -> list[users_models.Users] | users_models.Users | None:
    """
    Retrieve user by username.

    Args:
        username: Username to search for.
        db: SQLAlchemy database session.
        contains: If True, performs partial match search and returns
                  list of matching users. If False, performs exact
                  match and returns single user or None.

    Returns:
        If contains=False: Users model if found, None otherwise.
        If contains=True: List of User models matching the search.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    # Decode and normalize search term (needed for both exact and partial matches)
    normalized_username = unquote(username).replace("+", " ").lower()

    if contains:
        # Escape LIKE special characters to prevent SQL injection
        escaped_username = normalized_username.replace("\\", "\\\\").replace("%", r"\%").replace("_", r"\_")

        # Query users with username containing the search term
        stmt = select(users_models.Users).where(
            func.lower(users_models.Users.username).like(f"%{escaped_username}%", escape="\\")
        )
        return list(db.execute(stmt).scalars().all())
    else:
        # Exact match - no LIKE escaping needed
        stmt = select(users_models.Users).where(users_models.Users.username == normalized_username)
        return db.execute(stmt).scalar_one_or_none()

get_users_admin

get_users_admin(db)

Retrieve all admin users from the database.

Parameters:

Name Type Description Default
db Session

SQLAlchemy database session.

required

Returns:

Type Description
list[Users]

List of User models with admin access.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
@core_decorators.handle_db_errors
def get_users_admin(db: Session) -> list[users_models.Users]:
    """
    Retrieve all admin users from the database.

    Args:
        db: SQLAlchemy database session.

    Returns:
        List of User models with admin access.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    stmt = select(users_models.Users).where(users_models.Users.access_type == users_schema.UserAccessType.ADMIN.value)
    return list(db.execute(stmt).scalars().all())

get_users_number

get_users_number(db)

Get total count of users in the database.

Parameters:

Name Type Description Default
db Session

SQLAlchemy database session.

required

Returns:

Type Description
int

Total number of users.

Raises:

Type Description
HTTPException

500 error if database query fails.

Source code in backend/app/users/users/crud.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@core_decorators.handle_db_errors
def get_users_number(db: Session) -> int:
    """
    Get total count of users in the database.

    Args:
        db: SQLAlchemy database session.

    Returns:
        Total number of users.

    Raises:
        HTTPException: 500 error if database query fails.
    """
    stmt = select(func.count(users_models.Users.id))
    return db.execute(stmt).scalar_one()

get_users_with_pagination

get_users_with_pagination(db, page_number=None, num_records=None, show_inactive=True, show_email_unverified=True, show_pending_approval=True)

Retrieve a paginated list of users with optional filtering.

Parameters:

Name Type Description Default
db Session

Database session for executing queries.

required
page_number int | None

The page number for pagination (1-indexed). If None, pagination is not applied. Defaults to None.

None
num_records int | None

The number of records per page. If None, pagination is not applied. Defaults to None.

None
show_inactive bool | None

If False, excludes inactive users. Defaults to True (includes inactive users).

True
show_email_unverified bool | None

If False, excludes users with unverified emails. Defaults to True (includes email unverified users).

True
show_pending_approval bool | None

If False, excludes users pending admin approval. Defaults to True (includes pending approval users).

True

Returns:

Type Description
list[Users]

list[users_models.Users]: A list of User objects matching the specified criteria, ordered by username. Returns an empty list if no users match the filters.

Source code in backend/app/users/users/crud.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@core_decorators.handle_db_errors
def get_users_with_pagination(
    db: Session,
    page_number: int | None = None,
    num_records: int | None = None,
    show_inactive: bool | None = True,
    show_email_unverified: bool | None = True,
    show_pending_approval: bool | None = True,
) -> list[users_models.Users]:
    """
    Retrieve a paginated list of users with optional filtering.

    Args:
        db (Session): Database session for executing queries.
        page_number (int | None): The page number for pagination (1-indexed).
            If None, pagination is not applied. Defaults to None.
        num_records (int | None): The number of records per page.
            If None, pagination is not applied. Defaults to None.
        show_inactive (bool | None): If False, excludes inactive users.
            Defaults to True (includes inactive users).
        show_email_unverified (bool | None): If False, excludes users with
            unverified emails. Defaults to True (includes email unverified
            users).
        show_pending_approval (bool | None): If False, excludes users pending
            admin approval. Defaults to True (includes pending approval users).

    Returns:
        list[users_models.Users]: A list of User objects matching the specified
            criteria, ordered by username. Returns an empty list if no users
            match the filters.
    """
    stmt = select(users_models.Users)

    if show_inactive is False:
        stmt = stmt.where(users_models.Users.active.is_(True))
    if show_email_unverified is False:
        stmt = stmt.where(users_models.Users.email_verified.is_(True))
    if show_pending_approval is False:
        stmt = stmt.where(users_models.Users.pending_admin_approval.is_(False))

    stmt = stmt.order_by(users_models.Users.username)

    if page_number is not None and num_records is not None:
        stmt = stmt.offset((page_number - 1) * num_records).limit(num_records)

    return list(db.execute(stmt).scalars().all())

save_user_image_file async

save_user_image_file(user_id, file, db)

Save user image file with security validation and update DB.

Uses centralized file upload handler for validation and async I/O, then updates user photo path in database.

Parameters:

Name Type Description Default
user_id int

ID of user whose image is being saved.

required
file UploadFile

Uploaded image file (UploadFile).

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
str

Path to saved image file.

Raises:

Type Description
HTTPException

400 if filename or extension is invalid, 413 if too large, 500 if upload fails.

Source code in backend/app/users/users/utils.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
async def save_user_image_file(user_id: int, file: UploadFile, db: Session) -> str:
    """
    Save user image file with security validation and update DB.

    Uses centralized file upload handler for validation and async
    I/O, then updates user photo path in database.

    Args:
        user_id: ID of user whose image is being saved.
        file: Uploaded image file (UploadFile).
        db: SQLAlchemy database session.

    Returns:
        Path to saved image file.

    Raises:
        HTTPException: 400 if filename or extension is invalid,
            413 if too large, 500 if upload fails.
    """
    if not file.filename:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Filename is required",
        )

    # Defense-in-depth allow-list on the user-supplied extension.
    # SafeUploads still validates the magic number afterwards, so a
    # mismatched signature is rejected even if the extension passes.
    _, file_extension = os.path.splitext(file.filename)
    file_extension = file_extension.lower()
    if file_extension not in _ALLOWED_USER_IMAGE_EXTENSIONS:
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
            detail="Unsupported user image file type",
        )

    filename = f"{user_id}{file_extension}"

    # Save file using centralized file upload handler
    await core_file_uploads.save_validated_upload(
        file,
        kind=core_file_uploads.UploadKind.IMAGE,
        upload_dir=core_config.USER_IMAGES_DIR,
        filename=filename,
    )

    # Update user photo path in database
    return str(await users_crud.update_user_photo(user_id, db, os.path.join(core_config.USER_IMAGES_DIR, filename)))

update_user_photo async

update_user_photo(user_id, db, photo_path=None)

Update a user's photo path.

Parameters:

Name Type Description Default
user_id int

ID of user to update photo for.

required
db Session

SQLAlchemy database session.

required
photo_path str | None

New photo path. If None, removes photo.

None

Returns:

Type Description
str | None

The updated photo path, or None if removed.

Raises:

Type Description
HTTPException

404 if user not found.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
@core_decorators.handle_db_errors
async def update_user_photo(user_id: int, db: Session, photo_path: str | None = None) -> str | None:
    """
    Update a user's photo path.

    Args:
        user_id: ID of user to update photo for.
        db: SQLAlchemy database session.
        photo_path: New photo path. If None, removes photo.

    Returns:
        The updated photo path, or None if removed.

    Raises:
        HTTPException: 404 if user not found.
        HTTPException: 500 if database error occurs.
    """
    # Get the user from the database
    db_user = users_utils.get_user_by_id_or_404(user_id, db)

    # Update the user
    db_user.photo_path = photo_path

    # Commit the transaction
    db.commit()
    db.refresh(db_user)

    if photo_path:
        # Return the photo path
        return photo_path
    else:
        # Delete the user photo in the filesystem
        await users_utils.delete_user_photo_filesystem(user_id)

        return None

verify_user_email

verify_user_email(user_id, server_settings, db)

Verify user email and conditionally activate account.

Parameters:

Name Type Description Default
user_id int

ID of user to verify.

required
server_settings ServerSettingsRead

Server config determining activation policy.

required
db Session

SQLAlchemy database session.

required

Returns:

Type Description
None

None

Raises:

Type Description
HTTPException

404 if user not found.

HTTPException

500 if database error occurs.

Source code in backend/app/users/users/crud.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
@core_decorators.handle_db_errors
def verify_user_email(
    user_id: int,
    server_settings: server_settings_schema.ServerSettingsRead,
    db: Session,
) -> None:
    """
    Verify user email and conditionally activate account.

    Args:
        user_id: ID of user to verify.
        server_settings: Server config determining activation policy.
        db: SQLAlchemy database session.

    Returns:
        None

    Raises:
        HTTPException: 404 if user not found.
        HTTPException: 500 if database error occurs.
    """
    # Get the user from the database
    db_user = users_utils.get_user_by_id_or_404(user_id, db)

    db_user.email_verified = True
    if not server_settings.signup_require_admin_approval:
        db_user.pending_admin_approval = False
        db_user.active = True

    # Commit the transaction
    db.commit()
    db.refresh(db_user)