From a05ff346f19d588657ec71a325f21ed51c4b7d0d Mon Sep 17 00:00:00 2001 From: vikingowl Date: Tue, 16 Dec 2025 15:35:08 +0100 Subject: [PATCH] chore: init owlibou-ttrpg monorepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Rust API scaffold (axum + PostgreSQL + MinIO) - Add SvelteKit frontend scaffold (Tailwind CSS) - Add docker-compose for local development - Include models, routes, services, WebSocket structure 🤖 Generated with Claude Code --- .env.example | 8 +++ .gitignore | 37 +++++++++++ LICENSE | 17 ++++++ README.md | 78 ++++++++++++++++++++++++ api/.env.example | 25 ++++++++ api/Cargo.toml | 41 +++++++++++++ api/Dockerfile | 24 ++++++++ api/migrations/.gitkeep | 0 api/src/auth/.gitkeep | 0 api/src/auth/jwt.rs | 2 + api/src/auth/magic_link.rs | 2 + api/src/auth/mod.rs | 7 +++ api/src/auth/oauth.rs | 2 + api/src/auth/password.rs | 2 + api/src/config.rs | 29 +++++++++ api/src/db/.gitkeep | 0 api/src/db/mod.rs | 14 +++++ api/src/error.rs | 51 ++++++++++++++++ api/src/main.rs | 76 +++++++++++++++++++++++ api/src/models/.gitkeep | 0 api/src/models/campaign.rs | 29 +++++++++ api/src/models/file.rs | 25 ++++++++ api/src/models/mod.rs | 7 +++ api/src/models/note.rs | 38 ++++++++++++ api/src/models/session.rs | 29 +++++++++ api/src/models/user.rs | 30 +++++++++ api/src/routes/.gitkeep | 0 api/src/routes/auth.rs | 11 ++++ api/src/routes/campaigns.rs | 10 +++ api/src/routes/files.rs | 8 +++ api/src/routes/mod.rs | 7 +++ api/src/routes/notes.rs | 9 +++ api/src/routes/sessions.rs | 8 +++ api/src/services/.gitkeep | 0 api/src/services/encryption.rs | 6 ++ api/src/services/invite.rs | 6 ++ api/src/services/mod.rs | 5 ++ api/src/services/storage.rs | 7 +++ api/src/ws/.gitkeep | 0 api/src/ws/handler.rs | 7 +++ api/src/ws/messages.rs | 21 +++++++ api/src/ws/mod.rs | 4 ++ docker-compose.yml | 59 ++++++++++++++++++ docs/.gitkeep | 0 suite/.env.example | 3 + suite/Dockerfile | 26 ++++++++ suite/package.json | 34 +++++++++++ suite/postcss.config.js | 6 ++ suite/src/app.css | 12 ++++ suite/src/app.html | 13 ++++ suite/src/lib/api/.gitkeep | 0 suite/src/lib/api/client.ts | 61 ++++++++++++++++++ suite/src/lib/components/layout/.gitkeep | 0 suite/src/lib/components/ui/.gitkeep | 0 suite/src/lib/stores/.gitkeep | 0 suite/src/lib/stores/auth.ts | 47 ++++++++++++++ suite/src/lib/stores/websocket.ts | 48 +++++++++++++++ suite/src/lib/utils/.gitkeep | 0 suite/src/routes/+layout.svelte | 28 +++++++++ suite/src/routes/+page.svelte | 49 +++++++++++++++ suite/src/routes/.gitkeep | 0 suite/src/routes/notes/+page.svelte | 29 +++++++++ suite/svelte.config.js | 13 ++++ suite/tailwind.config.js | 24 ++++++++ suite/tsconfig.json | 14 +++++ suite/vite.config.ts | 6 ++ 66 files changed, 1154 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/.env.example create mode 100644 api/Cargo.toml create mode 100644 api/Dockerfile create mode 100644 api/migrations/.gitkeep create mode 100644 api/src/auth/.gitkeep create mode 100644 api/src/auth/jwt.rs create mode 100644 api/src/auth/magic_link.rs create mode 100644 api/src/auth/mod.rs create mode 100644 api/src/auth/oauth.rs create mode 100644 api/src/auth/password.rs create mode 100644 api/src/config.rs create mode 100644 api/src/db/.gitkeep create mode 100644 api/src/db/mod.rs create mode 100644 api/src/error.rs create mode 100644 api/src/main.rs create mode 100644 api/src/models/.gitkeep create mode 100644 api/src/models/campaign.rs create mode 100644 api/src/models/file.rs create mode 100644 api/src/models/mod.rs create mode 100644 api/src/models/note.rs create mode 100644 api/src/models/session.rs create mode 100644 api/src/models/user.rs create mode 100644 api/src/routes/.gitkeep create mode 100644 api/src/routes/auth.rs create mode 100644 api/src/routes/campaigns.rs create mode 100644 api/src/routes/files.rs create mode 100644 api/src/routes/mod.rs create mode 100644 api/src/routes/notes.rs create mode 100644 api/src/routes/sessions.rs create mode 100644 api/src/services/.gitkeep create mode 100644 api/src/services/encryption.rs create mode 100644 api/src/services/invite.rs create mode 100644 api/src/services/mod.rs create mode 100644 api/src/services/storage.rs create mode 100644 api/src/ws/.gitkeep create mode 100644 api/src/ws/handler.rs create mode 100644 api/src/ws/messages.rs create mode 100644 api/src/ws/mod.rs create mode 100644 docker-compose.yml create mode 100644 docs/.gitkeep create mode 100644 suite/.env.example create mode 100644 suite/Dockerfile create mode 100644 suite/package.json create mode 100644 suite/postcss.config.js create mode 100644 suite/src/app.css create mode 100644 suite/src/app.html create mode 100644 suite/src/lib/api/.gitkeep create mode 100644 suite/src/lib/api/client.ts create mode 100644 suite/src/lib/components/layout/.gitkeep create mode 100644 suite/src/lib/components/ui/.gitkeep create mode 100644 suite/src/lib/stores/.gitkeep create mode 100644 suite/src/lib/stores/auth.ts create mode 100644 suite/src/lib/stores/websocket.ts create mode 100644 suite/src/lib/utils/.gitkeep create mode 100644 suite/src/routes/+layout.svelte create mode 100644 suite/src/routes/+page.svelte create mode 100644 suite/src/routes/.gitkeep create mode 100644 suite/src/routes/notes/+page.svelte create mode 100644 suite/svelte.config.js create mode 100644 suite/tailwind.config.js create mode 100644 suite/tsconfig.json create mode 100644 suite/vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a623aaa --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Database +POSTGRES_USER=owl +POSTGRES_PASSWORD=owl +POSTGRES_DB=owlibou + +# MinIO +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea22ab6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Rust +api/target/ + +# Node +suite/node_modules/ +suite/.svelte-kit/ +suite/build/ + +# Environment +.env +**/.env +!**/.env.example + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Test coverage +coverage/ +*.lcov + +# Build artifacts +dist/ +*.exe +*.dll +*.so +*.dylib diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ded6ba5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2024 s0wlz x vikingowl + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..2658684 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Owlibou TTRPG + +Collaborative TTRPG platform with session notes, campaign management, and character tools. + +## Quick Start + +```bash +# Start all services (API, frontend, database, MinIO) +docker-compose up -d + +# Or run individually for development: + +# API (Rust) +cd api && cargo run + +# Frontend (SvelteKit) +cd suite && pnpm install && pnpm dev +``` + +## Architecture + +``` +owlibou-ttrpg/ +├── api/ # Rust backend (axum + PostgreSQL) +├── suite/ # SvelteKit frontend (Tailwind) +└── docs/ # Architecture documentation +``` + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Backend | Rust + axum + tokio | +| Database | PostgreSQL + pgcrypto | +| Real-time | WebSockets | +| Auth | JWT + OAuth2 | +| File Storage | MinIO (S3-compatible) | +| Frontend | SvelteKit + Tailwind | + +## Development + +### Prerequisites + +- Rust 1.75+ +- Node.js 20+ & pnpm +- Docker & Docker Compose +- PostgreSQL 16 (or use Docker) + +### Environment Setup + +```bash +# Copy environment files +cp .env.example .env +cp api/.env.example api/.env +cp suite/.env.example suite/.env + +# Start database and MinIO +docker-compose up -d db minio + +# Run migrations +cd api && sqlx migrate run + +# Start API +cd api && cargo watch -x run + +# Start frontend (new terminal) +cd suite && pnpm dev +``` + +### URLs + +- **Frontend:** http://localhost:5173 +- **API:** http://localhost:3000 +- **MinIO Console:** http://localhost:9001 + +## License + +AGPLv3 diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 0000000..3396aa3 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,25 @@ +# Database +DATABASE_URL=postgres://owl:owl@localhost:5432/owlibou + +# JWT +JWT_SECRET=change-me-in-production +JWT_EXPIRY_HOURS=24 + +# MinIO/S3 +MINIO_ENDPOINT=localhost:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET=owlibou + +# OAuth (optional) +OAUTH_GOOGLE_CLIENT_ID= +OAUTH_GOOGLE_CLIENT_SECRET= +OAUTH_GITHUB_CLIENT_ID= +OAUTH_GITHUB_CLIENT_SECRET= +OAUTH_DISCORD_CLIENT_ID= +OAUTH_DISCORD_CLIENT_SECRET= + +# Server +HOST=0.0.0.0 +PORT=3000 +RUST_LOG=info diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000..d8039a1 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "owlibou-api" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0" +description = "Backend API for Owlibou TTRPG platform" + +[dependencies] +# Web framework +axum = { version = "0.7", features = ["ws", "multipart"] } +tokio = { version = "1", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } + +# Database +sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] } + +# Auth +argon2 = "0.5" +jsonwebtoken = "9" +oauth2 = "4" + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Utils +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1" +anyhow = "1" +dotenvy = "0.15" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# S3/MinIO +aws-sdk-s3 = "1" +aws-config = "1" + +[dev-dependencies] +tokio-test = "0.4" diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..3a4d2b5 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,24 @@ +FROM rust:1.75-alpine AS builder + +RUN apk add --no-cache musl-dev openssl-dev pkgconfig + +WORKDIR /app +COPY Cargo.toml Cargo.lock ./ +COPY src ./src + +RUN cargo build --release + +FROM alpine:3.19 + +RUN apk add --no-cache ca-certificates + +WORKDIR /app +COPY --from=builder /app/target/release/owlibou-api . +COPY migrations ./migrations + +ENV HOST=0.0.0.0 +ENV PORT=3000 + +EXPOSE 3000 + +CMD ["./owlibou-api"] diff --git a/api/migrations/.gitkeep b/api/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/auth/.gitkeep b/api/src/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/auth/jwt.rs b/api/src/auth/jwt.rs new file mode 100644 index 0000000..f3ece9d --- /dev/null +++ b/api/src/auth/jwt.rs @@ -0,0 +1,2 @@ +// JWT token handling +// TODO: Implement JWT generation and validation diff --git a/api/src/auth/magic_link.rs b/api/src/auth/magic_link.rs new file mode 100644 index 0000000..d8c00d8 --- /dev/null +++ b/api/src/auth/magic_link.rs @@ -0,0 +1,2 @@ +// Magic link (passwordless) authentication +// TODO: Implement magic link generation and verification diff --git a/api/src/auth/mod.rs b/api/src/auth/mod.rs new file mode 100644 index 0000000..b0aaea9 --- /dev/null +++ b/api/src/auth/mod.rs @@ -0,0 +1,7 @@ +// Auth module +// TODO: Implement authentication + +pub mod jwt; +pub mod password; +pub mod magic_link; +pub mod oauth; diff --git a/api/src/auth/oauth.rs b/api/src/auth/oauth.rs new file mode 100644 index 0000000..ccb9404 --- /dev/null +++ b/api/src/auth/oauth.rs @@ -0,0 +1,2 @@ +// OAuth2 providers (Google, GitHub, Discord) +// TODO: Implement OAuth2 flows diff --git a/api/src/auth/password.rs b/api/src/auth/password.rs new file mode 100644 index 0000000..069bfd0 --- /dev/null +++ b/api/src/auth/password.rs @@ -0,0 +1,2 @@ +// Password hashing with Argon2 +// TODO: Implement password hashing and verification diff --git a/api/src/config.rs b/api/src/config.rs new file mode 100644 index 0000000..270548b --- /dev/null +++ b/api/src/config.rs @@ -0,0 +1,29 @@ +use std::env; + +#[derive(Debug, Clone)] +pub struct Config { + pub database_url: String, + pub jwt_secret: String, + pub jwt_expiry_hours: i64, + pub minio_endpoint: String, + pub minio_access_key: String, + pub minio_secret_key: String, + pub minio_bucket: String, +} + +impl Config { + pub fn from_env() -> anyhow::Result { + Ok(Self { + database_url: env::var("DATABASE_URL")?, + jwt_secret: env::var("JWT_SECRET")?, + jwt_expiry_hours: env::var("JWT_EXPIRY_HOURS") + .unwrap_or_else(|_| "24".to_string()) + .parse()?, + minio_endpoint: env::var("MINIO_ENDPOINT")?, + minio_access_key: env::var("MINIO_ACCESS_KEY")?, + minio_secret_key: env::var("MINIO_SECRET_KEY")?, + minio_bucket: env::var("MINIO_BUCKET") + .unwrap_or_else(|_| "owlibou".to_string()), + }) + } +} diff --git a/api/src/db/.gitkeep b/api/src/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/db/mod.rs b/api/src/db/mod.rs new file mode 100644 index 0000000..15c5786 --- /dev/null +++ b/api/src/db/mod.rs @@ -0,0 +1,14 @@ +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; +use std::env; + +pub async fn create_pool() -> anyhow::Result { + let database_url = env::var("DATABASE_URL")?; + + let pool = PgPoolOptions::new() + .max_connections(10) + .connect(&database_url) + .await?; + + Ok(pool) +} diff --git a/api/src/error.rs b/api/src/error.rs new file mode 100644 index 0000000..65d7321 --- /dev/null +++ b/api/src/error.rs @@ -0,0 +1,51 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde::Serialize; + +#[derive(Debug)] +pub enum AppError { + NotFound(String), + BadRequest(String), + Unauthorized(String), + Forbidden(String), + Internal(String), + Database(sqlx::Error), +} + +#[derive(Serialize)] +struct ErrorResponse { + error: String, + message: String, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, error_type, message) = match self { + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg), + AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg), + AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "unauthorized", msg), + AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg), + AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "internal_error", msg), + AppError::Database(e) => { + tracing::error!("Database error: {:?}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "database_error", "A database error occurred".to_string()) + } + }; + + let body = Json(ErrorResponse { + error: error_type.to_string(), + message, + }); + + (status, body).into_response() + } +} + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + AppError::Database(err) + } +} diff --git a/api/src/main.rs b/api/src/main.rs new file mode 100644 index 0000000..b8de706 --- /dev/null +++ b/api/src/main.rs @@ -0,0 +1,76 @@ +use axum::{ + routing::get, + Router, + Json, +}; +use serde::Serialize; +use std::net::SocketAddr; +use tower_http::cors::CorsLayer; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +mod config; +mod db; +mod error; +mod auth; +mod models; +mod routes; +mod services; +mod ws; + +#[derive(Serialize)] +struct HealthResponse { + status: &'static str, + version: &'static str, +} + +async fn health() -> Json { + Json(HealthResponse { + status: "ok", + version: env!("CARGO_PKG_VERSION"), + }) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Load .env file + dotenvy::dotenv().ok(); + + // Initialize tracing + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // TODO: Initialize database pool + // let pool = db::create_pool().await?; + + // TODO: Run migrations + // sqlx::migrate!().run(&pool).await?; + + // Build router + let app = Router::new() + .route("/health", get(health)) + // TODO: Add route modules + // .nest("/auth", routes::auth::router()) + // .nest("/campaigns", routes::campaigns::router()) + // .nest("/sessions", routes::sessions::router()) + // .nest("/notes", routes::notes::router()) + // .nest("/files", routes::files::router()) + .layer(CorsLayer::permissive()) + .layer(TraceLayer::new_for_http()); + + // Start server + let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let port: u16 = std::env::var("PORT") + .unwrap_or_else(|_| "3000".to_string()) + .parse()?; + + let addr = SocketAddr::new(host.parse()?, port); + tracing::info!("Starting server on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/api/src/models/.gitkeep b/api/src/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/models/campaign.rs b/api/src/models/campaign.rs new file mode 100644 index 0000000..261b55b --- /dev/null +++ b/api/src/models/campaign.rs @@ -0,0 +1,29 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Campaign { + pub id: Uuid, + pub owner_id: Uuid, + pub name: String, + pub description: Option, + pub ruleset: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateCampaign { + pub name: String, + pub description: Option, + pub ruleset: Option, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateCampaign { + pub name: Option, + pub description: Option, + pub ruleset: Option, +} diff --git a/api/src/models/file.rs b/api/src/models/file.rs new file mode 100644 index 0000000..3511c57 --- /dev/null +++ b/api/src/models/file.rs @@ -0,0 +1,25 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct File { + pub id: Uuid, + pub campaign_id: Uuid, + pub uploader_id: Uuid, + pub filename: String, + pub mime_type: String, + pub size_bytes: i64, + pub storage_key: String, + pub created_at: DateTime, +} + +#[derive(Debug, Serialize)] +pub struct FileUploadResponse { + pub id: Uuid, + pub filename: String, + pub mime_type: String, + pub size_bytes: i64, + pub url: String, +} diff --git a/api/src/models/mod.rs b/api/src/models/mod.rs new file mode 100644 index 0000000..aa6beda --- /dev/null +++ b/api/src/models/mod.rs @@ -0,0 +1,7 @@ +// Database models + +pub mod user; +pub mod campaign; +pub mod session; +pub mod note; +pub mod file; diff --git a/api/src/models/note.rs b/api/src/models/note.rs new file mode 100644 index 0000000..a223bb0 --- /dev/null +++ b/api/src/models/note.rs @@ -0,0 +1,38 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "note_visibility", rename_all = "lowercase")] +pub enum NoteVisibility { + Private, // Only owner and GM can see + Shared, // Visible to specified players + Public, // Visible to all campaign members +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Note { + pub id: Uuid, + pub session_id: Uuid, + pub author_id: Uuid, + pub title: String, + pub content: String, + pub visibility: NoteVisibility, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateNote { + pub title: String, + pub content: String, + pub visibility: Option, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateNote { + pub title: Option, + pub content: Option, + pub visibility: Option, +} diff --git a/api/src/models/session.rs b/api/src/models/session.rs new file mode 100644 index 0000000..87e7885 --- /dev/null +++ b/api/src/models/session.rs @@ -0,0 +1,29 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Session { + pub id: Uuid, + pub campaign_id: Uuid, + pub name: String, + pub description: Option, + pub session_date: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateSession { + pub name: String, + pub description: Option, + pub session_date: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateSession { + pub name: Option, + pub description: Option, + pub session_date: Option>, +} diff --git a/api/src/models/user.rs b/api/src/models/user.rs new file mode 100644 index 0000000..71a1973 --- /dev/null +++ b/api/src/models/user.rs @@ -0,0 +1,30 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct User { + pub id: Uuid, + pub email: String, + #[serde(skip_serializing)] + pub password_hash: Option, + pub display_name: String, + pub avatar_url: Option, + pub email_verified: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct CreateUser { + pub email: String, + pub password: Option, + pub display_name: String, +} + +#[derive(Debug, Deserialize)] +pub struct UpdateUser { + pub display_name: Option, + pub avatar_url: Option, +} diff --git a/api/src/routes/.gitkeep b/api/src/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/routes/auth.rs b/api/src/routes/auth.rs new file mode 100644 index 0000000..f4c8c40 --- /dev/null +++ b/api/src/routes/auth.rs @@ -0,0 +1,11 @@ +// Auth routes: /auth/* +// TODO: Implement auth endpoints + +// POST /auth/register +// POST /auth/login +// POST /auth/magic-link +// POST /auth/verify-magic-link +// GET /auth/oauth/:provider +// GET /auth/oauth/:provider/callback +// POST /auth/refresh +// POST /auth/logout diff --git a/api/src/routes/campaigns.rs b/api/src/routes/campaigns.rs new file mode 100644 index 0000000..7ae426e --- /dev/null +++ b/api/src/routes/campaigns.rs @@ -0,0 +1,10 @@ +// Campaign routes: /campaigns/* +// TODO: Implement campaign endpoints + +// GET /campaigns +// POST /campaigns +// GET /campaigns/:id +// PUT /campaigns/:id +// DELETE /campaigns/:id +// POST /campaigns/:id/invite +// GET /campaigns/:id/members diff --git a/api/src/routes/files.rs b/api/src/routes/files.rs new file mode 100644 index 0000000..eb497cd --- /dev/null +++ b/api/src/routes/files.rs @@ -0,0 +1,8 @@ +// File routes: /campaigns/:campaign_id/files/* +// TODO: Implement file endpoints + +// GET /campaigns/:campaign_id/files +// POST /campaigns/:campaign_id/files (multipart upload) +// GET /campaigns/:campaign_id/files/:id +// DELETE /campaigns/:campaign_id/files/:id +// GET /campaigns/:campaign_id/files/:id/download diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs new file mode 100644 index 0000000..44a489f --- /dev/null +++ b/api/src/routes/mod.rs @@ -0,0 +1,7 @@ +// HTTP route handlers + +pub mod auth; +pub mod campaigns; +pub mod sessions; +pub mod notes; +pub mod files; diff --git a/api/src/routes/notes.rs b/api/src/routes/notes.rs new file mode 100644 index 0000000..4b72a2a --- /dev/null +++ b/api/src/routes/notes.rs @@ -0,0 +1,9 @@ +// Note routes: /sessions/:session_id/notes/* +// TODO: Implement note endpoints + +// GET /sessions/:session_id/notes +// POST /sessions/:session_id/notes +// GET /sessions/:session_id/notes/:id +// PUT /sessions/:session_id/notes/:id +// DELETE /sessions/:session_id/notes/:id +// PUT /sessions/:session_id/notes/:id/visibility diff --git a/api/src/routes/sessions.rs b/api/src/routes/sessions.rs new file mode 100644 index 0000000..d4b96fb --- /dev/null +++ b/api/src/routes/sessions.rs @@ -0,0 +1,8 @@ +// Session routes: /campaigns/:campaign_id/sessions/* +// TODO: Implement session endpoints + +// GET /campaigns/:campaign_id/sessions +// POST /campaigns/:campaign_id/sessions +// GET /campaigns/:campaign_id/sessions/:id +// PUT /campaigns/:campaign_id/sessions/:id +// DELETE /campaigns/:campaign_id/sessions/:id diff --git a/api/src/services/.gitkeep b/api/src/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/services/encryption.rs b/api/src/services/encryption.rs new file mode 100644 index 0000000..207bf99 --- /dev/null +++ b/api/src/services/encryption.rs @@ -0,0 +1,6 @@ +// At-rest encryption helpers +// TODO: Implement encryption utilities + +// - Encrypt sensitive data before storage +// - Decrypt data on retrieval +// - Key management diff --git a/api/src/services/invite.rs b/api/src/services/invite.rs new file mode 100644 index 0000000..879466b --- /dev/null +++ b/api/src/services/invite.rs @@ -0,0 +1,6 @@ +// Player invite service +// TODO: Implement invite logic + +// - Generate invite tokens +// - Send invite emails +// - Validate and accept invites diff --git a/api/src/services/mod.rs b/api/src/services/mod.rs new file mode 100644 index 0000000..15a9e3e --- /dev/null +++ b/api/src/services/mod.rs @@ -0,0 +1,5 @@ +// Business logic services + +pub mod invite; +pub mod storage; +pub mod encryption; diff --git a/api/src/services/storage.rs b/api/src/services/storage.rs new file mode 100644 index 0000000..d418ec0 --- /dev/null +++ b/api/src/services/storage.rs @@ -0,0 +1,7 @@ +// MinIO/S3 storage service +// TODO: Implement file storage operations + +// - Upload files +// - Download files +// - Generate presigned URLs +// - Delete files diff --git a/api/src/ws/.gitkeep b/api/src/ws/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/api/src/ws/handler.rs b/api/src/ws/handler.rs new file mode 100644 index 0000000..edf0208 --- /dev/null +++ b/api/src/ws/handler.rs @@ -0,0 +1,7 @@ +// WebSocket connection handler +// TODO: Implement WebSocket handling + +// - Connection management +// - Authentication via token +// - Room/channel subscriptions (per session) +// - Broadcast messages diff --git a/api/src/ws/messages.rs b/api/src/ws/messages.rs new file mode 100644 index 0000000..150db77 --- /dev/null +++ b/api/src/ws/messages.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "type", content = "payload")] +pub enum WsMessage { + // Client -> Server + Subscribe { session_id: Uuid }, + Unsubscribe { session_id: Uuid }, + + // Server -> Client + NoteCreated { note_id: Uuid, session_id: Uuid }, + NoteUpdated { note_id: Uuid, session_id: Uuid }, + NoteDeleted { note_id: Uuid, session_id: Uuid }, + FileShared { file_id: Uuid, session_id: Uuid }, + PlayerJoined { user_id: Uuid, session_id: Uuid }, + PlayerLeft { user_id: Uuid, session_id: Uuid }, + + // Errors + Error { message: String }, +} diff --git a/api/src/ws/mod.rs b/api/src/ws/mod.rs new file mode 100644 index 0000000..a92b149 --- /dev/null +++ b/api/src/ws/mod.rs @@ -0,0 +1,4 @@ +// WebSocket module + +pub mod handler; +pub mod messages; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ddc557 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,59 @@ +services: + api: + build: ./api + ports: + - "3000:3000" + environment: + DATABASE_URL: postgres://owl:owl@db:5432/owlibou + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin + JWT_SECRET: change-me-in-production + RUST_LOG: info + depends_on: + db: + condition: service_healthy + minio: + condition: service_started + + suite: + build: ./suite + ports: + - "5173:5173" + environment: + PUBLIC_API_URL: http://localhost:3000 + PUBLIC_WS_URL: ws://localhost:3000/ws + depends_on: + - api + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: owl + POSTGRES_PASSWORD: owl + POSTGRES_DB: owlibou + volumes: + - pgdata:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U owl -d owlibou"] + interval: 5s + timeout: 5s + retries: 5 + + minio: + image: minio/minio + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin + volumes: + - miniodata:/data + ports: + - "9000:9000" + - "9001:9001" + +volumes: + pgdata: + miniodata: diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/.env.example b/suite/.env.example new file mode 100644 index 0000000..6bf3f5f --- /dev/null +++ b/suite/.env.example @@ -0,0 +1,3 @@ +# API +PUBLIC_API_URL=http://localhost:3000 +PUBLIC_WS_URL=ws://localhost:3000/ws diff --git a/suite/Dockerfile b/suite/Dockerfile new file mode 100644 index 0000000..5a20e42 --- /dev/null +++ b/suite/Dockerfile @@ -0,0 +1,26 @@ +FROM node:20-alpine AS builder + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +COPY . . +RUN pnpm build + +FROM node:20-alpine + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app +COPY --from=builder /app/build ./build +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/node_modules ./node_modules + +ENV HOST=0.0.0.0 +ENV PORT=5173 + +EXPOSE 5173 + +CMD ["node", "build"] diff --git a/suite/package.json b/suite/package.json new file mode 100644 index 0000000..b02ea13 --- /dev/null +++ b/suite/package.json @@ -0,0 +1,34 @@ +{ + "name": "owlibou-suite", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/node": "^20.0.0", + "autoprefixer": "^10.4.0", + "eslint": "^9.0.0", + "postcss": "^8.4.0", + "prettier": "^3.0.0", + "prettier-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0", + "svelte-check": "^3.0.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.0.0", + "vite": "^5.0.0" + }, + "dependencies": { + } +} diff --git a/suite/postcss.config.js b/suite/postcss.config.js new file mode 100644 index 0000000..0f77216 --- /dev/null +++ b/suite/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/suite/src/app.css b/suite/src/app.css new file mode 100644 index 0000000..f59318b --- /dev/null +++ b/suite/src/app.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --color-bg: theme('colors.gray.900'); + --color-text: theme('colors.gray.100'); +} + +body { + @apply bg-gray-900 text-gray-100 min-h-screen; +} diff --git a/suite/src/app.html b/suite/src/app.html new file mode 100644 index 0000000..116ab1f --- /dev/null +++ b/suite/src/app.html @@ -0,0 +1,13 @@ + + + + + + + Owlibou TTRPG + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/suite/src/lib/api/.gitkeep b/suite/src/lib/api/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/lib/api/client.ts b/suite/src/lib/api/client.ts new file mode 100644 index 0000000..2b61251 --- /dev/null +++ b/suite/src/lib/api/client.ts @@ -0,0 +1,61 @@ +import { PUBLIC_API_URL } from '$env/static/public'; +import { auth } from '$lib/stores/auth'; +import { get } from 'svelte/store'; + +interface RequestOptions { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body?: object; + headers?: Record; +} + +class ApiClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + private getAuthHeader(): Record { + const { token } = get(auth); + return token ? { Authorization: `Bearer ${token}` } : {}; + } + + async request(endpoint: string, options: RequestOptions = {}): Promise { + const { method = 'GET', body, headers = {} } = options; + + const response = await fetch(`${this.baseUrl}${endpoint}`, { + method, + headers: { + 'Content-Type': 'application/json', + ...this.getAuthHeader(), + ...headers + }, + body: body ? JSON.stringify(body) : undefined + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Request failed' })); + throw new Error(error.message || `HTTP ${response.status}`); + } + + return response.json(); + } + + get(endpoint: string) { + return this.request(endpoint, { method: 'GET' }); + } + + post(endpoint: string, body: object) { + return this.request(endpoint, { method: 'POST', body }); + } + + put(endpoint: string, body: object) { + return this.request(endpoint, { method: 'PUT', body }); + } + + delete(endpoint: string) { + return this.request(endpoint, { method: 'DELETE' }); + } +} + +export const api = new ApiClient(PUBLIC_API_URL); diff --git a/suite/src/lib/components/layout/.gitkeep b/suite/src/lib/components/layout/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/lib/components/ui/.gitkeep b/suite/src/lib/components/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/lib/stores/.gitkeep b/suite/src/lib/stores/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/lib/stores/auth.ts b/suite/src/lib/stores/auth.ts new file mode 100644 index 0000000..e120fd1 --- /dev/null +++ b/suite/src/lib/stores/auth.ts @@ -0,0 +1,47 @@ +import { writable } from 'svelte/store'; + +interface User { + id: string; + email: string; + displayName: string; + avatarUrl?: string; +} + +interface AuthState { + user: User | null; + token: string | null; + isAuthenticated: boolean; + isLoading: boolean; +} + +const initialState: AuthState = { + user: null, + token: null, + isAuthenticated: false, + isLoading: true +}; + +function createAuthStore() { + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + setUser: (user: User, token: string) => { + update((state) => ({ + ...state, + user, + token, + isAuthenticated: true, + isLoading: false + })); + }, + logout: () => { + set({ ...initialState, isLoading: false }); + }, + setLoading: (isLoading: boolean) => { + update((state) => ({ ...state, isLoading })); + } + }; +} + +export const auth = createAuthStore(); diff --git a/suite/src/lib/stores/websocket.ts b/suite/src/lib/stores/websocket.ts new file mode 100644 index 0000000..1d7bc3b --- /dev/null +++ b/suite/src/lib/stores/websocket.ts @@ -0,0 +1,48 @@ +import { writable } from 'svelte/store'; +import { PUBLIC_WS_URL } from '$env/static/public'; + +interface WsState { + connected: boolean; + socket: WebSocket | null; +} + +function createWebSocketStore() { + const { subscribe, set, update } = writable({ + connected: false, + socket: null + }); + + let socket: WebSocket | null = null; + + return { + subscribe, + connect: (token: string) => { + if (socket?.readyState === WebSocket.OPEN) return; + + socket = new WebSocket(`${PUBLIC_WS_URL}?token=${token}`); + + socket.onopen = () => { + update((state) => ({ ...state, connected: true, socket })); + }; + + socket.onclose = () => { + update((state) => ({ ...state, connected: false, socket: null })); + }; + + socket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + }, + disconnect: () => { + socket?.close(); + set({ connected: false, socket: null }); + }, + send: (message: object) => { + if (socket?.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(message)); + } + } + }; +} + +export const ws = createWebSocketStore(); diff --git a/suite/src/lib/utils/.gitkeep b/suite/src/lib/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/routes/+layout.svelte b/suite/src/routes/+layout.svelte new file mode 100644 index 0000000..6315308 --- /dev/null +++ b/suite/src/routes/+layout.svelte @@ -0,0 +1,28 @@ + + +
+
+ +
+ +
+ +
+ +
+
+ Owlibou TTRPG © s0wlz x vikingowl +
+
+
diff --git a/suite/src/routes/+page.svelte b/suite/src/routes/+page.svelte new file mode 100644 index 0000000..ae73f12 --- /dev/null +++ b/suite/src/routes/+page.svelte @@ -0,0 +1,49 @@ + + + + Owlibou TTRPG + + + diff --git a/suite/src/routes/.gitkeep b/suite/src/routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/suite/src/routes/notes/+page.svelte b/suite/src/routes/notes/+page.svelte new file mode 100644 index 0000000..0b6f165 --- /dev/null +++ b/suite/src/routes/notes/+page.svelte @@ -0,0 +1,29 @@ + + + + Session Notes | Owlibou TTRPG + + +
+
+

Session Notes

+ + New Adventure + +
+ +

+ Your adventures will appear here. Create one to get started! +

+ + +
+

No adventures yet.

+

Click "New Adventure" to create your first one.

+
+
diff --git a/suite/svelte.config.js b/suite/svelte.config.js new file mode 100644 index 0000000..f94eeef --- /dev/null +++ b/suite/svelte.config.js @@ -0,0 +1,13 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/suite/tailwind.config.js b/suite/tailwind.config.js new file mode 100644 index 0000000..5a22f32 --- /dev/null +++ b/suite/tailwind.config.js @@ -0,0 +1,24 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: { + colors: { + owl: { + 50: '#faf5ff', + 100: '#f3e8ff', + 200: '#e9d5ff', + 300: '#d8b4fe', + 400: '#c084fc', + 500: '#a855f7', + 600: '#9333ea', + 700: '#7e22ce', + 800: '#6b21a8', + 900: '#581c87', + 950: '#3b0764', + } + } + } + }, + plugins: [] +}; diff --git a/suite/tsconfig.json b/suite/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/suite/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/suite/vite.config.ts b/suite/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/suite/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});