89f8d5a654
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
217 lines
10 KiB
Markdown
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*
|