131 lines
6.4 KiB
Markdown
131 lines
6.4 KiB
Markdown
# HR-ATS Backend LLM Context Prompt
|
||
|
||
Copy everything below the line into any LLM session before asking it to write or edit backend code.
|
||
|
||
---
|
||
|
||
You are coding inside **HR-ATS-Portal** (`backend/`). You must follow this house style **exactly**. Mirror neighboring files. Do not invent alternate patterns, layers, or response shapes. Prefer matching existing code over “cleaner” industry defaults.
|
||
|
||
## Goal
|
||
|
||
Every change must look like it was written by the same author as `backend/users/` and `backend/inbox/`.
|
||
|
||
## 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)
|
||
```
|
||
|
||
- Bare `router = APIRouter()`; mount in `main.py` with `app.include_router(...)`.
|
||
- No package `__init__.py`. Run from `backend/` so imports are top-level (`users.app`, `db_setup`).
|
||
- Non-DB config: module-level `load_dotenv()` + `os.getenv(...)`. Do **not** extend `db_setup.Settings` for app secrets.
|
||
|
||
## Layer duties (non-negotiable)
|
||
|
||
| Layer | Owns | Must NOT do |
|
||
|---|---|---|
|
||
| `app.py` | Routes, inline request Pydantic models, `JSONResponse`, HTTP token envelope via serializers, inject `session` / `CurrentUser` | Business rules, SQL, JWT crypto beyond calling plugin functions |
|
||
| `views.py` | Business checks, call models, raise `HTTPException`, return ORM user (auth) or serialized dict (CRUD) | Call `serialize_token` or build login HTTP payloads |
|
||
| `models.py` | Fields, queries, inserts/updates/soft-delete, `selectinload` when needed | HTTPException, FastAPI, serializers |
|
||
| `serializers.py` | `serialize_*` → plain `dict` | DB, Depends |
|
||
| `plugins.py` | Pure helpers; raise library errors (`jwt.*`) | Import FastAPI / raise HTTPException |
|
||
| `permissions.py` | `OAuth2PasswordBearer`, `get_current_user`, `CurrentUser` alias, `require_permission` | Route handlers |
|
||
|
||
## Exact route pattern (`app.py`)
|
||
|
||
- Paths are verb-in-path: `/users/create`, `/users/fetch`, `/users/login` — **not** `/auth/token`, not REST-resource-only.
|
||
- Standard wrapper on every handler:
|
||
|
||
```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: `{"data":items,"total":total,"status_code":200}`. By id: include `"total":1`.
|
||
- Login / refresh — **service returns ORM user**; route mints tokens and serializes:
|
||
|
||
```python
|
||
user=await service.authenticate_user(form_data.username,form_data.password)
|
||
tokens=serialize_token(create_access_token(user),create_refresh_token(user),user)
|
||
return JSONResponse(content={**tokens,"status_code":200})
|
||
```
|
||
|
||
- Request body models stay **inline in `app.py`** (`UserCreate`, `UserUpdate`, `TokenRefresh`). Never move them into `serializers.py`.
|
||
- Use `session: AsyncSession = Depends(get_session)` by default. Use `Annotated` only when required (`OAuth2PasswordRequestForm`, `CurrentUser` before other defaulted params).
|
||
- Preserve tight local spacing: `service=User(session=session)`, `detail=str(e)`. Do not pretty-reformat unrelated code.
|
||
|
||
## Exact 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)
|
||
```
|
||
|
||
- Leave service method parameters untyped (match existing).
|
||
- Raise `HTTPException(status_code=...,detail="...")` for domain errors.
|
||
- `authenticate_user` / `refresh_access_token` return the **Users ORM instance only**.
|
||
- If you loaded via an accessor that does not `selectinload(role)`, re-fetch with `get_user_by_id` before serialization that touches `user.role`. (`get_user_by_email` and `get_user_by_id` both eager-load `role` today.)
|
||
|
||
## Exact model pattern (`models.py`)
|
||
|
||
- SQLModel `table=True`; accessors as `@classmethod async def`.
|
||
- Soft delete sets `is_deleted=True` and `is_active=False`.
|
||
- `selectinload` relations that serializers read.
|
||
- Commits happen inside write accessors (existing convention).
|
||
|
||
## Serializers
|
||
|
||
- Hand-built dicts only. `str(uuid)`, `.isoformat()` for datetimes. Never include `password`.
|
||
- Token response: OAuth2 fields at **root** (`access_token`, `refresh_token`, `token_type`, `expires_in`); user record under `data`.
|
||
|
||
## Auth (when touching users auth)
|
||
|
||
- PyJWT access + refresh with a `type` claim; `decode_token(..., expected_type=...)` rejects mismatches.
|
||
- Protect `/users/*` with `current_user: CurrentUser` except `/users/login` and `/users/refresh`. Prefer `Depends(require_permission(...))` on mutating/list routes that need a specific tag; keep `/users/me` on plain `CurrentUser` so users can discover a missing-role state.
|
||
- `get_current_user`: decode access → DB by `sub` → reject missing/deleted/inactive → return `serialize_user(user, with_permissions=True)`.
|
||
- Login: `OAuth2PasswordRequestForm` (username = email). `tokenUrl="users/login"` (no leading slash).
|
||
- JWT `iat`/`exp` use `datetime.now(timezone.utc)` only — never naive `datetime.now()`.
|
||
|
||
## Dependencies / env
|
||
|
||
- Add pins to `backend/requirements.txt` under banner comments with a trailing `# why` comment.
|
||
- Put secrets in `backend/.env`; keep key names in `backend/.env.example`.
|
||
|
||
## Hard bans
|
||
|
||
1. No repository / use-case / DTO layers beyond inline request models.
|
||
2. No Pydantic response models; no alternate envelopes; no `/api/v1` prefix.
|
||
3. No `serialize_token` inside `views.py`.
|
||
4. No FastAPI imports in `plugins.py`.
|
||
5. No drive-by refactors, renames, or whole-file reformats.
|
||
6. RBAC exists in `users/permissions.py`; do not invent a second scheme.
|
||
7. Do not edit unrelated domains (`inbox/` vs `users/`) unless asked.
|
||
8. Do not add `__init__.py` to make packages.
|
||
|
||
## Workflow when adding an endpoint
|
||
|
||
1. Model accessor (if DB).
|
||
2. Service method in `views.py`.
|
||
3. `serialize_*` if new shape.
|
||
4. Route in `app.py` with the standard try/except + `JSONResponse`.
|
||
5. Add `current_user: CurrentUser` or `Depends(require_permission(...))` if the route is protected.
|
||
|
||
Before finishing, re-read the touched files and confirm they still match a sibling file’s structure, naming, spacing, and response shape.
|