Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
586ee12
Add `CLAUDE.md` for development guidance; refactor error handling and…
JrTimha May 18, 2026
e8f70e5
Add `CLAUDE.md` for development guidance; refactor error handling and…
JrTimha May 18, 2026
1f40ece
Migrate from ScyllaDB to PostgreSQL with `sqlx` integration: remove S…
JrTimha May 18, 2026
3c4c48b
Refactor `Message` model: rename to `MessageEntity` and `MessageDTO` …
JrTimha May 18, 2026
4b6ecdf
Refactor repositories and services: modularize repository structure, …
JrTimha May 19, 2026
3950e40
Migrate `latest_message_preview_text` field to JSONB: update database…
JrTimha May 19, 2026
4043eb2
Refactor transactional message handling: update `delete_room_messages…
JrTimha May 19, 2026
c9f0b4c
Refactor config: replace `user_db_config` with `room_db_config`, upda…
JrTimha May 19, 2026
743b2b2
Add `.claude` command scaffolds for `migrate`, `new-broadcast-event`,…
JrTimha May 19, 2026
ce5e0a4
Remove `.claude` command scaffolds for `migrate`, `new-broadcast-even…
JrTimha May 19, 2026
33383fe
Add design documentation for location presence sharing and streaming …
JrTimha Jun 21, 2026
66ed691
Add design documentation for location presence sharing and streaming …
JrTimha Jun 21, 2026
f861021
Migrate notification cache to Redis Streams for self-trimming durable…
JrTimha Jun 21, 2026
8ddefda
Refactor CI/CD workflows: remove deprecated pipeline, add `ci.yml` an…
JrTimha Jun 21, 2026
9f2b254
Refactor imports, format code for consistency, and fix spacing in `me…
JrTimha Jun 21, 2026
0cdd165
Relax Clippy warnings enforcement in CI/CD workflows to prevent build…
JrTimha Jun 21, 2026
a1cbf05
Added CVE Scans
JrTimha Jun 21, 2026
b087ed8
Replace `dotenv` with `dotenvy`, update `Cargo.lock` dependencies, an…
JrTimha Jun 22, 2026
c3db013
Add `cargo-audit` configuration for vulnerability management, remove …
JrTimha Jun 22, 2026
ee59159
Refactor `event_producer`: extract `generate_header` for cleaner head…
JrTimha Jun 22, 2026
c2ac09d
Remove unnecessary blank line in `event_producer` and fix missing new…
JrTimha Jun 22, 2026
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
17 changes: 17 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# cargo-audit configuration (read automatically by `cargo audit`).
#
# Advisories listed under `ignore` are consciously accepted. Every entry MUST
# document *why* it is safe to accept and be revisited whenever a fix ships.
# The scan stays strict for everything else: any new, unlisted vulnerability
# still fails the build.

[advisories]
ignore = [
# rsa: "Marvin Attack" — timing side-channel during RSA *private-key*
# operations (signing / decryption). ISM only uses `jsonwebtoken` to
# *verify* Keycloak JWTs, which is a public-key operation; the vulnerable
# private-key path is never exercised. `rsa` is pulled in transitively via
# `jsonwebtoken`'s `rust_crypto` feature and no fixed version exists.
# Re-evaluate if a patched `rsa` is released or we switch crypto backend.
"RUSTSEC-2023-0071",
]
13 changes: 13 additions & 0 deletions .claude/agents/code-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: code-reviewer
description: Reviews code for correctness, security, and maintainability
tools: Read, Grep, Glob
---

You are a senior code reviewer. Review for:

1. Correctness: logic errors, edge cases, null handling
2. Security: injection, auth bypass, data exposure
3. Maintainability: naming, complexity, duplication

Every finding must include a concrete fix.
56 changes: 56 additions & 0 deletions .claude/rules/broadcast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
paths:
- src/broadcast/**
---

# Broadcast Rules

`BroadcastChannel` is a global singleton (`OnceCell<Arc<BroadcastChannel>>`). It holds a `RwLock<HashMap<Uuid, Sender<Notification>>>` — one Tokio broadcast channel per connected user.

## API

```rust
BroadcastChannel::get().send_event(notification, &user_id).await;
BroadcastChannel::get().send_event_to_all(user_ids, notification).await;
BroadcastChannel::get().subscribe_to_user_events(user_id).await; // → Receiver
BroadcastChannel::get().unsubscribe(user_id).await;
```

## Rules

- Always broadcast **after** a successful DB write, never before.
- Build notifications with `Notification::new(body)` — never construct the struct directly; it sets the envelope version and leaves `seq` unset (assigned per-user during delivery).
- `send_event` / `send_event_to_all` deliver to a single user via `deliver_to_user`, which assigns a per-user sequence number, caches durable events in Redis, and falls back to Kafka push for offline users.
- Push notifications are only sent for: `ChatMessage`, `FriendRequestReceived`, `NewRoom`.

## Envelope, Sequencing & Replay

Every notification is wrapped in a versioned envelope: `{ v, seq, type, createdAt, ...payload }`.

- `seq` is a **monotonic per-user** sequence (`Cache::next_sequence`, backed by Redis `INCR`). Each recipient of a fan-out gets its **own** `seq`.
- **Durable** events are sequenced and cached (per-user Redis Stream, entry ID `<seq>-0`, length-capped via `XADD ... MAXLEN ~ N`) so a reconnecting client can replay. **Ephemeral** events (`NotificationEvent::is_ephemeral() == true`) get no `seq` and are never cached — they are live-only.
- Without Redis (`NoOpCache`) there is no sequencing: `seq` is `None` and no replay is possible (best-effort delivery).
- On connect, SSE/WebSocket clients pass `?last_seq=<n>`; the server replays missing durable events, deduping live events with `seq <= high_water`. If the gap was trimmed out of the retained window (or a `Lagged` is hit), the server emits a `Resync` event and the client must reload state via REST. See `Cache::get_notifications_since_seq` → `ReplayResult`.

## NotificationEvent Variants

| Variant | Sent to | Trigger | Ephemeral |
|---|---|---|---|
| `ChatMessage { message, room_preview_text, sender }` | all room members | new message (`sender: RoomMember`) | no |
| `RoomChangeEvent { message, room_preview_text }` | all room members | join/leave/invite | no |
| `NewRoom { room, created_by }` | invited user | room creation / invite | no |
| `LeaveRoom { room_id }` | leaving user | user leaves room | no |
| `FriendRequestReceived { from_user }` | target user | friend request sent | no |
| `FriendRequestAccepted { from_user }` | requester | request accepted | no |
| `UserReadChat { user_id, room_id }` | all room members | room marked as read | no |
| `SystemMessage { message }` | any | system-level events | no |
| `Resync { reason }` | one client connection | replay gap / lag — client must reload via REST | yes |

## Broadcast Pattern

```rust
let bc = BroadcastChannel::get();
bc.send_event_to_all(member_ids, Notification::new(
NotificationEvent::ChatMessage { message, room_preview_text, sender },
)).await;
```
36 changes: 36 additions & 0 deletions .claude/rules/handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
paths:
- src/**/handler.rs
---

# Handler Rules

## Strict Separation

- **No business logic in handlers.** Handlers only extract inputs, call the service, and return the result.
- Business logic, validation, and error handling belong in the service layer.

## Auth Extraction

Every protected handler extracts the caller's identity via:

```rust
Extension(claims): Extension<KeycloakClaims>
```

The caller's UUID is available as `claims.sub`.

## Return Type

All handlers return `Result<Json<T>, HttpError>`. On success: `Ok(Json(...))`. On failure: `Err(HttpError)`.

`HttpError` serializes to:
```json
{ "status": 404, "errorCode": "NOT_FOUND", "message": "...", "timestamp": "...", "path": "/api/..." }
```

The `path` field is injected automatically by the `inject_request_path` middleware — do not set it manually.

## No unwrap()

Never use `unwrap()` or `expect()` in handlers. Propagate errors with `?` and convert via `HttpError`.
22 changes: 22 additions & 0 deletions .claude/rules/migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
paths:
- migrations/**
- .sqlx/**
---

# Migration Rules

## Workflow

1. `sqlx migrate add <name>` — creates `migrations/<timestamp>_<name>.up.sql` + `.down.sql`
2. Implement SQL in the generated files
3. `sqlx migrate run` — applies pending migrations
4. `cargo sqlx prepare` — regenerates `.sqlx/` compile-time metadata
5. Commit `.sqlx/` — required so CI can build without a live database

## Conventions

- Migration names in `snake_case`.
- Always write a corresponding `.down.sql` that fully reverses the `.up.sql`.
- Set `DATABASE_URL` in `.env` before running sqlx CLI commands.
- PostgreSQL is the only database — no cross-DB compatibility needed.
31 changes: 31 additions & 0 deletions .claude/rules/pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Cursor Pagination Rules

**All list endpoints use cursor pagination. No `page` or `pageSize` parameters anywhere in the API.**

## Infrastructure (`core/cursor.rs`)

```rust
CursorResults<T> { next_cursor: Option<String>, content: Vec<T> }
decode_cursor::<MyCursor>(base64_str) -> Result<MyCursor, CursorError>
encode_cursor(&cursor) -> Result<String, CursorError>
```

Cursors are base64url-encoded JSON structs. New cursor types must implement `Serialize + Deserialize + Default`.

## Existing Cursor Types

- `UserPaginationCursor { last_seen_name, last_seen_id }` — user search, friends list, and friend requests; keyset over `(display_name, id)`, optional name filter via the `raw_name` index
- `RoomPaginationCursor { last_seen_latest_message, last_seen_room_id }` — joined-rooms list; keyset over `(latest_message, id)` DESC, optional `ILIKE` name filter (other user for single rooms, room name for groups)
- Message timeline — timestamp-based (`created_at` DESC), indexed column. Returns `TimelinePage { messages, senders }`, where `senders` bundles the deduplicated `RoomMember`s that authored a message in the page or are the original author referenced by a reply (`reply_sender_id`); left authors still resolve from `app_user`, with null participant fields

## Page Size

- Clients may pass `limit`; the server clamps it via `clamp_page_size` (`core/cursor.rs`) to `[1, MAX_PAGE_SIZE]`, defaulting to `DEFAULT_PAGE_SIZE` (20) — never trust an unbounded client limit.
- Repositories fetch `page_size + 1` rows; `next_cursor` (`core/cursor.rs`) truncates to the page and encodes the continuation cursor from the last returned item.

## Rules

- Return `CursorResults<T>` from every list endpoint.
- The client passes `cursor` as a query parameter; omit for the first page.
- If the result set is smaller than the page limit, return `next_cursor: null`.
- Never leak internal IDs or timestamps directly — always encode them in the cursor.
27 changes: 27 additions & 0 deletions .claude/rules/repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
paths:
- src/**/repository/**
---

# Repository Rules

All data lives in PostgreSQL. SQLx macros (`sqlx::query!` / `sqlx::query_as!`) provide compile-time query type-checking against `.sqlx/` metadata.

## Executor Signatures

Before writing any repository function that participates in a transaction, follow `docs/sqlx-executor-pattern.md`. The three cases:

- `&PgPool` — standalone query, no transaction involvement
- `impl Executor<'_, Database = Postgres>` — caller decides whether to pass pool or transaction
- `&mut PgTransaction` — must run inside a transaction the caller owns

