Skip to content

Latest commit

 

History

History
242 lines (217 loc) · 11.3 KB

File metadata and controls

242 lines (217 loc) · 11.3 KB

Sinau LMS — Project Structure

Version: 0.1.0-draft

Repository Layout

sinau-lms/
├── docs/                          # Planning documents
│   ├── PRD.md                     # Product Requirements Document
│   ├── ERD.md                     # Entity Relationship Diagram
│   ├── ROADMAP.md                 # Development roadmap & sprints
│   ├── ARCHITECTURE.md            # This file
│   └── images/                    # Diagrams and visuals
│
├── server/                        # Rust backend (Axum 0.8)
│   ├── Cargo.toml                 # Rust dependencies
│   ├── sqlx-data.json            # sqlx query data (auto-generated)
│   ├── .sqlx/                     # Offline mode query metadata
│   ├── migrations/                # sqlx migrations (SQLite first)
│   │   ├── 001_create_users.sql
│   │   ├── 002_create_organizations.sql
│   │   ├── 003_create_courses.sql
│   │   ├── 004_create_chapters.sql
│   │   ├── 005_create_activities.sql
│   │   ├── 006_create_media.sql
│   │   ├── 007_create_api_tokens.sql
│   │   └── 008_create_webhooks.sql
│   └── src/
│       ├── main.rs                # Entry point: server startup
│       ├── config.rs              # ENV-based configuration
│       ├── db.rs                  # sqlx pool abstraction
│       ├── errors.rs              # Unified AppError enum
│       ├── routes/
│       │   ├── mod.rs             # Route aggregation
│       │   ├── auth.rs            # OAuth login/callback/logout/me
│       │   ├── users.rs           # User CRUD
│       │   ├── courses.rs        # Course CRUD + meta + clone
│       │   ├── chapters.rs       # Chapter CRUD + reorder
│       │   ├── activities.rs      # Activity CRUD
│       │   ├── organizations.rs  # Org CRUD
│       │   ├── media.rs           # File upload
│       │   ├── api_tokens.rs      # Token management
│       │   ├── webhooks.rs        # Webhook management
│       │   └── health.rs         # GET /api/health
│       ├── services/
│       │   ├── mod.rs
│       │   ├── auth/
│       │   │   ├── mod.rs
│       │   │   ├── config.rs      # ProviderConfig, AuthConfig
│       │   │   ├── oauth.rs       # exchange_code, fetch_identity
│       │   │   ├── session.rs     # JWT issue/validate
│       │   │   └── middleware.rs  # Axum auth extractors
│       │   ├── users.rs           # find_or_create_user
│       │   ├── courses.rs         # Course business logic
│       │   ├── media.rs           # Storage trait impl (local + S3)
│       │   └── webhooks.rs       # Event dispatch
│       └── models/                # sqlx row structs (not ORM)
│           ├── mod.rs
│           ├── user.rs
│           ├── organization.rs
│           ├── course.rs
│           ├── chapter.rs
│           ├── activity.rs
│           ├── media.rs
│           ├── api_token.rs
│           └── webhook.rs
│
├── web/                           # SvelteKit 5 frontend
│   ├── package.json
│   ├── svelte.config.js
│   ├── tailwind.config.ts
│   ├── vite.config.ts
│   ├── src/
│   │   ├── app.html
│   │   ├── app.d.ts
│   │   ├── lib/
│   │   │   ├── components/       # Shared UI components
│   │   │   │   ├── ui/            # shadcn-svelte components
│   │   │   │   ├── Editor.svelte  # TipTap wrapper
│   │   │   │   ├── CourseCard.svelte
│   │   │   │   ├── ChapterList.svelte
│   │   │   │   └── ActivityView.svelte
│   │   │   ├── api/              # API client functions
│   │   │   │   ├── client.ts      # Base fetch wrapper
│   │   │   │   ├── auth.ts
│   │   │   │   ├── courses.ts
│   │   │   │   ├── chapters.ts
│   │   │   │   ├── activities.ts
│   │   │   │   └── media.ts
│   │   │   ├── stores/
│   │   │   │   └── auth.ts        # User session store
│   │   │   └── utils/
│   │   │       └── cn.ts           # Tailwind class merge
│   │   └── routes/
│   │       ├── +layout.ts         # Root layout (auth check)
│   │       ├── +page.svelte       # Home / redirect
│   │       ├── login/
│   │       │   └── +page.svelte
│   │       ├── (dashboard)/
│   │       │   ├── +layout.svelte  # Auth required
│   │       │   ├── courses/
│   │       │   │   ├── +page.svelte        # Course list
│   │       │   │   ├── new/+page.svelte     # Create course
│   │       │   │   └── [uuid]/
│   │       │   │       ├── +page.svelte      # Edit course
│   │       │   │       └── chapters/
│   │       │   │           └── [id]/
│   │       │   │               └── +page.svelte  # Edit chapter
│   │       │   ├── users/
│   │       │   │   └── +page.svelte        # User management
│   │       │   ├── settings/
│   │       │   │   └── +page.svelte        # Org settings, webhooks
│   │       │   └── tokens/
│   │       │       └── +page.svelte        # API token management
│   │       └── course/
│   │           └── [uuid]/
│   │               ├── +page.svelte              # Public course view
│   │               └── chapter/
│   │                   └── [id]/
│   │                       ├── +page.svelte       # Chapter view
│   │                       └── activity/
│   │                           └── [activityId]/
│   │                               └── +page.svelte  # Activity view
│   └── static/
│       └── favicon.png
│
├── docker-compose.yml             # Dev environment
├── docker-compose.prod.yml       # Production environment
├── Dockerfile.server             # Multi-stage Rust build
├── Dockerfile.web                # Multi-stage Node build
├── .env.example                  # Environment variables template
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── README.md
└── LICENSE

