Files
kite/.planning/codebase/CONVENTIONS.md
T
2026-06-02 15:32:06 +02:00

217 lines
10 KiB
Markdown

# Coding Conventions
**Analysis Date:** 2026-06-02
## Naming Patterns
**Python files:**
- `snake_case` throughout — `auth.py`, `cloud_utils.py`, `document_tasks.py`
- Modules named for their responsibility, not their layer (e.g., `services/auth.py`, `services/audit.py`)
**Python functions:**
- `snake_case` for all functions and methods: `hash_password`, `verify_password`, `create_access_token`, `write_audit_log`
- Private helpers prefixed with underscore: `_set_refresh_cookie`, `_port_open`, `_set_doc_user_id`
- Async functions use same convention — no `async_` prefix
**Python classes:**
- `PascalCase` for ORM models and Pydantic models: `User`, `Document`, `RegisterRequest`, `DocumentPatch`
- Request/response models end in `Request` or `Response`: `RegisterRequest`, `LoginRequest`, `ChangePasswordRequest`
**Python variables:**
- `snake_case`: `user_id`, `access_token`, `used_bytes`, `credentials_enc`
- Constants use `UPPER_SNAKE_CASE`: `_PASSWORD_DETAIL` (underscore prefix when module-private)
- Module-level singletons prefixed underscore: `_pwd`, `_CLOUD_PROVIDERS`
**DB column naming:**
- `snake_case` for all columns: `user_id`, `password_hash`, `is_active`, `created_at`
- Exception: ORM attribute `metadata_` maps to DB column `metadata` (reserved SQLAlchemy name)
- Timestamp columns use `_at` suffix: `created_at`, `used_at`
- Boolean columns use `is_` or no prefix: `is_active`, `totp_enabled`, `password_must_change`
**Frontend files:**
- Vue components: `PascalCase``DocumentCard.vue`, `FolderTreeItem.vue`, `StorageBrowser.vue`
- Stores: `camelCase.js``auth.js`, `documents.js`, `cloudConnections.js`
- Utilities: `camelCase.js``formatters.js`
- API client: single file `src/api/client.js`
- Test files: `ComponentName.test.js` or `storeName.test.js` inside `__tests__/` subdirectory
**Frontend functions and variables:**
- `camelCase`: `formatDate`, `formatSize`, `providerColor`, `fetchDocuments`, `uploadToMinIO`
- Store composables use `use` prefix: `useAuthStore`, `useFoldersStore`, `useDocumentsStore`
- Private helpers prefixed underscore: `_refreshInFlight`
- Event names emitted from components: `kebab-case``'breadcrumb-navigate'`, `'folder-create'`, `'file-open'`
## Code Style
**Formatting:**
- No Prettier, ESLint, Black, or Ruff config committed — style maintained by convention only
- Backend follows PEP 8 organically; 4-space indentation
- Tailwind CSS utility classes applied inline in Vue templates; no scoped `<style>` blocks used
**Python style specifics:**
- `from __future__ import annotations` at top of all `api/` and `services/` files (all 8 api/ files confirmed)
- `Optional[X]` used instead of `X | None` union syntax — maintained for Python < 3.10 compatibility even though runtime is 3.12
- Type annotations on all function signatures and ORM `Mapped[...]` column declarations
- Docstrings present on all public functions and modules; module docstrings explain invariants and phase context
**Vue/JS style specifics:**
- `<script setup>` Composition API used for ALL Vue components — no Options API exists (all 30+ components confirmed)
- Pinia stores use setup function syntax (not options syntax): `defineStore('name', () => { ... })`
- `ref()` for all reactive state; `computed()` for derived values; `watch()` for side effects
- Props always explicitly typed: `{ type: Object, required: true }`
- `emits` declared on components that emit events
## Import Organization
**Python imports (consistent order across all api/ and services/ files):**
1. `from __future__ import annotations` (first line, when present)
2. Standard library (`import uuid`, `import hashlib`, `import logging`)
3. Third-party (`from fastapi import ...`, `from sqlalchemy import ...`, `from pydantic import ...`)
4. Internal (`from config import settings`, `from db.models import ...`, `from deps.auth import ...`, `from services import ...`)
Example from `backend/api/auth.py`:
```python
from __future__ import annotations
import uuid
from typing import Literal, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import BaseModel, EmailStr
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from db.models import BackupCode, Quota, RefreshToken, User
from deps.auth import get_current_user
from deps.db import get_db
from services import auth as auth_service
```
**Frontend imports (consistent order):**
1. `import { ... } from 'vue'` — Vue composables
2. `import { ... } from 'vue-router'` — router composables
3. `import { useXStore } from '../stores/x.js'` — Pinia stores
4. `import * as api from '../../api/client.js'` — API client (namespace import)
5. `import ChildComponent from './ChildComponent.vue'` — child components
6. `import { formatDate } from '../../utils/formatters.js'` — shared utilities
**Path resolution:** Relative paths throughout — no `@/` alias configured.
## Error Handling
**Backend — service vs API layer separation (strict pattern):**
- `services/` functions raise `ValueError` with descriptive messages — NEVER `HTTPException`
- `api/` handlers catch `ValueError` and map to HTTP status codes
- Pattern from `api/auth.py`:
```python
try:
auth_service.validate_password_strength(body.new_password)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
```
**HTTP status codes used:**
- `201` — resource created (register, share, folder)
- `401` — unauthenticated or wrong credentials
- `403` — forbidden (wrong role, wrong owner, admin blocked from document content)
- `404` — not found
- `409` — conflict (duplicate email/handle)
- `413` — quota exceeded
- `422` — validation failure (weak password, invalid field value)
- `429` — rate limited
**Audit log exceptions:**
- `services/audit.py` `write_audit_log()` catches all exceptions and calls `logger.warning()`
- Audit failure MUST NOT abort the primary operation — no re-raise under any circumstance
**Frontend error handling:**
- Stores catch errors and set `error.value = e.message`; `loading.value` always reset in `finally`
- `api/client.js` `request()` throws `Error` with `.status` and optional `.payload` properties
- On 401: automatic single-retry after `authStore.refresh()`; on refresh failure throws `'Session expired'`
## Logging
**Framework:** Python `logging` module with `logger = logging.getLogger(__name__)` per module.
**Patterns:**
- `%`-style format strings (never f-strings in log calls): `logger.warning("audit log write failed: %s", exc)`
- `logger.info` for successful notable operations; `logger.warning` for non-fatal failures; `logger.error` for operation failures
- Never log secrets, tokens, passwords, or PII
- Auth events, quota violations, and admin actions are written to the `AuditLog` DB table via `write_audit_log()` — not the Python logger
**Frontend:** No logging framework — `console.*` not used in production code.
## Comments
**Module docstrings — every backend module has:**
- Summary of what it implements (with HTTP endpoint paths)
- Security invariants it enforces (with REQ-IDs: `SEC-02`, `AUTH-07`, `D-04`)
- Plan/phase traceability note
**Inline comments:**
- Security-sensitive lines carry rationale: `# CLAUDE.md constraint`, `# SEC-06`, `# T-03-22`
- SQLAlchemy quirks explained inline where non-obvious
- `# ── Section Name ──────` horizontal rules separate logical sections within long files
**Test docstrings:**
- Every test function has a one-line docstring describing what it asserts: `"""POST /api/auth/register with valid data returns 201 with id and handle."""`
## Function Design
**Backend:**
- Single responsibility per function — auth service functions do exactly one thing
- DB-touching functions are `async` and take `AsyncSession` as a parameter
- Pydantic `@field_validator` used for complex field constraints (e.g., `filename_no_path_separators`)
**Frontend:**
- Store actions are `async` functions defined inside `defineStore` setup
- Utility functions in `src/utils/formatters.js` are pure — no side effects, no imports
- Test factory helpers follow `makeFolder(overrides = {})` pattern — spread overrides over defaults
## Module Design
**Backend:**
- All routers named `router`: `router = APIRouter(prefix="/api/...", tags=[...])`
- Settings singleton: `settings = Settings()` at bottom of `config.py`; imported as `from config import settings`
- No `__all__` declarations — convention limits what callers import
**Frontend:**
- Named exports from stores: `export const useAuthStore = defineStore(...)`
- Named exports from utilities: `export function formatDate(iso) { ... }`
- Default exports from Vue components (implicit via `<script setup>`)
- `src/api/client.js`: named exports only; `request()` is unexported internal helper
## Backend Dependency Injection
FastAPI `Depends()` is used for all cross-cutting concerns. Three standard dependencies in `backend/deps/`:
- `get_db` (`deps/db.py`) — yields `AsyncSession`; overridden in tests with in-memory SQLite session
- `get_current_user` (`deps/auth.py`) — validates Bearer JWT, returns `User`; raises 401
- `get_current_admin` (`deps/auth.py`) — delegates to `get_current_user`, checks `role == 'admin'`; raises 403
- `get_regular_user` (`deps/auth.py`) — delegates to `get_current_user`, blocks `role == 'admin'`; raises 403
Usage pattern in route handlers:
```python
@router.get("/protected")
async def protected_endpoint(
current_user: User = Depends(get_regular_user),
session: AsyncSession = Depends(get_db),
):
...
```
## Security-Enforced Invariants in Code
The following patterns are mandatory and must not be deviated from:
- **Token storage:** `accessToken` lives only in Pinia `ref()` — never `localStorage`, never `sessionStorage`
- **Refresh cookie:** `httponly=True, secure=True, samesite="strict"` on every `set_cookie` call
- **Ownership check:** every document/folder/share endpoint asserts `resource.user_id == current_user.id`
- **Object keys:** `{user_id}/{document_id}/{uuid4()}{ext}` — human filename stored in DB only
- **Quota:** atomic `UPDATE quotas SET used_bytes = used_bytes + $delta WHERE (used_bytes + $delta) <= limit_bytes RETURNING used_bytes` — never read-then-write
- **Admin exclusion:** admin accounts blocked from all `/api/documents/*` endpoints via `get_regular_user`
---
*Convention analysis: 2026-06-02*