Skip to content

Accounts — services

Responsibilities

The accounts services contain the business logic for workflows that should not live in views or serializers.

They are responsible for:

  • authentication and session lifecycle operations
  • password change and password reset issuance
  • invite lifecycle operations
  • role-based invite permission checks
  • account creation during invite acceptance
  • device/session mutation tied to authentication

They are not responsible for:

  • HTTP request/response handling
  • serializer field validation
  • URL routing
  • admin configuration
  • persistence-only model declarations

In this app, services are the place where multi-step domain actions live.


Main services

services.auth

Business logic for authentication, session management, and password workflows.

Responsibilities

  • resolve login identifier to the correct user account
  • authenticate and create device-backed sessions
  • revoke one or all refresh-token sessions
  • list user devices
  • change password
  • issue password reset emails
  • reset password after token validation

Main functions

resolve_login_username(identifier)

Resolves a login identifier to the username expected by Django authentication.

  • Inputs
  • identifier
  • Behavior
  • accepts username directly
  • if the identifier looks like an email address, resolves the matching user by email
  • Returns
  • username string or None

This allows the backend to support both username-based and email-based login without changing the underlying auth backend contract.


login_user(...)

Authenticates a user and registers or updates their device/session.

  • Inputs
  • request
  • identifier
  • password
  • device_id
  • platform
  • device_name
  • app_version
  • push_token
  • Behavior
  • resolves login identifier
  • authenticates the user
  • upserts the Device
  • creates a refresh token
  • creates a DeviceSession
  • Returns
  • dictionary with:
    • user
    • device
    • access
    • refresh
  • Raises
  • InvalidCredentialsError

This is the core login workflow for mobile and web clients.


logout_user(user, refresh_token)

Revokes a single refresh-token session.

  • Inputs
  • user
  • refresh_token
  • Behavior
  • parses the refresh token
  • extracts JTI
  • blacklists the token if present in outstanding tokens
  • marks matching DeviceSession as revoked
  • Raises
  • InvalidRefreshTokenError

This is the single-session logout workflow.


logout_all_user_sessions(user)

Revokes all active sessions for a user.

  • Inputs
  • user
  • Behavior
  • finds all active DeviceSession rows for the user
  • blacklists each refresh token JTI
  • marks all sessions revoked
  • Returns
  • count of revoked sessions

This is the “log out everywhere” workflow.


list_user_devices(user)

Returns registered devices for a user.

  • Inputs
  • user
  • Behavior
  • queries devices for the user
  • prefetches sessions
  • orders by recent activity
  • Returns
  • queryset of Device

This is used by device/session management endpoints.


revoke_user_session(user, session_id)

Revokes a single owned session.

  • Inputs
  • user
  • session_id
  • Behavior
  • loads the session only if it belongs to the user
  • blacklists its refresh JTI
  • marks the session revoked
  • Returns
  • revoked DeviceSession
  • Raises
  • SessionNotFoundError

This is used for targeted session revocation from device/session screens.


change_user_password(user, new_password)

Changes a logged-in user’s password.

  • Inputs
  • user
  • new_password
  • Behavior
  • sets the new password
  • saves the user
  • Returns
  • nothing

This function assumes validation has already been performed by the serializer.


issue_password_reset(email)

Issues a password reset email if the account exists.

  • Inputs
  • email
  • Behavior
  • normalizes the email
  • looks up active user
  • generates uid/token pair
  • queues password reset email task
  • Returns
  • True if a matching user was found
  • False otherwise

The API layer still returns a generic success response to avoid account enumeration.


reset_user_password(user, new_password)

Sets a new password after the reset token has already been validated.

  • Inputs
  • user
  • new_password
  • Behavior
  • updates stored password hash
  • Returns
  • nothing

Exceptions

AuthError

Base auth service exception.

InvalidCredentialsError

Raised when identifier resolution or authentication fails.

InvalidRefreshTokenError

Raised when logout receives an invalid refresh token.

SessionNotFoundError

Raised when a requested session does not exist for the given user.


services.invite

Business logic for invite lifecycle management.

Responsibilities

  • determine whether a user may invite into an organization
  • create invites with duplicate protection
  • resend invites
  • cancel invites
  • accept invites
  • create user accounts for invite acceptance when needed
  • assign org/customer membership on acceptance
  • expose filtered invite listings

Main functions

can_invite_to_org(user, org)

