Web Auth Services

File: api/nextango/apps/web_auth/services.py Last Updated: 2026-07-09

Overview

Four service classes carry the business logic behind web_auth's views: OTPService generates and verifies one-time passwords, RateLimitService applies progressive rate limiting and lockout, SessionService manages the WebSession lifecycle, and RegistrationService orchestrates customer registration using Redis as temporary storage ahead of user creation. All methods are static.


OTPService

Generates, verifies, and delivers one-time passwords.

generate_otp(email, purpose='login') Invalidates any existing unverified OTP for the same email and purpose, generates a new 6-digit numeric code with a 5-minute expiration, stores it, and emails it. Returns True on success. Raises if email delivery fails. purpose is 'login', 'password_recovery', 'verification', or 'email_change' (the last is used by the views layer but is not one of the model's declared purpose choices).

verify_otp(email, code) Looks up an unverified, unexpired OTP matching the email and code. If none matches, increments the attempt counter on all unverified OTPs for that email and returns False. If found and attempts are under 5, marks it verified and returns True. Returns False on any internal error rather than raising.

invalidate_otp(email) Marks all unverified OTPs for an email as verified, effectively cancelling them. Used to force regeneration of a fresh code.

Email delivery tries Resend first and falls back to Django SMTP if Resend is not configured or the request fails.


RateLimitService

Applies progressive rate limiting to OTP requests and progressive lockout to authentication failures. Backed by the Django cache framework, keyed per email.

check_rate_limit(email) Returns an error message string if the email has requested OTPs too frequently, or None if the request is allowed. Wait times escalate: 30 seconds after the first request, 5 minutes after 3 requests, 1 hour after 5 requests within the same hour, after which the counter resets.

record_failed_attempt(email, failure_type='invalid_otp') Records a failed authentication attempt (wrong password, wrong OTP, or unknown user) against the email, resetting the counter if the last failure was over an hour ago. A no-op if email is falsy.

check_failure_block(email) Returns a lockout message or None. Lockout escalates with failure count: 15 minutes at 5 or more failures, 1 hour at 7 or more, 2 hours at 10 or more, each window measured from the last failure.

clear_failed_attempts(email) Clears both the failure-lockout counter and the OTP rate-limit counter for the email. Called on successful authentication.


SessionService

Creates, validates, and terminates WebSession records, and looks up SessionPermission rules.

create_session(request, session_type, user=None, email=None) Ensures the Django session has a key, computes the expiration (2 hours for demo, 7 days for dashboard), and either updates the existing WebSession for the current session key or creates a new one. Stores the session's UUID in request.session['web_session_id'] and clears any legacy session keys (session_id, web_auth_session_id). Returns the WebSession instance. is_guest is derived from whether user is None.

validate_session(request) Looks up the WebSession referenced by request.session['web_session_id'], filtered to unexpired sessions, and touches last_activity. Returns the WebSession, or None if there is no session key, the row does not exist, or it has expired.

get_permissions(session_type) Returns the active SessionPermission queryset for a session type (demo or dashboard).

terminate_session(request) Deletes the WebSession referenced by the session, then flushes the Django session entirely.


RegistrationService

Orchestrates OTP-based, passwordless customer registration. No User record exists until the OTP step completes; pending registrations live in Redis with a 1-hour TTL, keyed by a SHA-256 hash of the email.

initiate_registration(email, request_metadata, name=None, first_name=None, last_name=None) Checks whether the email already belongs to an existing user (routes to login instead of registration) and whether a pending registration already exists in Redis. If a pending registration exists with a still-valid OTP, it is reused rather than regenerated, to avoid invalidating an OTP the user hasn't entered yet from a duplicate initiate call. Otherwise generates a registration token (reg_ prefix) and OTP, and stores the registration data. Returns a dict with success, message, registration_token, is_existing_user, and action (login or register). Rate-limited via RateLimitService.check_rate_limit().

verify_and_complete(email, otp_code, token) Verifies the registration token and OTP against the pending Redis entry. On OTP failure, increments an attempt counter and returns the remaining attempts, clearing the registration entirely after 5 failed attempts. On success, either logs in an existing user (ensuring they have a Customer role, adding one if missing) or creates a new User with a Customer role inside a transaction, guarding against a race where the email was registered concurrently. New users get a deterministic provisional sanity_id of user_{pk} so transaction references have a stable target before the first Sanity sync reconciles identity. Sanity sync itself is skipped during this creation (sync_context('skip_sync')). Clears rate-limit failures and the Redis registration entry on success.

resend_otp(email, token) Verifies the registration token against the pending Redis entry, applies the same rate limit as OTP requests generally, and generates a new OTP (invalidating the prior one). Resets the OTP attempt counter and increments a resend counter on the registration entry.


Was this page helpful?