Wires AuthController (signup, confirmEmail, login, me, logout)
to the existing auth use cases. Routes mounted under /api with
AuthMiddleware on logout/me. RepositoryServiceProvider gains
EmailConfirmationToken and Post bindings; AppServiceProvider
binds the Emailer/EmailFactory and constructs SignupUser with
the configured from-address.
Signup now collects only email + displayName, creates an
unconfirmed user with empty password hash, mints an
EmailConfirmationToken, and dispatches a confirmation email.
Password is set during ConfirmUserEmail.
Adds displayname to existing assertions and new tests covering:
null/short/invalid-charset displayname, duplicate displayname,
findability by displayname. AuthenticateUser tests pick up the
seedUser displayname argument.
Add display_name (unique) and email_confirmed_at columns plus
matching getters, DTO fields, repo methods (findByDisplayName,
update), and migration. Existing auth tests updated to construct
User with the new params.
7 cases: zero postId or requesterId -> BadRequest; unknown post
is idempotent no-op; author can delete own post; admin can
delete anyone's post; non-author non-admin -> ForbiddenException;
forbidden attempts leave post intact.
4 cases: zero/negative id -> BadRequest; unknown id -> null
(controller maps to 404); existing id returns the Post.
GetPost takes int id directly (no Request object - the value is
trivial and controllers pull it from a route param).
5 cases: zero/negative userId -> BadRequest; user with no posts
-> []; returns only requested user's posts (filters out other
authors); ordered newest-first by createdAt. fails red.
trims title and body, rejects empty (post-trim) values with
BadRequest. supplies createdAt from injected Clock. persists
through PostRepository->create and returns the resulting Post.
44 tests pass.
7 cases: null + whitespace title -> BadRequest; null +
whitespace body -> BadRequest; valid request returns Post with
correct userId/title/body and createdAt = clock.now(); the post
is findable via the repo afterwards; title and body get trimmed
of leading/trailing whitespace. fails red - CreatePost class
absent.
PostModel maps posts table (id, user_id fk, title, body text,
created_at indexed). EloquentPostRepository: create, find,
findByUserId (desc by created_at), findRecent (limit, desc),
delete - chain via ::query() to keep larastan happy.
FakePostRepository sorts on read (defensive copy each return).
cascade-on-delete on user_id so removing a user nukes their
posts.
phpstan.neon suppresses staticMethod.dynamicCall under
app/*/Eloquent*Repository.php - phpstan-strict-rules flags
Eloquent's fluent builder idiom (Model::query()->orderBy())
because the static methods become instance calls mid-chain.
suppression scoped to repo files only so the rule still
applies elsewhere.
reads auth_token cookie (constant COOKIE_NAME for cross-layer
sharing with the AuthController). missing/empty cookie or
unknown token -> 401 json {error: unauthenticated}. expired
session is deleted then 401 returned. valid session attaches
the User entity to request attributes under 'user' so
downstream controllers can read it via request attributes. 37
tests pass.
generates token via injected TokenGenerator, asks Clock for now,
sets expiry to now+7d, persists through SessionRepository->create
and returns the resulting Session. all 31 tests pass.
4 cases: returns Session with the generated token + supplied
user; createdAt matches injected Clock now; expiresAt is now+7d;
session is findable via SessionRepository->findByToken. fails
red - CreateSession class missing.
input validation: email + password required. constructs
EmailAddress vo (BadRequest on bad format). looks up user; absent
or password-mismatch -> UnauthorizedException with constant
'invalid credentials' message (no enumeration leak). password
verified through PasswordHasher->verify against stored hash on
the User entity (no separate profile lookup -> tide keeps
password on the user row). returns the User entity for the
caller (typically CreateSession + AuthController). 27 tests
pass.
9 cases: null/empty/malformed email -> BadRequest; null/empty
password -> BadRequest; unknown email -> Unauthorized; wrong
password -> Unauthorized; valid creds return the User entity;
isAdmin flag survives the auth round-trip. fails red - the
AuthenticateUser class does not exist yet.
validates email present + format (wraps EmailAddress vo's
InvalidArgumentException as BadRequest), password present +
>= 8 chars, then ensures email not already registered. hashes
password through injected PasswordHasher and persists via
UserRepository->create with isAdmin=false (admins are seeder-
only per plan). throws DomainException on duplicate email so
the controller layer can map it to 409. all 18 tests pass.
9 cases: null/empty/malformed email -> BadRequest; null or
sub-8-char password -> BadRequest; duplicate email -> DomainException;
valid signup returns User with hashed password and isAdmin=false;
user is findable by email afterwards; EmailAddress vo lowercases
the domain. fails red - SignupUser class not yet defined.
Session: immutable holder of token, owning User, createdAt,
expiresAt. isExpired(now) compares >= expiresAt. SessionModel
keys on token (string primary, non-incrementing). migration adds
sessions table with foreign user_id (cascade on user delete) and
indexed expires_at for cleanup queries. EloquentSessionRepository
takes UserRepository to rehydrate the owning User on findByToken;
sessions for deleted users return null. FakeSessionRepository
mirrors with an in-memory map keyed by token, defensive copies on
read.
Clock + SystemClock (DateTimeImmutable in UTC), TokenGenerator +
RandomTokenGenerator (bin2hex(random_bytes(32)) -> 64-char hex),
PasswordHasher + BcryptPasswordHasher (password_hash with
PASSWORD_DEFAULT, password_verify). matching fakes:
FakeClock with mutable setTime, FakeTokenGenerator with a
pre-seeded queue (throws once exhausted), FakePasswordHasher
returns 'hashed:<plain>' for deterministic test assertions.
composer stan now passes --memory-limit=512M (default 128M
overflows once larastan loads more rules).
User holds email (EmailAddress vo), passwordHash, isAdmin - tide
keeps password and admin flag on the user row directly (no
separate profile entity like youngstartup). UserRepository
exposes find, findByEmail, create. CreateUserDto is readonly with
explicit isAdmin (per shared.md no-default-args rule).
hot-fix: pgdata anchored to repo root, .postgres/.direnv ignored
at any nesting. resolves stray backend/.postgres tracking caught
during phase 1 merge.
shellHook now derives PGDATA from $(git rev-parse --show-toplevel)
instead of $PWD. nix develop / direnv from a subdir (e.g.
backend/) used to seed a duplicate cluster at backend/.postgres
that leaked into git tracking. .gitignore loses its leading slash
on .postgres/ + .direnv/ + .pc.* so any nested cluster also gets
ignored. fallback to pwd preserves behavior outside a git repo.
968 stray backend/.postgres/* blobs already pruned from history
via git-filter-repo before this commit.
immutable readonly. trims whitespace, splits on @, lowercases the
domain (local-part case preserved per RFC 5321), validates with
FILTER_VALIDATE_EMAIL after normalization. throws
InvalidArgumentException on empty / missing-@ / malformed input.
exposes value(), getDomain(), equals(), __toString(). all 7
EmailAddressTest cases green; 9 tests total pass.