Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ S3_BUCKET=programmerbar

REDIS_URL=redis://localhost:6379

WEB_REDIRECT_URL="http://localhost:5173"

ADMIN_KEY=foobar

IS_DEV="true"

AUTH_SECRET="foobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbazfoobarbaz"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
run: cargo install sqlx-cli --no-default-features --features postgres

- name: 🗄️ Run database migrations
run: pnpm --filter=api run-migrations --database-url postgres://postgres:postgres@localhost:5432/programmerbar
run: pnpm --filter=api db:migrate --database-url postgres://postgres:postgres@localhost:5432/programmerbar

- name: 📝 Prepare SQLx offline cache
run: |
Expand Down
37 changes: 29 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Architecture

### Monorepo Structure

- `apps/www/` - Main SvelteKit application (primary codebase)
- `apps/cms/` - Sanity headless CMS for content management
- `apps/api/` - Rust API backend with Axum web framework
- `internal/emails/` - React Email templates for notifications

### Technology Stack

- **Frontend**: SvelteKit 2.28 with Svelte 5, Tailwind CSS v4
- **Backend**: Rust with Axum web framework, Tokio async runtime
- **Database**: Cloudflare D1 (SQLite) with Drizzle ORM 0.44
Expand All @@ -24,6 +26,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **CMS**: Sanity v4 for product/content management

### Service Architecture

The application uses a service layer pattern with dependency injection via SvelteKit `locals`. All services are available in `app.d.ts`:

- `userService` - User CRUD, role management (board/normal)
Expand All @@ -40,9 +43,10 @@ The application uses a service layer pattern with dependency injection via Svelt
## Development Commands

### Setup

```bash
# Copy environment variables
cp apps/www/.dev.vars.example apps/www/.dev.vars
cp apps/www/.env.example apps/www/.env
# Fill in Resend API key, Feide OAuth credentials

# Install dependencies (Node.js and Rust)
Expand All @@ -56,9 +60,10 @@ pnpm dev
```

### Common Tasks

```bash
# Development
pnpm dev # Start all services (website: :5173, CMS: :3333, API: :3000)
pnpm dev # Start all services (website: :5173, CMS: :3333, API: :8000)

# Rust API Development
cd apps/api
Expand Down Expand Up @@ -88,6 +93,7 @@ pnpm preview # Local production preview
```

### Database Management

