from __future__ import annotations from enum import StrEnum class AuthErrorCode(StrEnum): """ Stable, machine-readable error codes used across the API. Rules: - never rename an existing value (treat as public API) - only add new values """ # Generic / defaults AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR" AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED" FORBIDDEN = "FORBIDDEN" HTTP_ERROR = "HTTP_ERROR" INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" # Auth/JWT INVALID_TOKEN = "INVALID_TOKEN" SESSION_ALREADY_ACTIVE = "SESSION_ALREADY_ACTIVE" # Authorization NOT_STAFF = "NOT_STAFF" NOT_IN_ACTIVE_PGROUP = "NOT_IN_ACTIVE_PGROUP" class DAQErrorCode(StrEnum): BEAMLINE_BUSY = "BEAMLINE_BUSY" _ERROR_CODE_HELP: dict[str, str] = { # Auth/JWT AuthErrorCode.AUTHENTICATION_ERROR: ( "Generic authentication problem. Usually means the request lacked valid credentials " "(expired/invalid token, missing Authorization header, etc.)." ), AuthErrorCode.AUTHENTICATION_FAILED: ( "Authentication failed during login/token creation. Typically incorrect credentials " "or an inability to validate the user." ), AuthErrorCode.INVALID_TOKEN: ( "The provided token could not be decoded/validated (bad signature, expired, malformed). " "Re-authenticate to obtain a new token." ), AuthErrorCode.SESSION_ALREADY_ACTIVE: ( "A different session currently owns control. Use “force current session” (if allowed) " "or wait for the active session to expire/end." ), # Authorization AuthErrorCode.FORBIDDEN: ( "Generic permissions failure. The user is authenticated but not allowed to perform this action." ), AuthErrorCode.NOT_STAFF: ( "This action requires staff privileges. Log in with a staff account or ask staff to perform it." ), AuthErrorCode.NOT_IN_ACTIVE_PGROUP: ( "You are not a member of the currently active p-group. Change p-group or use an account " "that belongs to the active group." ), # Generic AuthErrorCode.HTTP_ERROR: ( "Generic HTTP error wrapper. The server returned an HTTPException that wasn’t mapped to a more specific code." ), AuthErrorCode.INTERNAL_SERVER_ERROR: ( "Unhandled server error. Check server logs for a stack trace and context." ), DAQErrorCode.BEAMLINE_BUSY: ( "Beamline state is set to Busy by prior action. If this state persists an additional error may have occurred, " "preventing the state from being released, this should timeout within 10 minutes." "If this occurs please seek assistance from your local contact." ) } def error_code_help(code: str) -> str | None: """ Return a human help message for a code string, if known. Accepts either enum value strings or raw strings. """ if not code: return None return _ERROR_CODE_HELP.get(str(code)) def export_error_code_help() -> dict[str, str]: """ Export help text as {"CODE": "help text", ...} """ return {str(k): str(v) for k, v in _ERROR_CODE_HELP.items()} def export_error_codes_grouped() -> dict[str, dict[str, str]]: """ Export codes grouped by enum class name: { "AuthErrorCode": {"INVALID_TOKEN": "INVALID_TOKEN", ...}, "DAQErrorCode": {"BEAMLINE_BUSY": "BEAMLINE_BUSY", ...} } """ enums: tuple[type[StrEnum], ...] = (AuthErrorCode, DAQErrorCode) return {e.__name__: {c.name: str(c.value) for c in e} for e in enums} def export_error_codes() -> dict[str, str]: """ Backwards-compatible, flat export used by older clients/tests/docs: {"INVALID_TOKEN": "INVALID_TOKEN", ...} NOTE: This intentionally exports only AuthErrorCode to avoid breaking existing consumers that assume a flat map and/or specific keys. """ return {c.name: str(c.value) for c in AuthErrorCode}