Skip to content

Fittable/backend

Repository files navigation

Fittable | Backend API

A production REST API for Fittable, a scheduling and workforce management platform for university student workers. The system integrates with the university's proprietary LMS (KLAS) via a reverse-engineered auth flow, syncs academic timetables, and generates official Korean-language PDF work documents.


Tech Stack

Python FastAPI PostgreSQL SQLAlchemy Alembic Pydantic Google Sheets Sentry Supabase Docker AWS EC2


System Architecture

graph TB
    subgraph client["Client"]
        FE["Next.js\nVercel"]
    end

    subgraph ec2["AWS EC2"]
        direction TB
        API["FastAPI\nAsync REST API"]
        AUTH["Auth Layer\nKLAS Session  +  JWT"]
        SVC["Service Layer\nshift · holiday · profile · timetable"]
        PDF["PDF Engine\nReportLab + NotoSansKR"]
        SS["Sheets Service\ngspread"]
    end

    subgraph storage["Supabase"]
        DB[("PostgreSQL\nasyncpg + pgbouncer")]
    end

    subgraph external["External Services"]
        KLAS["KLAS Portal\nUniversity LMS"]
        GS["Google Sheets\nSchedule Export"]
        GD["Google Drive\nFile Storage"]
        SNT["Sentry\nError Tracking"]
    end

    FE -- "HTTPS / REST" --> API
    API --> AUTH
    AUTH --> SVC
    SVC --> DB
    SVC --> PDF
    SVC --> SS
    AUTH -. "RSA login\nHTML scraping" .-> KLAS
    SS --> GS
    SS --> GD
    API -. "error events" .-> SNT

    style ec2 fill:#1a1a2e,stroke:#FF9900,color:#fff
    style storage fill:#1a1a2e,stroke:#3FCF8E,color:#fff
    style external fill:#1a1a2e,stroke:#888,color:#fff
    style client fill:#1a1a2e,stroke:#009688,color:#fff
Loading

Data Flows

1. KLAS Authentication Flow

KLAS has no public API. The login flow was reverse-engineered from the browser — RSA-encrypted credentials, multi-step handshake, and HTML scraping.

sequenceDiagram
    autonumber
    participant C as Client
    participant API as FastAPI
    participant KLAS as KLAS Portal
    participant DB as PostgreSQL

    C->>API: POST /api/auth/login<br/>{student_id, password}

    rect rgb(30, 40, 60)
        Note over API,KLAS: Step 1 — Establish browser-like session
        API->>KLAS: GET /LoginForm
        KLAS-->>API: Set-Cookie: SESSION
    end

    rect rgb(30, 40, 60)
        Note over API,KLAS: Step 2 — RSA key exchange
        API->>KLAS: POST /LoginSecurity
        KLAS-->>API: RSA public key (PEM)
        Note over API: Serialize {loginId, loginPwd}<br/>→ JSON → RSA-PKCS1v15 encrypt<br/>→ base64 encode
    end

    rect rgb(30, 40, 60)
        Note over API,KLAS: Step 3 — Lockout check + login
        API->>KLAS: POST /LoginCaptcha {encryptedToken}
        KLAS-->>API: failCount (abort if > 2)
        API->>KLAS: POST /LoginConfirm {encryptedToken}
        KLAS-->>API: {errorCount: 0} + authenticated SESSION cookie
    end

    rect rgb(30, 50, 40)
        Note over API,KLAS: Step 4 — Profile scrape
        API->>KLAS: GET /Profile page
        KLAS-->>API: HTML (name, major, DOB, gender, nationality)
        API->>KLAS: GET DID portal (token from iframe)
        KLAS-->>API: HTML with base64 profile image
        Note over API: Regex-parse all fields
    end

    API->>DB: UPSERT user (status: pending on first login)
    DB-->>API: user record

    Note over API: Store KLASService instance<br/>in memory keyed by token_urlsafe()<br/>Issue JWT for DB-backed routes
    API-->>C: {token: KLAS session,<br/>access_token: JWT}
Loading

2. Shift Management & Export Flow

%%{init: {'themeVariables': {'fontSize': '11px'}}}%%
flowchart TD
    A([Admin]) -->|JWT Bearer| B["POST /api/shifts\nPUT /api/shifts/{id}\nDELETE /api/shifts/{id}"]
    B --> C{"Validate\nCurrentUser dep"}
    C -->|401| ERR1[Unauthorized]
    C -->|approved / admin| D[shift_service]
    D --> E[("PostgreSQL")]

    E --> F["GET /api/shifts\n?start=&end=&user_id="]
    F --> G{Export type?}

    G -->|"GET /shifts/export/pdf\n?room=102|103|all"| H["pdf_schedule.py\nReportLab calendar grid\nper-room color highlights"]
    H --> I["Download\nwork_schedule_YYYY-MM-DD.pdf"]

    G -->|"POST /shifts/export/sheet"| J["schedule_sheet_service\ngspread + service account"]
    J --> K["Google Sheets\nCalendar grid format"]

    G -->|JSON| L["Frontend\nSchedule UI"]

    E --> M["GET /api/shifts/hours-summary\n?month=&year="]
    M --> N["Hours per user\n+ work status PDF"]

    style ERR1 fill:#5a1a1a,color:#fff
    style I fill:#1a3a1a,color:#fff
    style K fill:#1a3a2a,color:#fff
