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
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ can target the same API.
**Stack:** C++23 · Qt 6.8 · QML · PostgreSQL 16 · CMake ≥ 3.28 · libpqxx · libsodium ·
Qt Test / Qt Quick Test — no source-level dependencies.

**Status:** pre-release, under active development. Increment 1 (foundations: build system,
database, migrations, REST skeleton, client shell, test scaffolding) is wrapping up with
public-repo readiness (license, CI); next up is authentication & RBAC. See the [Roadmap](#roadmap) and the
**Status:** pre-release, under active development. Increment 1 (foundations build system,
database, migrations, REST skeleton, client shell, tests, CI) shipped as `v0.1.0`;
Increment 2 (authentication & RBAC) is in progress. See the [Roadmap](#roadmap) and the
[Implementation log](#implementation-log).

The project maximizes Qt framework usage — Qt is used everywhere unless it is clearly
Expand All @@ -33,12 +33,20 @@ keeping the future container's migration entrypoint minimal.
> Developed incrementally, one reviewed step at a time. This README grows with each step —
> see [Repository layout](#repository-layout) for what exists today.

**Contents:** [Architecture](#architecture) · [Prerequisites](#prerequisites) ·
[Building](#building) · [Running the stack](#running-the-stack) ·
[Development database](#development-database) · [Testing](#testing) ·
[Code style](#code-style) · [Development workflow](#development-workflow) ·
[Repository layout](#repository-layout) · [Roadmap](#roadmap) ·
[Implementation log](#implementation-log) · [License](#license)
## Table of contents

1. [Architecture](#architecture)
2. [Prerequisites](#prerequisites)
3. [Building](#building)
4. [Running the stack](#running-the-stack)
5. [Development database](#development-database)
6. [Testing](#testing)
7. [Code style](#code-style)
8. [Development workflow](#development-workflow)
9. [Repository layout](#repository-layout)
10. [Roadmap](#roadmap)
11. [Implementation log](#implementation-log)
12. [License](#license)

## Architecture

Expand Down Expand Up @@ -180,6 +188,11 @@ The database URL resolves in order: existing `MODULO_DB_URL` in the environment
`.env` at the repo root → the dev-database default. `modulo_migrate --help` shows
the underlying CLI (`--url`, `--dir`).

Current schema: `0001_init` (metadata) and `0002_auth` (`users`, `roles`, `user_roles`,
`sessions`, plus a reusable `set_updated_at()` trigger and the `citext` extension for
case-insensitive emails). The data model is diagrammed in
[`docs/high_level_design.md`](docs/high_level_design.md#5-data-model).

## Testing

Tests are registered with CTest under the labels `unit`, `integration`, and `ui`:
Expand Down Expand Up @@ -269,8 +282,8 @@ CMakePresets.json configure/build/test presets (dev, dev-asan, dev-tidy, release

| Increment | Scope |
|---|---|
| 1 — Foundations (in progress, final step) | Build system, dockerized Postgres, migrations, REST skeleton with health endpoint, client shell, test scaffolding, public-repo readiness |
| 2 — Auth & RBAC | Users/roles/sessions schema, Argon2id password hashing (libsodium), opaque bearer tokens, `authed()` / `requireRole()` guards, login flow + dark theme system in the client |
| 1 — Foundations (DONE, `v0.1.0`) | Build system, dockerized Postgres, migrations, REST skeleton with health endpoint, client shell, test scaffolding, public-repo readiness |
| 2 — Auth & RBAC (in progress) | Users/roles/sessions schema, Argon2id password hashing (libsodium), opaque bearer tokens, `authed()` / `requireRole()` guards, login flow + dark theme system in the client |
| 3 — Transactions | BUY/SELL/SWAP records with server-side filtering & pagination; add/edit/delete dialog with price-per-unit ⇄ total-value derivation |
| 4 — Transfers | Bank ⇄ exchange IN/OUT transfers; shared bank-account / exchange reference data |
| 5 — Holdings & dashboards | Per-asset aggregation (amount, median buy/sell, net profit, portfolio share, value in USD/EUR) and the first Qt Charts dashboards |
Expand All @@ -295,6 +308,7 @@ CMakePresets.json configure/build/test presets (dev, dev-asan, dev-tidy, release
| 1.6b — Qt Test everywhere | Decision: Qt Test replaces Catch2 (one framework for C++ and QML); Catch2 + CPM removed — the project now has zero source-level dependencies |
| 1.7 — Docs finalization | README restructured for a public audience (status, contents, architecture, workflow, roadmap); HLD gained the test-architecture view; local working agreement (CLAUDE.md) refreshed |
| 1.8 — Public-repo readiness | MIT `LICENSE`; GitHub Actions CI (macOS runner: brew deps, `ci` preset, `-Werror` build, format check, unit + ui tests); README badges + License section; repository made public and tagged `v0.1.0` |
| 2.1 — Auth schema | `0002_auth.sql`: `users` (citext email, Argon2id hash, disabled_at), `roles` (admin/user), `user_roles`, `sessions` (SHA-256 token digest, sliding expiry, revocation, partial index); `set_updated_at()` trigger; ER diagram in the HLD |

## License

Expand Down
70 changes: 70 additions & 0 deletions db/migrations/0002_auth.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- 0002_auth: users, roles and sessions.
--
-- Password hashing (Argon2id) and session-token generation happen in the
-- server (libsodium); the database only ever stores the password hash and
-- the SHA-256 digest of a session token, never the token itself.
-- Append-only: never edit this file once applied.

CREATE EXTENSION IF NOT EXISTS citext; -- case-insensitive emails

-- Generic "bump updated_at on UPDATE" trigger, reused by later tables.
CREATE FUNCTION set_updated_at() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$$;

CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email citext NOT NULL UNIQUE,
display_name text NOT NULL,
password_hash text NOT NULL, -- Argon2id, libsodium crypto_pwhash_str format
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
disabled_at timestamptz, -- set = account locked out

CONSTRAINT users_email_not_blank CHECK (length(trim(email)) > 0),
CONSTRAINT users_display_name_not_blank CHECK (length(trim(display_name)) > 0)
);

CREATE TRIGGER users_set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

-- Fixed role catalogue; ids are stable and referenced by the server.
CREATE TABLE roles (
id smallint PRIMARY KEY,
name text NOT NULL UNIQUE
);

INSERT INTO roles (id, name) VALUES
(1, 'admin'),
(2, 'user');

CREATE TABLE user_roles (
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
role_id smallint NOT NULL REFERENCES roles (id),
PRIMARY KEY (user_id, role_id)
);

CREATE TABLE sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
token_sha256 bytea NOT NULL UNIQUE, -- digest of the opaque bearer token
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL, -- sliding: extended on use
last_seen_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz, -- set = logged out / revoked

CONSTRAINT sessions_token_sha256_length CHECK (octet_length(token_sha256) = 32),
CONSTRAINT sessions_expires_after_creation CHECK (expires_at > created_at)
);

-- Lookups: "which user owns this token?" (covered by the UNIQUE index on
-- token_sha256) and "active sessions of a user" (logout-everywhere, listing).
CREATE INDEX sessions_user_id_active_idx
ON sessions (user_id)
WHERE revoked_at IS NULL;
54 changes: 53 additions & 1 deletion docs/high_level_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,59 @@ sequenceDiagram
M-->>U: "N applied, M skipped" (exit code)
```

## 5. Test architecture
## 5. Data model

```mermaid
erDiagram
users {
uuid id PK
citext email UK "case-insensitive"
text display_name
text password_hash "Argon2id (libsodium)"
timestamptz created_at
timestamptz updated_at "trigger set_updated_at"
timestamptz disabled_at "null = active"
}
roles {
smallint id PK
text name UK "admin, user"
}
user_roles {
uuid user_id PK, FK
smallint role_id PK, FK
}
sessions {
uuid id PK
uuid user_id FK
bytea token_sha256 UK "digest only, 32 bytes"
timestamptz created_at
timestamptz expires_at "sliding 30 days"
timestamptz last_seen_at
timestamptz revoked_at "null = active"
}
meta {
text key PK
text value
timestamptz updated_at
}
schema_migrations {
integer version PK
text name
text checksum "md5 of file"
timestamptz applied_at
}

users ||--o{ user_roles : "has"
roles ||--o{ user_roles : "granted as"
users ||--o{ sessions : "owns (cascade delete)"
```

Migrations so far: `0001_init` (meta), `0002_auth` (users, roles, user_roles, sessions, `set_updated_at()` trigger,
`citext` extension). `schema_migrations` is not created by a migration file — the migration engine
(`modules/db`, `migrator.cpp`) creates it on first run and owns it. Tokens are never stored: the server keeps only the SHA-256 digest, so a database leak cannot
be replayed as a login.

## 6. Test architecture

```mermaid
flowchart LR
Expand Down
Loading