feat(auth): add /v3/action/auth/authenticate_passcode endpoint
Moves passcode→JWT auth into the V3 action namespace. The new router (api_v3_actions_auth.py) is a clean re-registration of the existing working logic. The legacy /api/authenticate_passcode is kept but marked deprecated via DeprecationParams while the frontend transitions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ class PasscodeAuthRequest(BaseModel):
|
|||||||
site_id: str = Field(..., description="The random string ID of the site")
|
site_id: str = Field(..., description="The random string ID of the site")
|
||||||
passcode: str = Field(..., min_length=5, description="The passcode to verify")
|
passcode: str = Field(..., min_length=5, description="The passcode to verify")
|
||||||
|
|
||||||
@router.post('/authenticate_passcode', response_model=Resp_Body_Base)
|
@router.post('/authenticate_passcode', response_model=Resp_Body_Base, dependencies=[Depends(DeprecationParams)])
|
||||||
async def authenticate_passcode(
|
async def authenticate_passcode(
|
||||||
auth_req: PasscodeAuthRequest,
|
auth_req: PasscodeAuthRequest,
|
||||||
response: Response = Response,
|
response: Response = Response,
|
||||||
|
|||||||
97
app/routers/api_v3_actions_auth.py
Normal file
97
app/routers/api_v3_actions_auth.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from fastapi import APIRouter, Response
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
import json
|
||||||
|
|
||||||
|
from app.lib_general import sign_jwt, log
|
||||||
|
from app.config import settings
|
||||||
|
from app.db_sql import sql_select, get_id_random
|
||||||
|
from app.models.response_models import Resp_Body_Base, mk_resp
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
ROLE_PRIORITY = ['super', 'manager', 'administrator', 'trusted', 'public', 'authenticated']
|
||||||
|
|
||||||
|
ROLE_TTL = {
|
||||||
|
'super': 8 * 3600,
|
||||||
|
'manager': 24 * 3600,
|
||||||
|
'administrator': 48 * 3600,
|
||||||
|
'trusted': 48 * 3600,
|
||||||
|
'public': 24 * 3600,
|
||||||
|
'authenticated': 12 * 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasscodeAuthRequest(BaseModel):
|
||||||
|
"""Request model for site-based passcode authentication."""
|
||||||
|
site_id: str = Field(..., description="Random string ID of the site")
|
||||||
|
passcode: str = Field(..., min_length=5, description="The passcode to verify")
|
||||||
|
|
||||||
|
@router.post('/authenticate_passcode', response_model=Resp_Body_Base)
|
||||||
|
async def authenticate_passcode(
|
||||||
|
auth_req: PasscodeAuthRequest,
|
||||||
|
response: Response = Response,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Passcode-to-JWT. Verifies a passcode against site.access_code_kv_json.
|
||||||
|
Returns a signed JWT with the site's account context, full role flags,
|
||||||
|
and a per-role TTL. jwt.json_str.auth_type='passcode' distinguishes this
|
||||||
|
token from a user login JWT.
|
||||||
|
"""
|
||||||
|
site_id = auth_req.site_id
|
||||||
|
passcode = auth_req.passcode
|
||||||
|
|
||||||
|
search_data = {'id_random': site_id}
|
||||||
|
if record := sql_select(table_name='site', data=search_data):
|
||||||
|
access_codes_raw = record.get('access_code_kv_json')
|
||||||
|
access_codes = {}
|
||||||
|
if access_codes_raw:
|
||||||
|
try:
|
||||||
|
access_codes = json.loads(access_codes_raw) if isinstance(access_codes_raw, str) else access_codes_raw
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to parse access_code_kv_json for site {site_id}: {e}")
|
||||||
|
|
||||||
|
matched_role = None
|
||||||
|
for role in ROLE_PRIORITY:
|
||||||
|
code = access_codes.get(role)
|
||||||
|
if code and str(code) == str(passcode):
|
||||||
|
matched_role = role
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_role:
|
||||||
|
log.info(f"Auth Success: Verified '{matched_role}' passcode for site {site_id}")
|
||||||
|
|
||||||
|
account_id_random = record.get('account_id_random')
|
||||||
|
if not account_id_random:
|
||||||
|
if account_id_int := record.get('account_id'):
|
||||||
|
account_id_random = get_id_random(record_id=account_id_int, table_name='account')
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'account_id': account_id_random,
|
||||||
|
'super': (matched_role == 'super'),
|
||||||
|
'manager': (matched_role == 'manager'),
|
||||||
|
'administrator': (matched_role == 'administrator'),
|
||||||
|
'trusted': (matched_role == 'trusted'),
|
||||||
|
'public': (matched_role == 'public'),
|
||||||
|
'authenticated': (matched_role == 'authenticated'),
|
||||||
|
'json_str': json.dumps({
|
||||||
|
'auth_type': 'passcode',
|
||||||
|
'site_id': site_id,
|
||||||
|
'role': matched_role
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
token = sign_jwt(
|
||||||
|
secret_key=settings.JWT_KEY,
|
||||||
|
ttl=ROLE_TTL[matched_role],
|
||||||
|
**payload
|
||||||
|
)
|
||||||
|
|
||||||
|
return mk_resp(
|
||||||
|
data={'jwt': token, 'account_id': account_id_random, 'role': matched_role},
|
||||||
|
response=response
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.warning(f"Auth Failed: Invalid passcode for site {site_id}")
|
||||||
|
return mk_resp(data=False, status_code=401, response=response, status_message="Invalid passcode.")
|
||||||
|
else:
|
||||||
|
log.warning(f"Auth Failed: Site {site_id} not found.")
|
||||||
|
return mk_resp(data=False, status_code=404, response=response, status_message="Site not found.")
|
||||||
@@ -5,6 +5,7 @@ from app.routers import (
|
|||||||
data_store,
|
data_store,
|
||||||
event_badge_importing,
|
event_badge_importing,
|
||||||
event_importing,
|
event_importing,
|
||||||
|
api_v3_actions_auth,
|
||||||
api_v3_actions_email,
|
api_v3_actions_email,
|
||||||
api_v3_actions_hosted_file, api_v3_actions_event_file, api_v3_actions_event_exhibit, api_v3_actions_e_zoom, api_v3_actions_e_novi_mailman, api_v3_actions_idaa, api_v3_actions_user, lookup_v3,
|
api_v3_actions_hosted_file, api_v3_actions_event_file, api_v3_actions_event_exhibit, api_v3_actions_e_zoom, api_v3_actions_e_novi_mailman, api_v3_actions_idaa, api_v3_actions_user, lookup_v3,
|
||||||
user,
|
user,
|
||||||
@@ -54,6 +55,7 @@ def setup_routers(app: FastAPI):
|
|||||||
app.include_router(api_v3_actions_idaa.router, prefix='/v3/action/idaa', tags=['IDAA Actions (V3)'])
|
app.include_router(api_v3_actions_idaa.router, prefix='/v3/action/idaa', tags=['IDAA Actions (V3)'])
|
||||||
app.include_router(api_v3_actions_user.router, prefix='/v3/action/user', tags=['User (V3 Actions)'])
|
app.include_router(api_v3_actions_user.router, prefix='/v3/action/user', tags=['User (V3 Actions)'])
|
||||||
app.include_router(api_v3_actions_email.router, prefix='/v3/action/email', tags=['Email (V3 Actions)'])
|
app.include_router(api_v3_actions_email.router, prefix='/v3/action/email', tags=['Email (V3 Actions)'])
|
||||||
|
app.include_router(api_v3_actions_auth.router, prefix='/v3/action/auth', tags=['Auth (V3 Actions)'])
|
||||||
# app.include_router(lookup.router, prefix='/lu', tags=['Lookup']) # LEGACY (disabled) - superseded by /v3/lookup
|
# app.include_router(lookup.router, prefix='/lu', tags=['Lookup']) # LEGACY (disabled) - superseded by /v3/lookup
|
||||||
app.include_router(lookup_v3.router, prefix='/v3/lookup', tags=['Lookup V3'])
|
app.include_router(lookup_v3.router, prefix='/v3/lookup', tags=['Lookup V3'])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user