## After Any SQL Change

Run `cargo sqlx prepare` to regenerate `.sqlx/` compile-time metadata, then commit `.sqlx/`.

## Query Conventions

- Use `sqlx::query!` for queries without a return type mapping.
- Use `sqlx::query_as!` for queries mapping to a struct.
- No N+1 queries — fetch related data in a single query or via `JOIN`.
- All indexed lookups; no full-table scans on hot paths.
42 changes: 42 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"permissions": {
"allow": [
"Bash(cargo build*)",
"Bash(cargo run*)",
"Bash(cargo check*)",
"Bash(cargo test*)",
"Bash(cargo clippy*)",
"Bash(cargo fmt*)",
"Bash(cargo sqlx*)",
"Bash(sqlx migrate*)",
"Bash(git status*)",
"Bash(git log*)",
"Bash(git diff*)",
"Bash(git branch*)",
"Bash(git show*)",
"Bash(docker compose up*)",
"Bash(docker compose ps*)",
"Bash(docker compose logs*)"
],
"deny": [
"Bash(git push*)",
"Bash(git reset --hard*)",
"Bash(git clean*)",
"Bash(docker compose down*)",
"Bash(rm -rf*)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "cargo check 2>&1 | tail -30"
}
]
}
]
}
}
20 changes: 20 additions & 0 deletions .claude/skills/migrate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: migrate
description: Create and apply a new SQLx migration for the ISM project. Use when adding a database migration, creating tables, altering columns, or updating the schema.
disable-model-invocation: true
argument-hint: <migration-name>
allowed-tools: Bash(sqlx *) Bash(cargo sqlx prepare)
---

