123 lines
5.9 KiB
Plaintext
123 lines
5.9 KiB
Plaintext
---
|
|
description: Strict HR-ATS backend house style — layering, routes, serializers, auth
|
|
globs: backend/**/*.py
|
|
alwaysApply: false
|
|
---
|
|
|
|
# HR-ATS Backend — singular pattern (mandatory)
|
|
|
|
Match existing modules (`users/`, `inbox/`) exactly. Do not introduce alternate frameworks, layers, or response shapes.
|
|
|
|
## Package layout (every domain)
|
|
|
|
```
|
|
backend/<domain>/
|
|
app.py # routes only — HTTP in/out
|
|
views.py # service class — business logic
|
|
models.py # SQLModel table + classmethod DB accessors
|
|
serializers.py # hand-rolled dict builders (no Pydantic response models)
|
|
plugins.py # pure helpers (hash, JWT, clean payload) — NO FastAPI imports
|
|
permissions.py # OAuth2 scheme + Depends aliases (auth domains only)
|
|
```
|
|
|
|
- Mount with bare `router = APIRouter()`; register in `main.py` via `app.include_router(...)`.
|
|
- No package `__init__.py`; run uvicorn from `backend/` so imports are top-level (`users.app`, `db_setup`).
|
|
- Config: `load_dotenv()` + `os.getenv(...)` in the module that needs it. Do not extend `db_setup.Settings` for non-DB keys.
|
|
|
|
## Layer duties (strict)
|
|
|
|
| Layer | Owns | Must NOT |
|
|
|---|---|---|
|
|
| `app.py` | Routes, request Pydantic models (inline), `JSONResponse`, call serializers for HTTP payloads, inject `CurrentUser` / `session` | Business rules, DB queries, JWT encode logic beyond calling plugins |
|
|
| `views.py` | Validate rules, call models, raise `HTTPException`, return ORM rows or serialized dicts for CRUD | Build login/token HTTP envelopes; call `serialize_token` |
|
|
| `models.py` | Table fields, `select`/`insert`/`update`/`soft_delete`, `selectinload` when relations are needed | HTTPExceptions, serializers, FastAPI |
|
|
| `serializers.py` | `serialize_*` → plain `dict` (`str(uuid)`, `.isoformat()` dates, never password) | DB access, Depends |
|
|
| `plugins.py` | Pure functions; raise library errors (e.g. `jwt.*`), not HTTP | Import FastAPI |
|
|
| `permissions.py` | `OAuth2PasswordBearer`, `get_current_user`, `CurrentUser = Annotated[...]`, `require_permission` | Route handlers |
|
|
|
|
## Route pattern (`app.py`)
|
|
|
|
- Verb-in-path: `/users/create`, `/users/fetch`, `/users/login` — not `/auth/token`, not REST nouns-only.
|
|
- Every handler uses this wrapper:
|
|
|
|
```python
|
|
try:
|
|
service=User(session=session)
|
|
data=await service.some_method(...)
|
|
return JSONResponse(content={"data":data,"status_code":200})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500,detail=str(e))
|
|
```
|
|
|
|
- List fetch: `{"data":items,"total":total,"status_code":200}`; single-by-id: `total: 1`.
|
|
- Login/refresh: build tokens in the route, then serialize:
|
|
|
|
```python
|
|
user=await service.authenticate_user(...)
|
|
tokens=serialize_token(create_access_token(user),create_refresh_token(user),user)
|
|
return JSONResponse(content={**tokens,"status_code":200})
|
|
```
|
|
|
|
- Request schemas live **inline** in `app.py` (`UserCreate`, `TokenRefresh`, …). Not in `serializers.py`.
|
|
- Dependencies: default form `session: AsyncSession = Depends(get_session)` unless `Annotated` is required (auth form / `CurrentUser` before other defaults).
|
|
- Tight spacing house style: `service=User(session=session)`, `detail=str(e)`, `key=value` in calls — match neighbors, do not “pretty-reformat” whole files.
|
|
|
|
## Service pattern (`views.py`)
|
|
|
|
```python
|
|
class User:
|
|
def __init__(self,session:AsyncSession):
|
|
self.session=session
|
|
|
|
async def create_user(self,payload):
|
|
...
|
|
return serialize_user(user) # CRUD returns serialized dict
|
|
```
|
|
|
|
- Untyped method args (match existing). Raise `HTTPException(status_code=...,detail="...")`.
|
|
- Auth methods (`authenticate_user`, `refresh_access_token`) return the **ORM user** only — never `serialize_token`.
|
|
- After email lookup that lacks `selectinload(role)`, re-fetch via `get_user_by_id` before anything that touches `user.role`.
|
|
|
|
## Model accessors (`models.py`)
|
|
|
|
- `@classmethod async def get_* / insert_* / update_* / soft_delete_* / count_*`.
|
|
- Soft delete: set `is_deleted=True`, `is_active=False`.
|
|
- Use `selectinload(cls.role)` on fetches that will be serialized with role fields.
|
|
- Commit inside model write methods (existing pattern).
|
|
|
|
## Auth pattern
|
|
|
|
- Access + refresh JWTs via `plugins` (`type` claim must be checked in `decode_token`).
|
|
- `permissions.CurrentUser` on every protected `/users/*` route; login + refresh stay open.
|
|
- `get_current_user`: decode access → DB load by `sub` → reject missing/deleted/inactive → `serialize_user`.
|
|
- Login uses `OAuth2PasswordRequestForm`; username field carries email.
|
|
- OAuth2 fields (`access_token`, `refresh_token`, `token_type`, `expires_in`) at **response root**; user under `data`.
|
|
- `tokenUrl="users/login"` (no leading slash).
|
|
- No FastAPI in `plugins.py`. Translate `jwt.PyJWTError` → 401 in `permissions` / `views`.
|
|
|
|
## Dependencies / env
|
|
|
|
- Pin in `requirements.txt` under comment banners with trailing rationale comments.
|
|
- Secrets in `backend/.env`; document keys in `backend/.env.example`.
|
|
- JWT times: `datetime.now(timezone.utc)` only (never naive `datetime.now()` for token `iat`/`exp`).
|
|
|
|
## Hard bans
|
|
|
|
- No new abstraction layers (repositories, use-cases, DTOs beyond inline Pydantic requests).
|
|
- No Pydantic response models; no `jsonable_encoder` for these routes.
|
|
- No changing response envelope (`data` + `status_code`) or inventing `/api/v1` prefixes.
|
|
- No drive-by refactors or reformatting unrelated code.
|
|
- No RBAC second scheme — vocabulary and `require_permission` live in `users/permissions.py` only.
|
|
- Do not touch `inbox/` when the task is `users/` (and vice versa) unless asked.
|
|
- No `__init__.py` packages; no moving request models into `serializers.py`.
|
|
|
|
## When adding a new domain endpoint
|
|
|
|
1. Accessor on `models.py` if DB changes.
|
|
2. Method on service in `views.py`.
|
|
3. `serialize_*` in `serializers.py` if new shape.
|
|
4. Route in `app.py` with the standard try/except + `JSONResponse`.
|
|
5. Protect with `current_user: CurrentUser` if under an authenticated router.
|