Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
0d2e406
:hammer: Publish database module
turtton Oct 10, 2024
faa5a58
:hammer: Prepare axum server
turtton Oct 10, 2024
d42fe23
:construction: Add get accounts api
turtton Nov 7, 2024
6aa47ab
:memo: Add keycloak start command
turtton Nov 28, 2024
bbf7d7e
:construction: Add basic permission system
turtton Dec 5, 2024
e8afaff
:pushpin: Share destructure/dotenvy
turtton Jan 25, 2025
f83646e
:memo: Update keycloak script
turtton Jan 25, 2025
c7a2c45
:recycle: Change stellar related structure as auth
turtton Jan 25, 2025
ed37f2c
:hammer: Add some functions
turtton Jan 25, 2025
13ed900
:hammer: Add created_at field in Account
turtton Jan 25, 2025
ee00562
:bug: Fix invalid param
turtton Jan 25, 2025
cb0b393
:hammer: Implement get_all_accounts
turtton Jan 25, 2025
812bf33
:white_check_mark: Fix test codes
turtton Jan 25, 2025
f4c70ac
:hammer: Add get account path
turtton Jan 25, 2025
9369963
:bug: Add missed case
turtton Jan 27, 2025
b8f8dde
:art: Format codes
turtton Jan 27, 2025
2e1d3bd
:wrench: Automate keycloak initialization
turtton Feb 7, 2025
bf1c9be
:see_no_evil: Ignore sqlx log folder
turtton Feb 7, 2025
f18ef05
:truck: Update keycloak files
turtton Feb 13, 2025
981f1fc
:arrow_up: Update flake.lock
turtton Feb 13, 2025
33aacdf
:heavy_plus_sign: Add test_with
turtton Feb 13, 2025
8e2e0f5
:wrench: Add server as package
turtton Feb 13, 2025
be3f432
:wrench: Add keycloak env defaults
turtton Feb 13, 2025
e413412
:recycle: Extract closure to function and improve response
turtton Feb 20, 2025
afd85f2
:bulb: I tried to use mold but gained no advantages...(1sec slower th…
turtton Feb 27, 2025
2bf4ac8
:hammer: Add defaults
turtton Feb 27, 2025
eba1b84
:construction: Add auth_account treatments
turtton Feb 27, 2025
3fc173d
:art: Format codes
turtton Mar 13, 2025
ca4cd4d
:hammer: Implement get_auth_account
turtton Mar 13, 2025
30397c3
:hammer: Add redis database
turtton Mar 23, 2025
72418da
:hammer: Implement DatabaseConnection for RedisConnection
turtton Mar 27, 2025
14f3290
:hammer: Add Signal system
turtton Mar 27, 2025
5af3c72
:hammer: Add persist_and_emit in command handler
turtton Apr 12, 2025
edeb399
:hammer: Implement event applier system
turtton Apr 12, 2025
840b160
:lock: Implement RSA-2048 key pair generation with encryption
turtton Dec 31, 2025
e1f99db
:hammer: Implement create/get/delete account methods
turtton Jan 1, 2026
e0c768c
:wrench: Add agent settings
turtton Jan 1, 2026
5ecbd87
:hammer: Add default generator
turtton Jan 1, 2026
cbd06b0
:recycle: Add adapter layer and improve crypto security
turtton Jan 2, 2026
dfe4ce3
:recycle: Replace Account creation event sourcing with direct INSERT
turtton Feb 16, 2026
f38a1d1
:sparkles: Add PUT /accounts/:id endpoint for updating account is_bot…
turtton Feb 17, 2026
6f27c9a
:recycle: Return 204 No Content from PUT /accounts/:id
turtton Feb 18, 2026
43bee15
:recycle: Add AccountRepository adapter to unify Query/Modifier access
turtton Mar 8, 2026
70157f5
:recycle: Introduce AccountReadModel and AccountEventStore for proper…
turtton Mar 9, 2026
93d17db
:white_check_mark: Fix event store tests and add optimistic concurren…
turtton Mar 9, 2026
e80e878
:recycle: Rename Transaction to Executor in trait definitions
turtton Mar 9, 2026
343bf98
:recycle: Introduce adapter layer with AccountCommandProcessor/QueryP…
turtton Mar 9, 2026
a90d265
:hammer: Update lock file
turtton Mar 9, 2026
90d1040
:recycle: Apply CQRS pattern to AuthAccount and remove generic Event …
turtton Mar 9, 2026
364ceb8
:bug: Fix AuthAccount CQRS issues found in code review
turtton Mar 9, 2026
3e9a5e4
:memo: Fix CLAUDE.md discrepancies found by code review
turtton Mar 9, 2026
149875a
:recycle: Apply CQRS to Profile/Metadata and Repository pattern to Fo…
turtton Mar 9, 2026
21867bd
:recycle: Replace Keycloak with Ory Kratos + Hydra for authentication
turtton Mar 12, 2026
ebeb9ac
:white_check_mark: Add OAuth2 Login/Consent integration tests with wi…
turtton Mar 12, 2026
26d1bbd
:recycle: Skip OAuth2 tests when DATABASE_URL is unavailable
turtton Mar 12, 2026
06120c2
:wrench: Use test_with::env to skip OAuth2 tests when DATABASE_URL is…
turtton Mar 12, 2026
17ec372
:sparkles: Introduce Ory Keto permission system with Relation Tuple a…
turtton Mar 12, 2026
aa95db1
:recycle: Refactor Account deletion to deactivation with cascade cleanup
turtton Mar 12, 2026
6ed0d4e
:sparkles: Replace individual GET endpoints with batch fetch APIs
turtton Mar 12, 2026
1c4744a
:recycle: Separate Permission types into AccountRelation and Instance…
turtton Mar 12, 2026
e4fec1c
:sparkles: Implement Account Suspend/Ban moderation system
turtton Mar 13, 2026
7948af5
:recycle: Extract schema definitions from route handlers into schema …
turtton Mar 13, 2026
0573479
:sparkles: Generate OpenAPI schema from route definitions using utoipa
turtton Mar 13, 2026
4d673d6
:wrench: Add AGPL-3.0 license info to OpenAPI spec
turtton Mar 13, 2026
93d144c
:recycle: Replace internal UUID with URL in Profile image fields
turtton Mar 13, 2026
e1cec45
:zap: Fix N+1 query in get_profiles_batch with batch image lookup
turtton Mar 13, 2026
52e78d4
:recycle: Replace Option<Option<T>> with FieldAction<T> enum
turtton Mar 13, 2026
0822c85
:recycle: Migrate entity IDs from UUIDv7 to ferroid Snowflake ID
turtton Mar 14, 2026
bb1769f
:white_check_mark: Add test data factory with Builder pattern
turtton Mar 15, 2026
89e4e06
:bug: Fix test data collision in cargo-nextest parallel execution
turtton Mar 15, 2026
37eeea8
:rotating_light: Fix clippy warnings and login reject flow
turtton Mar 15, 2026
1c19dc5
:recycle: Introduce Param/Dto pattern to reduce function arguments
turtton Mar 15, 2026
b6ac85e
:card_file_box: Consolidate migration scripts into single init.sql
turtton Mar 15, 2026
932e59f
:memo: Remove old files
turtton Mar 15, 2026
ebc1bdf
:memo: Update api spec file
turtton Mar 15, 2026
5234a74
:art: Apply code review fixes across all layers
turtton Mar 15, 2026
7ab79f1
:shield: Apply comprehensive code review fixes
turtton Mar 15, 2026
9e49428
:memo: Update api spec
turtton Mar 15, 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
16 changes: 16 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read file_path; if [[ \"$file_path\" == *.rs ]]; then cargo fmt 2>/dev/null || true; fi; }",
"timeout": 30
}
]
}
]
}
}
24 changes: 20 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
DATABASE_URL=postgres://user:password@localhost:5432/dbname
DATABASE_URL=postgres://postgres:develop@localhost:5432/postgres
# or
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=user
DATABASE_PASSWORD=password
DATABASE_NAME=dbname
DATABASE_USER=postgres
DATABASE_PASSWORD=develop
DATABASE_NAME=postgres