Create a new SQLx migration for the ISM project.

Migration name (snake_case): $ARGUMENTS

Steps:
1. Run `sqlx migrate add $ARGUMENTS` — creates a new timestamped file pair in `migrations/`
2. Show the path of the newly created migration file
3. Wait for the SQL implementation before proceeding
4. Once the migration is filled in and ready to apply:
- Run `sqlx migrate run`
- Run `cargo sqlx prepare` to update compile-time query metadata
- Remind the user that `.sqlx/` must be committed
32 changes: 32 additions & 0 deletions .claude/skills/new-broadcast-event/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: new-broadcast-event
description: Add a new broadcast notification type to the ISM real-time system. Use when introducing a new SSE/WebSocket event variant that needs to be sent to connected clients.
disable-model-invocation: true
argument-hint: <EventName>
allowed-tools: Read Edit Bash(cargo check)
---

Add a new broadcast notification type to the ISM real-time system.

Event name: $ARGUMENTS

## Your Task

First read:
- `src/broadcast/notification.rs` — existing `NotificationEvent` variants
- `src/broadcast/mod.rs` — `BroadcastChannel` API

Then implement:

### 1. Define the new event (`src/broadcast/notification.rs`)
- Add a new variant to the `NotificationEvent` enum
- Define the corresponding payload type as a struct (with `serde::Serialize`)

