homepage >> Technical doc: Developer Guide    
 

Yossef Benharosh is an apt web developer and the author of the eBook The essentials of object oriented PHP.

Yossef Benharosh web developer profile linkedin twitter github

Technical doc: Developer Guide

Table of contents

  1. Goals & Principles
  2. Architecture overview (high level)
  3. Request / response contract (frontend ↔ backend)
  4. Auth & session rules (tokens, cookies, verification)
  5. Modal + overlay patterns (ModalManager, Confirm)
  6. Frontend structure & conventions (managers, UI, DI)
  7. Backend structure & conventions (models, schemas, utils, routers)
  8. Security considerations (XSS, CSRF, token lifecycle)
  9. Email verification flow (generate → send → verify)
  10. Adding a new module: step-by-step checklist
  11. Tests, migrations, and deployment notes
  12. Appendices: snippets & examples
  13. Pre-deployment steps
  14. TODOs and recommended follow ups

 

1. Goals & principles (short)

  • Single responsibility: one job per file/module. Keep helpers tiny and well-named.
  • Predictability: naming conventions & folder layout are fixed - follow them.
  • Separation of concerns: business logic in core/util/*, routing in routers/*, schemas in core/schemas.py.
  • Composition over inheritance on the frontend (ModalManager + modal instances, e.g. DI of SideDrawer etc.).
  • Security-first: validate/escape on both backend and frontend; treat front-end validation as UX, backend as authorization/last-guard.
  • Accessibility & UX: keyboard, ARIA, visible loading states, focus management and alert announcements.

 

2. Architecture overview

Backend

  • FastAPI + SQLAlchemy.
  • core/ holds models, schemas, utilities (business logic).
  • routers/ holds HTTP endpoints; they call core/util/* functions.

Frontend

  • Templates: Jinja2 in frontend/templates.
  • Vanilla JS modules under frontend/assets/scripts/:
    • managers/ — controllers (per-component).
    • ui/ — reusable widgets: Alerts, ModalManager, Confirm, UI helpers.
    • utils/ — api.js (request wrapper), auth.js, etc.
  • main.js wires managers and injects dependencies (DI).

 

3. Request/response contract

All frontend code expects the central request() helper which always returns a standardized object:

{
  success: boolean,
  status: number,
  data?: any,     // parsed object for 2xx responses
  error?: string  // human message or machine message
}

Backend endpoints should return JSON compatible with request() expectations. Client-side code branches on success. Do not bypass request().

request() must:

  • Detect non-JSON bodies safely.
  • Parse JSON only if content-type indicates JSON.

Return { success:false, status: xxx, error: <message> } on errors.

 

4. Auth & session rules

Tokens & cookies (current / preferred):

  • access_token (HttpOnly cookie): short-lived, used to authenticate requests.
  • refresh_token (HttpOnly cookie): used to refresh access token (rotate on use).
  • session cookie: optional (signed token). If present, treat the user as logged in; stored sub = user id (string).
  • JWT payload uses "sub": str(user_id), "exp", and "type" when applicable ("refresh", "email_confirmation"...).

Important rules:

  • Keep substring in tokens (some JWT libraries validate type).
  • Always decode tokens with algorithms=[settings.JWT_ALGORITHM] and settings.SECRET_KEY.
  • Protect token generation functions to always stringify sub.
  • Invalidate refresh tokens server-side on logout if you're tracking them (recommended).
  • For critical changes (password or username), consider rotating session tokens or forcing re-login.

Auth endpoints (examples):

  • POST /auth/login: returns cookie access_token via response.set_cookie(...).
  • POST /auth/logout: clear auth cookies, optionally revoke refresh token.
  • GET /auth/me: return current user (based on access_token cookie).
  • POST /auth/register: create user + send confirmation email (do not auto-login).
  • POST /auth/verify-confirmation-token: verify confirmation token.
  • POST /auth/forgot-password: send reset link.
  • POST /auth/reset-password: verify reset token and set new password.

Frontend AuthApi methods must use credentials: "include" when server relies on cookies.

 

5. Modals & Overlay (ModalManager pattern)

Goal: one global overlay, consistent open/close, ESC and click-outside handling, focus management, and automatic close of side drawer.

Core pieces:

  • ModalManager (singleton): registers all DOM modal roots (or instances) and provides open(id) / close(id) / closeAll(); shows/hides global overlay; closes SideDrawer when opening a modal.
  • Modal instances (e.g., AuthEditModal, ConfirmModal) implement .open(initialData) and .close(); composition: pass modalManager in their constructor.
  • ConfirmModal should be a class with show(opts) that returns Promise (resolve true on OK).

Behavior:

  • ModalManager.open(el):
    • call closeAll(), hide side drawer, show overlay, show modal (add .show), set focus to first interactive element (via Accessibility.focusFirstInteractive).
  • ModalManager.closeAll():
    • hide all modals and overlay, restore side drawer state.

Accessibility:

  • Overlay has .show class;
    When the modal opens, focus first [data-autofocus] or first non-disabled input/button.
  • Escape and overlay click closes modal.

Usage example in main.js:

const modalManager = new ModalManager({ sideDrawer });
modalManager.init();
const authEditModal = new AuthEditModal({ modalManager });
const auth = new AuthsManager({ authEditModal });

 

6. Frontend structure & conventions

Files & roles:

  • managers/* : per-page controllers. Expose init() and receive dependencies via DI.
  • ui/* : pure UI widgets: Alerts, ConfirmModal, ModalManager, UI (helpers like setButtonLoading, markInvalid, setRequiredForVisibleFields), Accessibility.
  • utils/api.js : request() and API wrappers: AuthApi, ProjectsApi, etc.
  • dom.js : central DOM references; use getters or document.querySelectorAll once on load and expose them in a structured object.

DI pattern:

  • Create instances in main.js and pass them to managers instead of global access.
  • Example: new AuthsManager({ sideDrawer, authEditModal, modalManager }).

Forms pattern:

  • For multi-mode auth UI, prefer multiple <form id="form-login">, <form id="form-register"> etc., rather than toggling required inputs inside a single form.
  • Attach submit handlers to each form, or to the container with delegated logic (consistent and tested).
  • Before submission, call UI.setRequiredForVisibleFields(container) to avoid "invalid control not focusable" error.

Validators:

  • UI validators (in UiHelpers) must return { valid: boolean, error: string | null }. Frontend only for UX; backend must re-validate.
  • Example: validateUsername() should consult window.FORBIDDEN_USERNAMES set (loaded asynchronously), and return human-friendly error messages.

Forbidden usernames:

  • Keep canonical list server-side (Python set).
  • Expose GET /auth/forbidden-usernames endpoint that returns {"forbidden": ["admin", ...]}.

Frontend loads it once (non-blocking) and caches in window.FORBIDDEN_USERNAMES. If unavailable, fall back to the empty set and re-try.

 

7. Backend structure & conventions

Preferred layout:

  • core/models.py : SQLAlchemy models (single file or split by domain).
  • core/schemas.py : pydantic models for all endpoints (request/response).
  • core/util/*.py : business logic functions (create_user, update_user, list_projects...). Routers call these.
  • routers/*.py : routing / parameter/dep resolution only; do not place business logic here.

Model pattern for new entities:

  • Fields: id (PK), created_at, updated_at, active, owner_id (FK to users.id), data (JSON/text) as needed.

Ownership:

  • Mutating endpoints must enforce owner_id == current_user.id. You can return 404 for non-owners (security-through-obscurity pattern), or 403—use current project convention.

Schemas:

  • Use Pydantic field_validator for per-field validation, but treat Pydantic as input validation only use the core/util/* validator for more complex checks like uniqueness.

User update pattern:

  • In users_service.update_user:
    • If username is updated, normalize (strip/lower?) and check uniqueness (exclude current id).
    • Validate username via validate_username() (server-side).
    • If password changes, hash with hash_password() and optionally rotate session token or revoke refresh tokens.

Forbidden usernames:

  • Keep RAW_FORBIDDEN_USERNAMES in a util module and convert to a set() of lower-case words for fast lookup.

Email sending:

  • All email sending should go through a single helper send_email(schemas.EmailRequest). Higher-level functions like send_reset_password() and send_confirmation_email() call it with appropriate HTML or templates.
  • For attachments: MessageSchema(..., attachments=[]) expects a list — always pass an empty list [] if no attachments, not None. (This is the fix for your FastMail error.)
  • Example fix in send_email:
    attachments = [data.attachment] if data.attachment else []
    message = MessageSchema(..., attachments=attachments)

 

8. Security checklist (important)

Backend:

  • Validate all inputs server-side (lengths, character classes, forbidden names).
  • Make the username unique at DB level (unique index) AND in service code (check first).
  • Hash passwords with a modern algorithm (bcrypt/argon2) and salt properly.
  • Rate limit login / forgot-password endpoints to prevent abuse.
  • Revoke or rotate refresh tokens on logout / password change.
  • For email content, escape user-supplied values; send only pre-constructed safe HTML templates.

Client:

  • Avoid innerHTML for untrusted data; use textContent or sanitized rendering.
  • Token links should use HTTPS in production.
  • Do not expose secret keys in the front end.
  • CSP headers recommended on production.

Cookies:

  • HttpOnly, Secure, SameSite=Lax (or Strict if appropriate).
  • Use short-lived access tokens; refresh tokens rotate.

CSP / XSS:

  • Sanitize any user-provided HTML before storing/displaying.

 

9. Email verification & reset flows

Registration → Email verification

  1. 4Client POST /auth/register with validated email/username/password.
  2. Server creates a user with is_verified=False.
  3. Server generates an email token:
    payload = {"sub": str(user_id), "exp": now + minutes, "type": "email_confirmation"}

token = jwt_encode(payload, SECRET_KEY, algorithm=ALGORITHM)

  1. Server calls send_confirmation_email(user, token), which calls send_email(schema); send_email must always pass attachments=[] or None→[].
  2. Frontend shows a confirmation alert and instructs the user to check the inbox.
  3. When user clicks the link (front page with ?verify_token=...), the frontend should call POST /auth/verify-confirmation-token with { token }
  4. Server decodes token, checks type, finds user, sets is_verified=True. Return success.

Password reset

  • Similar pattern but type: "reset". Reset endpoint should accept token + new_password JSON body and update password after validating token and password policy.

 

10. Add a new module (step-by-step)

Backend

  1. Add model in core/models.py.
  2. Add Pydantic schemas in core/schemas.py (XCreate, XUpdate, XOut).
  3. Implement core/util/x.py functions (list, get, create, update, delete). Enforce owner checks inside update & delete.
  4. Create /routers/x.py with endpoints and include in main.py.
  5. Use Alembic migration.

Frontend

  1. Add frontend/templates/components/x.html with placeholders and DOM IDs.
  2. Add frontend/assets/scripts/managers/xManager.js implementing init().
  3. Add XApi methods in utils/api.js.
  4. Wire up main.js to instantiate new XManager({...deps...}).
  5. Use Alerts, Confirm, ModalManager, UI helpers.

 

11. Tests, migrations & deployment

Migrations: use Alembic for DB schema changes. Add migration whenever models change.

Tests:

  • API tests for auth flows (register → verify → login).
  • Integration tests for DB operations (create/update/ownership).

Deployment:

  • Set MODE = "PROD".
  • Ensure JWT_SECRET and mail creds are in environment variables.
  • TLS (HTTPS) required for cookies secure=True.
  • Consider applying CSP header and HSTS.

 

12. Useful snippets & gotchas

send_email attachments fix (FastMail)

async def send_email(data: schemas.EmailRequest):
    try:
        attachments = [data.attachment] if data.attachment else []
        message = MessageSchema(
            subject=data.subject,
            recipients=[data.recipient or settings.ADMIN_EMAIL],
            body=data.body,
            subtype=MessageType.html,
            attachments=attachments,
        )
        fm = FastMail(conf_email)
        await fm.send_message(message)
        return {"message": "Email sent successfully"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to send email: {e}")

Verify token route: accept JSON body (avoid 422 missing query param)

@router.post("/verify-confirmation-token")
def verify_email(payload: dict, db: Session = Depends(get_db)):
    token = payload.get("token")
    if not token:
        raise HTTPException(status_code=400, detail="token is required")
    verified = auth_service.verify_confirmation_token(db, token)
    return {"success": verified}

(Or better: use a Pydantic schema: class TokenPayload(BaseModel): token: str.)

Frontend: call POST with JSON

AuthApi.verifyConfirmationToken = (formData) =>
  request(`${API.authApi}/verify-confirmation-token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(formData),
    credentials: "include"
  });

UiHelpers.validateUsername should be sync or return a Promise (if consult remote), but frontend code should await optionally:

let validationResult = UiHelpers.validateUsername(username);
if (validationResult instanceof Promise) validationResult = await validationResult;

 

13. Pre-deployment steps

Security

  • Add DB-level unique indexes on users.email and users.username (plus proper migration).
  • Implement refresh-token rotation and revocation table; ensure logout invalidates refresh token.
  • Consider migration from stateless → stateful refresh tokens
  • XSS protection
    • Server side
      • Sanitize user input
      • Encode output (escape HTML)
      • Consider strict Content Security Policy (CSP) if feasible
    • Client side
      • No dangerous innerHTML
      • Use textContent
      • Validate project names / descriptions
    • RegExp whitelist for names
    • API rejects unsafe strings
  • Rate limits: login/register/reset.
  • Brute-force protections (including cloudflare)
  • XSS protections
  • Honey pots

 

14. TODOs & recommended follow-ups

  • Implement UX/UI accessibility to a reasonable level.
  • Before deployment, harden app security measures (see step 13).
  • Implement focus-trap inside modals (for accessibility).
  • Consider adding a small E2E tests.
  • Responsive email templates
  • Forms: If validation fails, focus the first invalid field
    Note: because you auto-remove the .invalid class, use setTimeout(() => field.focus())

 

Download the project