HYDRA_ISSUER_URL=http://localhost:4444/
HYDRA_ADMIN_URL=http://localhost:4445/
KRATOS_PUBLIC_URL=http://localhost:4433/
EXPECTED_AUDIENCE=account

KETO_READ_URL=http://localhost:4466
KETO_WRITE_URL=http://localhost:4467

REDIS_URL=redis://localhost:6379
# or
REDIS_HOST=localhost

WORKER_ID=0

CORS_ALLOWED_ORIGINS=*
13 changes: 12 additions & 1 deletion .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,22 @@ jobs:
check:
uses: ShuttlePub/workflows/.github/workflows/check.yml@a95cf7631a550a66628dd4a2a8d6ef4d253edab4
with:
workspace: '[ "kernel", "application", "server" ]'
workspace: '[ "kernel", "adapter", "application", "server" ]'
check-driver:
uses: ShuttlePub/workflows/.github/workflows/test-psql.yml@454354614b775cd3b470c991b532e8db1c1ed89f
with:
workspace: driver
openapi-check:
name: Check openapi.json is up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Regenerate openapi.json
run: cargo test -p server write_openapi_spec_to_file -- --ignored
- name: Check for uncommitted diff
run: git diff --exit-code openapi.json
coverage:
uses: ShuttlePub/workflows/.github/workflows/coverage.yml@d4c43ec23ffeaf5538fc32bdf63aa5e042ccee61
secrets: inherit
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
.idea
.direnv
/migrations/dbml-error.log
.env
.env
logs
master-key-password
1 change: 1 addition & 0 deletions AGENTS.md
198 changes: 198 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Emumet is an Account Service for ShuttlePub, implementing Event Sourcing with CQRS pattern. The name derives from EMU (Extravehicular Mobility Unit) + Helmet.