Determines whether a user may manage invites for an organization.

  • Inputs
  • user
  • org
  • Behavior
  • allows superusers
  • otherwise requires active org membership with admin/owner role
  • Returns
  • boolean

This is the role-based access gate for invite administration.


generate_unique_username(base_username)

Builds a unique username candidate for invite-created accounts.

  • Inputs
  • base_username
  • Behavior
  • starts from a normalized base
  • appends an incrementing suffix while the username exists
  • Returns
  • unique username string

This prevents username collisions when a new account is created from an invite email.


get_active_duplicate_invite(invite_type, email, org)

Checks for an already-active pending invite.

  • Inputs
  • invite_type
  • email
  • org
  • Behavior
  • matches same invite type, email, and org
  • only returns invites that are:
    • not accepted
    • not cancelled
    • not expired
  • Returns
  • matching Invite or None

This supports duplicate active invite protection.


list_invites_for_org(org, status_filter=None)

Lists invites for one organization.

  • Inputs
  • org
  • status_filter
  • Behavior
  • returns invites ordered by newest first
  • can filter by:
    • pending
    • accepted
    • expired
    • cancelled
  • Returns
  • queryset or filtered list of Invite
  • Raises
  • ValueError for unsupported status filter

This is used by invite management screens.


create_invite(...)

Creates a new invite and optionally sends email.

  • Inputs
  • created_by
  • invite_type
  • email
  • org
  • role
  • expires_in_days
  • send_email
  • Behavior
  • normalizes email/role
  • checks for duplicate active invite
  • creates the invite
  • queues email if enabled
  • Returns
  • new Invite
  • Raises
  • InviteAlreadyExistsError

This is the primary onboarding entry point.


resend_invite(invite, extend_if_expired=True, extension_days=14, send_email=True)

Resends an invite.

  • Inputs
  • invite
  • extend_if_expired
  • extension_days
  • send_email
  • Behavior
  • rejects cancelled invites
  • rejects accepted invites
  • optionally extends expired invites
  • queues invite email
  • Returns
  • updated Invite
  • Raises
  • InviteStateError

This is used when admins need to re-notify a user without creating a new invite.


cancel_invite(invite, cancelled_by)

Cancels a pending invite.

  • Inputs
  • invite
  • cancelled_by
  • Behavior
  • no-ops for already-cancelled invite
  • rejects accepted invite
  • stamps cancelled_at and cancelled_by
  • Returns
  • updated Invite
  • Raises
  • InviteStateError

This makes pending invites unusable without deleting historical records.


accept_invite(invite, actor_user, password=None)

Accepts an invite and ensures the correct membership is created.

  • Inputs
  • invite
  • actor_user
  • password
  • Behavior
  • rejects invalid invite states
  • if authenticated:
    • requires the authenticated user email to match the invite email
  • if anonymous:
    • reuses existing account by email, or
    • creates a new user when password is supplied
  • creates or updates:
    • OrgMembership for internal invites
    • CustomerMembership for customer invites
  • marks invite accepted
  • Returns
  • tuple of (user, invite)
  • Raises
  • InviteStateError
  • InvitePermissionError
  • InviteValidationError

This is the core invite-consumption workflow.


Exceptions

InviteError

Base invite service exception.

InviteAlreadyExistsError

Raised when an active duplicate invite already exists.

Carries: - invite — the existing conflicting invite

InviteStateError

Raised when an invite is in the wrong lifecycle state for the requested action.

Examples: - resending a cancelled invite - accepting an expired invite - cancelling an accepted invite

InvitePermissionError

Raised when the acting user is not allowed to accept the invite.

Most commonly: - authenticated user email does not match invite email

InviteValidationError

Raised when required input for the invite workflow is missing or invalid.

Most commonly: - anonymous acceptance requires account creation, but no password was supplied


Service relationship overview

flowchart TD
    AuthService["services.auth"]
    InviteService["services.invite"]

    User["User"]
    Device["Device"]
    DeviceSession["DeviceSession"]
    Invite["Invite"]
    OrgMembership["OrgMembership"]
    CustomerMembership["CustomerMembership"]
    EmailTasks["Email tasks"]

    AuthService --> User
    AuthService --> Device
    AuthService --> DeviceSession
    AuthService --> EmailTasks

    InviteService --> Invite
    InviteService --> User
    InviteService --> OrgMembership
    InviteService --> CustomerMembership
    InviteService --> EmailTasks