API
This commit is contained in:
145
app/routers/auth.py
Normal file
145
app/routers/auth.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.middleware.auth import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
get_current_user_id,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
from app.models import (
|
||||
AppleAuthRequest,
|
||||
AuthResponse,
|
||||
DeviceTokenRequest,
|
||||
LoginRequest,
|
||||
RefreshRequest,
|
||||
RegisterRequest,
|
||||
UserOut,
|
||||
)
|
||||
from app.services import push
|
||||
from app.services.db import get_pool
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
def _build_auth_response(user_row) -> AuthResponse:
|
||||
user_id = str(user_row["id"])
|
||||
return AuthResponse(
|
||||
access_token=create_access_token(user_id),
|
||||
refresh_token=create_refresh_token(user_id),
|
||||
expires_in=3600,
|
||||
user=UserOut(
|
||||
id=user_row["id"],
|
||||
email=user_row["email"],
|
||||
display_name=user_row["display_name"],
|
||||
timezone=user_row["timezone"],
|
||||
created_at=user_row["created_at"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def register(req: RegisterRequest):
|
||||
pool = await get_pool()
|
||||
|
||||
existing = await pool.fetchrow("SELECT id FROM users WHERE email = $1", req.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=409, detail="Email already registered")
|
||||
|
||||
hashed = hash_password(req.password)
|
||||
row = await pool.fetchrow(
|
||||
"""INSERT INTO users (email, password_hash, display_name, timezone)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, email, display_name, timezone, created_at""",
|
||||
req.email,
|
||||
hashed,
|
||||
req.display_name,
|
||||
req.timezone,
|
||||
)
|
||||
return _build_auth_response(row)
|
||||
|
||||
|
||||
@router.post("/login", response_model=AuthResponse)
|
||||
async def login(req: LoginRequest):
|
||||
pool = await get_pool()
|
||||
|
||||
row = await pool.fetchrow(
|
||||
"SELECT id, email, password_hash, display_name, timezone, created_at FROM users WHERE email = $1",
|
||||
req.email,
|
||||
)
|
||||
if not row or not row["password_hash"]:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
if not verify_password(req.password, row["password_hash"]):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
return _build_auth_response(row)
|
||||
|
||||
|
||||
@router.post("/apple", response_model=AuthResponse)
|
||||
async def apple_auth(req: AppleAuthRequest):
|
||||
# Decode the Apple identity token to extract the subject (user ID)
|
||||
# In production, verify signature against Apple's public keys
|
||||
from jose import jwt as jose_jwt
|
||||
|
||||
try:
|
||||
# Decode without verification for hackathon — in prod, fetch Apple's JWKS
|
||||
claims = jose_jwt.get_unverified_claims(req.identity_token)
|
||||
apple_user_id = claims["sub"]
|
||||
email = claims.get("email")
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Invalid Apple identity token")
|
||||
|
||||
pool = await get_pool()
|
||||
|
||||
# Try to find existing user
|
||||
row = await pool.fetchrow(
|
||||
"SELECT id, email, display_name, timezone, created_at FROM users WHERE apple_user_id = $1",
|
||||
apple_user_id,
|
||||
)
|
||||
if row:
|
||||
return _build_auth_response(row)
|
||||
|
||||
# Check if email already exists (link accounts)
|
||||
if email:
|
||||
row = await pool.fetchrow(
|
||||
"SELECT id, email, display_name, timezone, created_at FROM users WHERE email = $1",
|
||||
email,
|
||||
)
|
||||
if row:
|
||||
await pool.execute("UPDATE users SET apple_user_id = $1 WHERE id = $2", apple_user_id, row["id"])
|
||||
return _build_auth_response(row)
|
||||
|
||||
# Create new user
|
||||
row = await pool.fetchrow(
|
||||
"""INSERT INTO users (apple_user_id, email, display_name, timezone)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, email, display_name, timezone, created_at""",
|
||||
apple_user_id,
|
||||
email,
|
||||
req.full_name,
|
||||
"America/Chicago",
|
||||
)
|
||||
return _build_auth_response(row)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=AuthResponse)
|
||||
async def refresh(req: RefreshRequest):
|
||||
payload = decode_token(req.refresh_token, expected_type="refresh")
|
||||
user_id = payload["sub"]
|
||||
|
||||
pool = await get_pool()
|
||||
row = await pool.fetchrow(
|
||||
"SELECT id, email, display_name, timezone, created_at FROM users WHERE id = $1::uuid",
|
||||
user_id,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
|
||||
return _build_auth_response(row)
|
||||
|
||||
|
||||
@router.post("/device-token", status_code=204)
|
||||
async def register_device(req: DeviceTokenRequest, user_id: str = Depends(get_current_user_id)):
|
||||
await push.register_device_token(user_id, req.platform, req.token)
|
||||
Reference in New Issue
Block a user