Loading

3. Timetable Sync Flow

%%{init: {'themeVariables': {'fontSize': '11px'}}}%%
graph TD
    A([Student]) -->|KLAS session token| B["GET /api/timetable\n?year=&semester="]
    B --> C{Session valid?}
    C -->|invalid| ERR["401 Expired\nLogin again"]
    C -->|valid| E["Resolve KLASService\nfrom in-memory store"]
    E --> F["POST KLAS /TimeTable API\n{searchYear, searchHakgi}"]
    F --> G["Raw JSON response\nwtSubj_N · wtTime · wtSpan_N"]
    G --> H["parse_timetable()\ntime index → HH:MM\nday index → Mon–Sat"]
    H --> I["UPSERT TimetableEntry rows\ninto PostgreSQL"]
    I --> J[("PostgreSQL\ntimetable_entries")]
    J --> K["Shift conflict detection\non scheduling"]

    style ERR fill:#5a1a1a,color:#fff
Loading

Data Model

%%{init: {'themeVariables': {'fontSize': '11px'}}}%%
erDiagram
    USER {
        uuid id PK
        string student_id UK
        string name
        string major
        string room_no
        string work_category
        string role "admin or worker"
        string status "pending · approved · active"
    }
    SHIFT {
        uuid id PK
        uuid user_id FK
        uuid created_by FK
        date date
        time start_time
        time end_time
        string note
        datetime updated_at
    }
    TIMETABLE_ENTRY {
        uuid id PK
        uuid user_id FK
        string course_code
        string course_title
        string day
        int day_num "0=Mon … 5=Sat"
        time start_time
        time end_time
        string location
        string professor
        int year
        string semester "1 or 2"
    }
    HOLIDAY {
        uuid id PK
        date date UK
        string name
        string local_name
        string type "Public holiday · Observance"
        string local_type "공휴일 · 기념일"
        string source "google · manual"
    }

    USER ||--o{ SHIFT : "assigned to"
    USER ||--o{ SHIFT : "created by"
    USER ||--o{ TIMETABLE_ENTRY : "has"
Loading

API Surface

All routes are prefixed /api:

Prefix Description
/auth KLAS login/logout, /me current user
/timetable Proxy to KLAS timetable + university academic calendar
/shifts CRUD, hours summary, PDF export, Google Sheets sync
/profile Profile read/update, room assignment
/users Admin: user listing, approval workflow
/holidays Holiday CRUD (sourced from Google Calendar or manual)
/work-log Korean work log PDF (교비근로 근무일지)

Interactive docs: /docs (Swagger dark theme) · /redoc


Architecture Notes

Async end-to-end — FastAPI with async def handlers, SQLAlchemy 2.0 async engine, asyncpg driver. pgbouncer compatibility is handled at the engine level (statement_cache_size=0, query param stripped at startup) and mirrored in Alembic's migration engine.

Dual auth — KLAS session token (in-memory, holds a live requests.Session) for portal-proxying endpoints; JWT (Bearer or httpOnly cookie) for all database-backed routes. Both surface as typed dependency annotations (CurrentUser, CurrentUserFromKlas, AdminUser).

Korean PDF fonts — Font loading resolves: PDF_FONT_PATH env var → locally cached file → auto-download from Google Fonts GitHub (Noto Sans KR, OFL license). Clean EC2 deploys get a working font without committing binary files.

Admin approval gate — Users are created on first KLAS login with status=pending. Shifts can only be assigned to approved/active workers.


Local Setup

uv venv && source .venv/bin/activate
uv pip install -r requirements.txt
cp .env.example .env   # fill in DATABASE_URL, JWT_SECRET, KLAS_* URLs
alembic upgrade head
uvicorn app.main:app --reload

DATABASE_URL uses the Supabase connection string with the postgresql+asyncpg:// scheme (Session mode pooler URL for pgbouncer compatibility).

Variable Description
DATABASE_URL postgresql+asyncpg://user:pass@host/db
JWT_SECRET JWT signing secret
KLAS_* University portal endpoint URLs (7 vars)
ADMIN_STUDENT_ID Student ID granted admin role on first login
GOOGLE_SERVICE_ACCOUNT_FILE Path to GCP service account JSON
PDF_FONT_PATH NotoSansKR-Regular.ttf path (optional, auto-downloaded)
SENTRY_DSN Sentry DSN (optional)

Health check: GET /health — verifies DB connectivity.

About

Work schedule management API for university student office assistants.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages