feat(v1.1.8): app_users table + repo (migration 0026)
Per-app end-user table for the v1.1.8 users::* SDK. Distinct from admin_users (control-plane operators) — same Argon2id password hash shape but everything else (uniqueness scope, ownership, lifecycle) independent. - Uniqueness on (app_id, lower(email)) — case-insensitive within an app; same email may exist across two apps. - AppUserRepository trait + Postgres impl; every method takes app_id explicitly so cross-app reads are unmistakable at the call site (matches v1.1.3 cross-app discipline). - Public AppUserRow never includes the password hash; the credentials shape is its own struct returned only by the login lookup. - Cursor-based list keyed on (created_at, id). - Reserved a timing-flat dummy Argon2id PHC constant in auth.rs for the upcoming login path so the bad-email and good-email branches share wall-clock cost. - Added AppUserId + InvitationId id types in shared::ids. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
crates/manager-core/migrations/0026_app_users.sql
Normal file
31
crates/manager-core/migrations/0026_app_users.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- v1.1.8 User Management — data-plane app users.
|
||||
--
|
||||
-- Distinct from `admin_users` (control-plane operators). These are the
|
||||
-- end-users of apps built on PiCloud — created and managed by user
|
||||
-- scripts via the `users::*` SDK, surfaced to the dashboard via
|
||||
-- `/api/v1/admin/apps/{id}/users/*`.
|
||||
--
|
||||
-- Identity tuple is `(app_id, id)`; uniqueness is enforced on
|
||||
-- `(app_id, lower(email))` so the same email can exist across two apps
|
||||
-- but not twice within one app. Email case is preserved in storage and
|
||||
-- normalized only at the index / lookup boundary.
|
||||
--
|
||||
-- Password hash is Argon2id PHC (same algorithm as `admin_users` — the
|
||||
-- script-end-user trust shape and the operator-account trust shape
|
||||
-- happen to coincide on the hashing primitive even though everything
|
||||
-- else about the two tables is independent).
|
||||
|
||||
CREATE TABLE app_users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
email_verified_at TIMESTAMPTZ,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_app_users_app_email_lower ON app_users (app_id, lower(email));
|
||||
CREATE INDEX idx_app_users_app ON app_users (app_id);
|
||||
Reference in New Issue
Block a user