```bash
# Create user invitation (required for first login)
pnpm dlx tsx ./apps/www/scripts/add-invitation.ts "user@email.com"
Expand All @@ -99,16 +105,18 @@ pnpm dlx tsx ./apps/www/scripts/users.ts
## Database Schema (Drizzle ORM)

### Core Entities

- **users** - Authentication via Feide, roles (board/normal), beer credits
- **sessions** - Lucia auth sessions
- **events** - Event management with dates
- **shifts** - Volunteer shifts with user assignments and event relationships
- **shifts** - Volunteer shifts with user assignments and event relationships
- **groups** - User groups and memberships
- **invitations** - User invitation system with expiration
- **notifications** - User notification delivery
- **claimedCredits** - Beer credit transaction tracking

### Key Relationships

- Users → Shifts (many-to-many via assignments)
- Events → Shifts (one-to-many)
- Users → Groups (many-to-many via memberships)
Expand All @@ -117,23 +125,27 @@ pnpm dlx tsx ./apps/www/scripts/users.ts
## Authentication & Authorization

### Feide Integration

- **Provider**: Norwegian education federation SSO
- **Flow**: OAuth 2.0 with Arctic library
- **User Creation**: Automatic on first Feide login (requires invitation)

### Role-Based Access

- `normal` - Basic portal access, can volunteer for shifts
- `board` - Admin access to user management, event creation, reporting
- Route protection: `/portal/admin/*` requires board role

## Portal System

### User Portal (`/portal/`)

- Event browsing and shift volunteering
- Personal profile and notification management
- Beer credit tracking and claiming

### Admin Portal (`/portal/admin/`)

- User management (roles, credits, deletion)
- Event creation with shift scheduling
- Volunteer assignment and management
Expand All @@ -142,49 +154,58 @@ pnpm dlx tsx ./apps/www/scripts/users.ts
## Development Patterns

### Form Handling

Use SvelteKit's enhanced forms with `use:enhance` for AJAX submissions. Always call `invalidateAll()` after successful mutations to refresh data.

### Service Usage

Access services via `locals` in server actions/load functions:

```typescript
export const actions = {
default: async ({ locals, request }) => {
await locals.userService.updateUser(userId, data);
return { success: true };
}
default: async ({ locals, request }) => {
await locals.userService.updateUser(userId, data);
return { success: true };
}
};
```

### Database Migrations

- Schema changes: Edit `src/lib/db/schemas/index.ts`
- Generate migration: `pnpm db:generate`
- Apply locally: `pnpm db:migrate:local`
- Production migrations run automatically on deployment

### Email Templates

Email templates are in `internal/emails/src/templates/`. Use `EmailService` to send with proper rendering and delivery via Resend.

## Deployment

### Cloudflare Pages

- Automatic deployment on `main` branch merges
- Database migrations applied automatically
- Environment variables configured in Cloudflare dashboard
- Domain: `programmer.bar`

### Required Environment Variables

- `RESEND_API_KEY` - Email delivery service
- `FEIDE_CLIENT_ID` & `FEIDE_CLIENT_SECRET` - OAuth authentication
- Database credentials configured via Wrangler for D1 access

## Common Issues

### First-Time Setup

Users must have invitations created before they can log in via Feide. Use the invitation script after setting up the local environment.

### Service Dependencies

All services require database and auth initialization. Check `src/hooks.server.ts` for service dependency injection setup.

### Migration Failures
If migrations fail, check D1 database status in Cloudflare dashboard and ensure proper credentials in `drizzle.config.ts`.

If migrations fail, check D1 database status in Cloudflare dashboard and ensure proper credentials in `drizzle.config.ts`.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
1. Fill in env-variables

```bash
cp apps/www/.dev.vars.example .dev.vars
cp apps/www/.env.example .env
```

1. Fill in the empty variables
Expand All @@ -35,6 +35,7 @@

- Website will run on [http://localhost:5173](http://localhost:5173)
- Sanity will run on [http://localhost:3333](http://localhost:3333)
- API will run on [http://localhost:8000](http://localhost:8000)

### Add invitation

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE pending_application (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
feide_id TEXT NOT NULL,
created_at INTEGER NOT NULL
);

CREATE INDEX pending_application_email_idx ON pending_application (email);

CREATE INDEX pending_application_feide_id_idx ON pending_application (feide_id);

ALTER TABLE
"user"
ADD
can_refer boolean DEFAULT true NOT NULL;
8 changes: 4 additions & 4 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "cargo run",
"dev": "cargo watch -x run",
"build": "cargo build --release",
"start": "cargo run --release",
"test": "cargo test",
"lint": "cargo clippy -- -D warnings",
"lint:fix": "cargo clippy --fix --allow-dirty -- -D warnings",
"format": "cargo fmt",
"format:check": "cargo fmt --check",
"clean": "cargo clean",
"add-migration": "sqlx migrate add",
"run-migrations": "sqlx migrate run"
"clean": "cargo clean && rm -rf .turbo",
"db:generate": "sqlx migrate add",
"db:migrate": "sqlx migrate run"
}
}
20 changes: 19 additions & 1 deletion apps/api/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,34 @@ use oauth2::{ClientId, ClientSecret, RedirectUrl};

#[derive(Debug, Clone)]
pub struct Config {
/// Database connection URL
pub database_url: String,
/// Server port to run the application on
pub server_port: u16,
/// Feide OAuth2 client ID
pub feide_client_id: ClientId,
/// Feide OAuth2 client secret
pub feide_client_secret: ClientSecret,
/// Feide OAuth2 redirect URI
pub feide_redirect_uri: RedirectUrl,
/// Flag indicating if the application is running in development mode
pub is_dev: bool,
/// S3 storage configuration
pub s3_endpoint: String,
/// S3 access key
pub s3_access_key: String,
/// S3 secret key
pub s3_secret_key: String,
/// S3 region
pub s3_region: String,
/// S3 bucket name
pub s3_bucket: String,
/// Redis connection URL
pub redis_url: String,
/// Admin key for privileged operations
pub admin_key: String,
/// Redirect URL for when logging in from the web-app
pub web_redirect_url: Option<String>,
}

impl Config {
Expand Down Expand Up @@ -63,7 +78,9 @@ impl Config {
let admin_key =
std::env::var("ADMIN_KEY").expect("Expected ADMIN_KEY environment variable to be set");

Config {
let web_redirect_url = std::env::var("WEB_REDIRECT_URL").ok();

Self {
database_url,
server_port,
feide_client_id,
Expand All @@ -77,6 +94,7 @@ impl Config {
s3_bucket,
redis_url,
admin_key,
web_redirect_url,
}
}
}
36 changes: 36 additions & 0 deletions apps/api/src/dto/admin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use serde::Serialize;
use utoipa::ToSchema;

use crate::models::user::User;

#[derive(Serialize, ToSchema)]
pub struct UserResponse {
/// Unique identifier for the user
pub id: String,
/// Name of the user
pub name: String,
/// Email address of the user
pub email: String,
/// Optional Feide ID for the user
pub feide_id: Option<String>,
/// Role of the user in the system
pub role: String,
/// Number of extra beers the user can claim
pub additional_beers: i32,
/// Optional alternative email address for the user
pub alt_email: Option<String>,
}

impl From<User> for UserResponse {
fn from(user: User) -> Self {
Self {
id: user.id,
name: user.name,
email: user.email,
feide_id: user.feide_id,
role: user.role,
additional_beers: user.additional_beers,
alt_email: user.alt_email,
}
}
}
2 changes: 2 additions & 0 deletions apps/api/src/dto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub mod admin;
pub mod event;
pub mod image;
pub mod product;
pub mod root;
pub mod status;
pub mod user;

pub use admin::*;
pub use event::*;
pub use image::*;
pub use product::*;
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/extractors/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl FromRequestParts<AppState> for AuthorizedMember {
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let session_id = extract_session_cookie(parts)?;
let session_id = extract_session_cookie(parts, state.key.clone())?;
let (session, user) = state.auth_service.validate_session(&session_id).await?;

Ok(AuthorizedMember { session, user })
Expand All @@ -28,7 +28,7 @@ impl FromRequestParts<AppState> for AuthorizedBoardMember {
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
let session_id = extract_session_cookie(parts)?;
let session_id = extract_session_cookie(parts, state.key.clone())?;
let (session, user) = state.auth_service.validate_session(&session_id).await?;

if !user.is_board_member() {
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
pub mod admin;
pub mod auth;
mod utils;

pub const SESSION_COOKIE_NAME: &str = "session_token";
Loading
Loading