Key Architectural Decisions

1. Why sqlx over SeaORM?

  • Compile-time SQL validation — sqlx checks queries at compile time with sqlx::query!(). No runtime ORM surprises.
  • Explicit SQL — Full control over queries. No hidden N+1, no magic.
  • Lightweight — Less abstraction = less magic = less debugging.
  • SQLite + Postgres — sqlx supports both via feature flags. SeaORM too, but sqlx is simpler.

2. Why Copy+Adapt (not Fork)?

  • Full ownership — No upstream merge conflicts, no tracking upstream changes.
  • Custom JWT claims — Different user model, different org structure.
  • Clean history — Sinau's git history is purely its own.

3. Why Denormalized FKs?

  • activities.course_id and activities.chapter_id are denormalized.
  • Avoids JOINs for common queries (e.g., "all activities in course X").
  • Trade-off: Slightly more writes on create/delete. Acceptable for LMS read-heavy workload.

4. Why UUID v7?

  • Time-sortable (unlike UUID v4).
  • Decentralized (no sequence coordination like auto-increment).
  • Works identically on SQLite and PostgreSQL.

5. Storage Abstraction (Local + S3)

The storage layer uses a trait-based backend so files can live on local filesystem OR any S3-compatible endpoint:

  • Local: files saved to UPLOAD_DIR, served via Axum static files
  • S3: files uploaded to S3-compatible bucket (MinIO, AWS S3, Cloudflare R2, etc.)

ENV toggle: STORAGE_BACKEND=local (default) or STORAGE_BACKEND=s3

Default infra: MinIO at s3.ajianaz.dev with dedicated sinau bucket.

File path convention: {org_slug}/{type}/{uuid}.{ext}

Implementation pattern:

#[async_trait]
pub trait StorageBackend: Send + Sync {
    async fn upload(&self, key: &str, data: bytes::Bytes, content_type: &str) -> Result<String>;
    async fn delete(&self, key: &str) -> Result<()>;
    async fn get_url(&self, key: &str) -> String;
    async fn exists(&self, key: &str) -> Result<bool>;
}

pub struct LocalStorage { base_dir: PathBuf, base_url: String }
pub struct S3Storage { client: S3Client, bucket: String, public_url: String }

Config.rs returns Box based on STORAGE_BACKEND env.

Data Flow

Browser (SvelteKit SSR)
  │
  ├── GET /course/:uuid (SSR) ──> Axum API ──> SQLite/Postgres
  │                                              │
  └── POST /api/courses ──────> Axum API ──────> SQLite/Postgres
                                              │
OAuth Flow:
  Browser ──> GET /api/auth/google/login ──> Redirect to Google
  Google ──> GET /api/auth/google/callback ──> exchange_code()
                                              ──> fetch_identity()
                                              ──> find_or_create_user()
                                              ──> issue_jwt()
  Browser <── 302 + Set-Cookie (session)

Configuration (Environment Variables)

Variable Required Default Description
APP_URL Yes Base URL (e.g., https://sinau.example.com)
APP_SECRET Yes JWT signing secret + cookie encryption
DATABASE_URL Yes sqlite:./data/sinau.db or postgres://...
ADMIN_EMAILS No Comma-separated emails auto-promoted to admin
SECURE_COOKIE No false Set true for HTTPS (Secure; SameSite=None)
STORAGE_BACKEND No local Storage backend: local or s3
UPLOAD_DIR No ./uploads Local filesystem upload directory
UPLOAD_MAX_SIZE_MB No 10 Max file upload size in MB
S3_ENDPOINT No S3-compatible endpoint (e.g. MinIO, AWS S3, Cloudflare R2)
S3_BUCKET No sinau S3 bucket name
S3_ACCESS_KEY No S3 access key
S3_SECRET_KEY No S3 secret key
S3_REGION No us-east-1 S3 region
S3_PUBLIC_URL No Public URL base for file access
OAUTH_GOOGLE_CLIENT_ID No Google OAuth client ID
OAUTH_GOOGLE_CLIENT_SECRET No Google OAuth client secret
OAUTH_GITHUB_CLIENT_ID No GitHub OAuth client ID
OAUTH_GITHUB_CLIENT_SECRET No GitHub OAuth client secret
OAUTH_KEYCLOAK_CLIENT_ID No Keycloak OAuth client ID
OAUTH_KEYCLOAK_CLIENT_SECRET No Keycloak OAuth client secret
OAUTH_KEYCLOAK_AUTH_URL No Keycloak auth endpoint URL
OAUTH_KEYCLOAK_TOKEN_URL No Keycloak token endpoint URL
OAUTH_KEYCLOAK_USERINFO_URL No Keycloak userinfo endpoint URL
OAUTH_KEYCLOAK_REDIRECT_URI No Keycloak callback URL
CORS_ORIGINS No * Allowed CORS origins (comma-separated)