### 2. Broadcast call in the service
- Show where in the service the broadcast call belongs
- Use the pattern from `BroadcastChannel::get()` — either `send_event` or `send_event_to_all`
- Always broadcast **after** a successful DB write, never before

### 3. Final check
- Run `cargo check`
- Ensure all `match` arms on `NotificationEvent` are updated
43 changes: 43 additions & 0 deletions .claude/skills/new-endpoint/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
name: new-endpoint
description: Scaffold a new API endpoint in the ISM project following the established layered architecture (repository → service → handler → route). Use when adding new HTTP endpoints.
disable-model-invocation: true
argument-hint: <module> <HTTP-method> <path>
allowed-tools: Read Grep Edit Bash(cargo check) Bash(cargo sqlx prepare)
---

Scaffold a new API endpoint in the ISM project following the established layered architecture.

Input: $ARGUMENTS
Format: `<module> <HTTP-method> <path>` — e.g. `rooms POST /api/rooms/{id}/pin`

## Your Task

First read the existing files of the given module to match the style:
- `src/<module>/handler.rs`
- `src/<module>/routes.rs`
- the corresponding service and repository file

Then implement in this order:

### 1. Repository (`src/<module>/repository/`)
- New function with the correct SQLx executor signature (read `docs/sqlx-executor-pattern.md`)
- Query using `sqlx::query!` / `sqlx::query_as!` macro
- Afterwards: run `cargo sqlx prepare` and remind to commit `.sqlx/`

### 2. Service (`src/<module>/*_service.rs`)
- Business logic, validation, error handling via `HttpError`
- Calls the repository function

### 3. Handler (`src/<module>/handler.rs`)
- Extract `Extension(claims): Extension<KeycloakClaims>` for auth
- Call service, return `Ok(Json(...))` or `Err(HttpError)`
- No business logic in the handler

### 4. Route (`src/<module>/routes.rs`)
- Register the route in the router
- Correct HTTP method and path

### 5. Final check
- Run `cargo check`
- Flag any open `unwrap()` calls or missing error handling
Loading