## Build & Development Commands

```bash
# Build
cargo build

# Run tests (requires DATABASE_URL environment variable)
cargo test

# Run single test
cargo test <test_name>

# Run server
cargo run -p server
```

## Required Services

Use `podman-compose up` (or `docker-compose up`) to start all required services:

```bash
podman-compose up -d
```

This starts: PostgreSQL, Redis, Ory Kratos, and Ory Hydra.

### Manual startup (alternative)

#### PostgreSQL
```bash
podman run --rm --name emumet-postgres -e POSTGRES_PASSWORD=develop -p 5432:5432 docker.io/postgres
```
- User: postgres / Password: develop

#### Redis
Required for message queue (rikka-mq).

### Auth: Ory Kratos + Hydra

- **Kratos** (identity management): http://localhost:4433 (public), http://localhost:4434 (admin)
- Self-service registration enabled
- Identity schema: email + password
- Test user: testuser@example.com / testuser
- **Hydra** (OAuth2/OIDC): http://localhost:4444 (public), http://localhost:4445 (admin)
- Login/Consent Provider: Emumet server (GET /oauth2/login, GET/POST /oauth2/consent)
- JWT issuer

Config files: `ory/kratos/`, `ory/hydra/`

## Environment Variables

Copy `.env.example` to `.env`:
- `DATABASE_URL` or individual `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_NAME`
- `HYDRA_ISSUER_URL` — Hydra public URL for JWT validation (default: http://localhost:4444/)
- `HYDRA_ADMIN_URL` — Hydra admin URL for Login/Consent API (default: http://localhost:4445/)
- `KRATOS_PUBLIC_URL` — Kratos public URL for session verification (default: http://localhost:4433/)
- `EXPECTED_AUDIENCE` — Expected JWT audience claim (default: account)
- `REDIS_URL` or `REDIS_HOST` — Redis connection for message queue

### Master Key Password

Account creation requires a master key password file for signing key encryption:
- Production: `/run/secrets/master-key-password`
- Development: `./master-key-password` (create manually, `chmod 600`)

## Architecture

### Workspace Structure (5 crates with dependency flow)

```
kernel → adapter → application → server
kernel → driver → server
```

- **kernel**: Domain entities, interface traits (EventStore, ReadModel, Repository), Event Sourcing core. Traits are exposed via logical `pub mod interfaces {}` block in `lib.rs` (not a physical directory).
- **adapter**: CQRS processors (CommandProcessor/QueryProcessor) that compose kernel traits, crypto trait composition (SigningKeyGenerator)
- **application**: Use case services (Account CRUD use cases), event appliers (projection update), DTOs
- **driver**: PostgreSQL/Redis implementations of kernel interfaces
- **server**: Axum HTTP server, JWT auth (Ory Hydra), OAuth2 Login/Consent Provider, route handlers, DI wiring (Handler/AppModule)

### CQRS + Event Sourcing Pattern

Two entity types exist in the codebase: **CQRS-migrated** and **legacy (Query/Modifier)**.

#### CQRS-migrated entities (Account, AuthAccount, Profile, Metadata)

Each CQRS entity has these components across layers:

```
Command Flow:
REST handler → CommandProcessor (adapter)
→ EventStore.persist_and_transform() (kernel trait, driver impl)
→ EventApplier (kernel) → entity reconstruction
→ [AuthAccount only: ReadModel.create() for immediate consistency]
→ Signal → async applier → ReadModel projection update

Query Flow:
REST handler → QueryProcessor (adapter)
→ ReadModel.find_*() (kernel trait, driver impl)
```

**kernel** defines per-entity interface traits:
- `AccountEventStore` / `AuthAccountEventStore` / `ProfileEventStore` / `MetadataEventStore` — event persistence + retrieval per entity-specific table
- `AccountReadModel` / `AuthAccountReadModel` / `ProfileReadModel` / `MetadataReadModel` — projection reads + writes

**adapter** provides processors with blanket impls:
- `AccountCommandProcessor` / `ProfileCommandProcessor` / `MetadataCommandProcessor` — EventStore + EventApplier + Signal (projection via async applier)
- `AuthAccountCommandProcessor` — EventStore + EventApplier + ReadModel.create() + Signal (synchronous projection for find-or-create pattern)
- `*QueryProcessor` — ReadModel facade

**driver** implements per-entity stores:
- `PostgresAccountEventStore` → `account_events` table
- `PostgresAuthAccountEventStore` → `auth_account_events` table
- `PostgresProfileEventStore` → `profile_events` table
- `PostgresMetadataEventStore` → `metadata_events` table
- `PostgresAccountReadModel` → `accounts` table
- `PostgresAuthAccountReadModel` → `auth_accounts` table
- `PostgresProfileReadModel` → `profiles` table
- `PostgresMetadataReadModel` → `metadatas` table

**application** provides use case services and event appliers:
- `GetAccountUseCase` / `CreateAccountUseCase` / `UpdateAccountUseCase` / `DeactivateAccountUseCase` / `SuspendAccountUseCase` / `UnsuspendAccountUseCase` / `BanAccountUseCase` — Account CRUD + moderation orchestration via CommandProcessor/QueryProcessor
- `GetProfileUseCase` / `CreateProfileUseCase` / `UpdateProfileUseCase` — Profile CRUD
- `GetMetadataUseCase` / `CreateMetadataUseCase` / `UpdateMetadataUseCase` / `DeleteMetadataUseCase` — Metadata CRUD
- `UpdateAuthAccount` / `UpdateProfile` / `UpdateMetadata` — event appliers that replay events from EventStore, update ReadModel projections

#### Repository entities (Follow, RemoteAccount, Image, AuthHost)

These use the Repository pattern — a single trait combining read and write operations:
- `*Repository` traits in `kernel/src/repository/` — unified CRUD interface
- `Postgres*Repository` driver implementations in `driver/src/database/postgres/`
- Follow and RemoteAccount are pure CRUD (Event Sourcing removed)
- AuthHost and Image are pure CRUD (never had Event Sourcing)

### Key Patterns

**DependOn\* trait pattern**: Dependency injection via associated types. `DependOnFoo` provides `fn foo(&self) -> &Self::Foo`. Blanket impls auto-wire when dependencies are satisfied.

**impl_database_delegation! macro** (kernel/src/lib.rs): Delegates all database `DependOn*` traits from a wrapper type to a database field. Used by `Handler` to wire `PostgresDatabase`.

**EventApplier trait** (kernel/src/event.rs): Reconstructs entity state from events. `fn apply(entity: &mut Option<Self>, event: EventEnvelope) -> Result<()>`. Entity becomes `None` on Deleted events.

**Optimistic concurrency control**: Commands carry `prev_version: Option<KnownEventVersion>`. `KnownEventVersion::Nothing` = must be first event, `KnownEventVersion::Prev(version)` = must match latest version. EventStore validates before persisting.

**Signal → Applier pipeline**: `Signal` trait emits entity IDs via Redis (rikka-mq). `ApplierContainer` (server/src/applier.rs) receives and dispatches to entity-specific appliers that update ReadModel projections.

### Auth Architecture

JWT validation middleware (`server/src/auth.rs`):
- OIDC Discovery → JWKS cache (with kid-miss re-fetch, rate-limited)
- Bearer token → RS256 validation → `Extension<AuthClaims>` inserted into request
- `AuthClaims` → `OidcAuthInfo` → `resolve_auth_account_id` (find-or-create AuthHost + AuthAccount)

OAuth2 Login/Consent Provider (`server/src/route/oauth2.rs`):
- GET /oauth2/login — Kratos session → Hydra login accept
- GET /oauth2/consent — skip check → redirect or show consent
- POST /oauth2/consent — accept/reject with scope validation

Value mapping: JWT `iss` → `AuthHost.url`, JWT `sub` (Kratos identity UUID) → `AuthAccount.client_id`

### Entity Structure

Entities use vodca macros (`References`, `Newln`, `Nameln`) and `destructure::Destructure` for field access.

Event Sourcing対象エンティティ (Account, AuthAccount, Profile, Metadata):
- ID type (UUIDv7-based, provides temporal ordering)
- Event enum with variants (Created, Updated, Deleted) + `Nameln` for event name serialization
- `EventApplier` implementation
- `CommandEnvelope` factory methods (e.g., `Account::create()`, `Account::delete()`)

純粋CRUDエンティティ (Follow, RemoteAccount, AuthHost, Image):
- ID type のみ。Event enum / EventApplier なし
- Repository パターンで直接 CRUD 操作

### Server DI Architecture

`Handler` — owns PostgresDatabase + RedisDatabase + crypto providers + HydraAdminClient + KratosClient. `impl_database_delegation!` wires kernel traits.

`AppModule` — wraps `Arc<Handler>` + `Arc<ApplierContainer>`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore, Repository). Blanket impls provide CommandProcessor/QueryProcessor automatically. Provides `hydra_admin_client()` and `kratos_client()` accessors.

### Testing

Database tests use `#[test_with::env(DATABASE_URL)]` attribute to skip when database is unavailable.

### Data Cleanup (after auth migration)

If migrating from Keycloak to Ory, truncate auth-related tables:
```sql
TRUNCATE auth_hosts, auth_accounts, auth_account_events;
```
Loading
Loading