Skip to content

Commit ce8954a

Browse files
authored
Create accounts.py
1 parent 7b16cab commit ce8954a

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

‎app/controllers/accounts.py‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Account business logic: registration, credentials, token identity.
2+
3+
The routes keep only FastAPI's dependency plumbing; deciding whether a
4+
credential or token grants access is decided here.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import jwt as pyjwt
10+
from sqlalchemy import select
11+
from sqlalchemy.orm import Session
12+
13+
from ..infra.security import (
14+
create_access_token,
15+
create_refresh_token,
16+
decode_token,
17+
hash_password,
18+
verify_password,
19+
)
20+
from ..models import User, new_id
21+
from ..models.db import commit_now
22+
from .errors import Conflict, Forbidden, Unauthorized
23+
24+
25+
def token_pair(user: User) -> tuple[str, str]:
26+
"""Fresh (access, refresh) pair for a user."""
27+
return create_access_token(user.id), create_refresh_token(user.id)
28+
29+
30+
def register(db: Session, email: str, password: str) -> User:
31+
"""Create an account; the email must be unused."""
32+
if db.scalar(select(User).where(User.email == email)) is not None:
33+
raise Conflict("email already registered")
34+
user = User(id=new_id("usr"), email=email, password_hash=hash_password(password))
35+
db.add(user)
36+
db.flush()
37+
commit_now(db)
38+
return user
39+
40+
41+
def login(db: Session, email: str, password: str) -> User:
42+
"""Authenticate a password.
43+
44+
A wrong email and a wrong password give the same 401, so neither
45+
can be used to enumerate accounts; a disabled account is told
46+
apart (403) only once the password checked out.
47+
"""
48+
user = db.scalar(select(User).where(User.email == email))
49+
if user is None or not verify_password(password, user.password_hash):
50+
raise Unauthorized("invalid email or password")
51+
if not user.is_active:
52+
raise Forbidden("account disabled")
53+
return user
54+
55+
56+
def _user_of_token(db: Session, token: str, *, expected_type: str) -> User:
57+
try:
58+
payload = decode_token(token, expected_type=expected_type)
59+
except pyjwt.PyJWTError as exc:
60+
raise Unauthorized(f"invalid token: {exc}") from exc
61+
user = db.get(User, payload["sub"])
62+
if user is None or not user.is_active:
63+
raise Unauthorized("unknown or inactive user")
64+
return user
65+
66+
67+
def user_for_access_token(db: Session, token: str | None) -> User:
68+
"""Identify the caller from a bearer access token."""
69+
if not token:
70+
raise Unauthorized("missing bearer token")
71+
return _user_of_token(db, token, expected_type="access")
72+
73+
74+
def user_for_refresh_token(db: Session, token: str) -> User:
75+
"""Identify the holder of a refresh token."""
76+
return _user_of_token(db, token, expected_type="refresh")
77+
78+
79+
def require_admin(user: User) -> User:
80+
if not user.is_admin:
81+
raise Forbidden("admin required")
82+
return user

0 commit comments

Comments
 (0)