Skip to content

Commit 4b12eda

Browse files
authored
Merge: PR #42 from sameboat-platform/feat/week-3-checkout
This pull request introduces several backend hardening features focused on authentication, session management, and documentation. The most important changes are the addition of password complexity enforcement during registration, in-memory login rate limiting with a new error code, scheduled session pruning, and comprehensive documentation updates reflecting these improvements. Integration tests have been added for all major new features.
2 parents ca0c41d + 6e4f231 commit 4b12eda

19 files changed

+651
-31
lines changed

CHANGELOG.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## [Unreleased]
6+
### Added
7+
- Password complexity validation on registration (min 8 chars, includes upper/lower/digit) with validation message.
8+
- In-memory login rate limiting (5 attempts within 5 minutes by email+IP) returning HTTP 429 `RATE_LIMITED`.
9+
- Scheduled session pruning job (hourly) with JPQL bulk delete; transactional execution.
10+
- Integration tests: password complexity, rate limiting, session pruning.
11+
- Documentation: `docs/RISKS.md` and `docs/spikes/jwt-session-tradeoffs.md`.
12+
- Public version endpoint `GET /api/version` that returns the deployed version (from JAR manifest when available, falling back to `project.version`).
13+
- Spring profiles for development and production:
14+
- `dev`: in-memory H2, relaxed cookies (host-only, `Secure=false`), and CORS including `http://localhost:5173`.
15+
- `prod`: Postgres/Neon, secure cookies (domain + `Secure=true`), explicit CORS allowlist.
16+
- CORS configuration via `CorsConfig` and Security chain `.cors()`; credentials enabled and origins restricted to configured list.
17+
- CI release job on tag push (`v*`): builds JAR and attaches it to a GitHub Release using `softprops/action-gh-release@v1`.
18+
19+
### Changed
20+
- `README.md`: Documented password policy, `RATE_LIMITED` error code, and updated sample passwords; linked to new docs.
21+
- `docs/api.md`: Updated error codes, register/login behavior, and added pruning notes.
22+
- `Architecture.md`: Reflected opaque sessions, rate limiting, password policy, and pruning.
23+
- `CONTRIBUTING.md`: Included `RATE_LIMITED` in error codes and noted password complexity under security.
24+
- Unified Actuator base path to `/actuator` across profiles; health and info remain exposed publicly via security rules. Legacy `/api/actuator/*` references were removed from tests/config where applicable.
25+
- Security rules clarified: public `GET /health`, `GET /actuator/health`, `GET /actuator/info`, auth endpoints `POST /api/auth/login|register|logout` (and legacy `/auth/*`) remain public; other endpoints require authentication (e.g., `GET /api/me`).
26+
- Input validation tightened on auth payloads (`@Valid` + Bean Validation). Validation errors are mapped by `GlobalExceptionHandler` to `400` with `{"error":"VALIDATION_ERROR"}`.
27+
- Documentation updates for profile usage, CORS, cookies, and deployment notes (Render/Neon).
28+
29+
### Fixed
30+
- Session pruning ClassCastException by replacing derived delete with explicit JPQL bulk delete method and using `@Transactional` in pruner.
31+
- Added Flyway migration `V4__add_timezone_to_users.sql` to align schema with `UserEntity.timezone`, resolving startup error `column ... timezone does not exist`.
32+
- Addressed IDE warnings for the GitHub Release step by pinning the action and passing the `files` input; pipeline runs green on tags.
33+
34+
---
35+
36+
## [v0.1.0] - 2025-10-05 – Initial release
37+
### Added
38+
39+
40+
### Changed
41+
42+
43+
### Fixed
44+
45+
46+
---
47+
Reference: See `docs/weekly-plan/week-3/week-3-checkout-backend.md` for a narrative weekly summary.
48+
49+
50+
[Unreleased]: https://github.com/sameboat-platform/backend/compare/v0.1.0...HEAD
51+
[v0.1.0]: https://github.com/sameboat-platform/backend/releases/tag/v0.1.0

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ Maintain ≥ 70% instruction coverage (Jacoco gate enforces this during `mvn ver
4141

4242
## 7. Error Handling & Codes
4343
Use / extend centralized handler `GlobalExceptionHandler`.
44-
Existing codes: VALIDATION_ERROR, BAD_REQUEST, BAD_CREDENTIALS, UNAUTHENTICATED, SESSION_EXPIRED, INTERNAL_ERROR.
44+
Existing codes: VALIDATION_ERROR, BAD_REQUEST, BAD_CREDENTIALS, UNAUTHENTICATED, SESSION_EXPIRED, RATE_LIMITED, INTERNAL_ERROR.
4545
Add new domain exceptions + codes only with accompanying tests and update the catalog in `copilot-instructions.md` section 21.
4646

4747
## 8. Security (SECURITY_BASELINE)
4848
- Validate all request DTOs (Jakarta Bean Validation).
49+
- Enforce password complexity for registration: min 8 chars and must include upper, lower, and digit.
4950
- Avoid exposing sensitive internal details in error messages.
5051
- Never log secrets or raw tokens.
5152
- Enforce least privilege (avoid broad queries when specific ones suffice).

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
> git remote set-url origin git@github.com:sameboat-platform/sameboat-backend.git
99
> ```
1010
11-
> Quick Links: [Instructions (setup & migrations)](./docs/instructions.md) | [API Reference](./docs/api.md) | [Week 3 Plan](./docs/weekly-plan/week-3/week-3-plan.md) | [Contributing](./CONTRIBUTING.md) | Guard Rails: [Copilot Instructions](.github/copilot-instructions.md) | Journals: [Index](./docs/journals/README.md) · [Week 1](./docs/journals/Week1-Journal.md) · [Week 2](./docs/journals/Week2-Journal.md)
11+
> Quick Links: [Instructions (setup & migrations)](./docs/instructions.md) | [API Reference](./docs/api.md) | [Risks](./docs/RISKS.md) | [JWT vs Extended Sessions (Spike)](./docs/spikes/jwt_vs_extended_sessions.md) | [Week 3 Plan](./docs/weekly-plan/week-3/week-3-plan.md) | [Contributing](./CONTRIBUTING.md) | Guard Rails: [Copilot Instructions](.github/copilot-instructions.md) | Journals: [Index](./docs/journals/README.md) · [Week 1](./docs/journals/Week1-Journal.md) · [Week 2](./docs/journals/Week2-Journal.md)
1212
1313
## Getting Started (Contributors)
1414
Before writing code or using AI assistance:
@@ -187,6 +187,8 @@ Opaque session cookie `SBSESSION=<UUID>` (alias accepted: `sb_session`) issued o
187187
`POST /auth/login` (also `/api/auth/login`): returns full user envelope `{ "user": { ... } }`.
188188
- If `sameboat.auth.dev-auto-create=true` (test/dev), a non‑existent user with stub password is auto-created.
189189
- Passwords are stored with **BCrypt** (Spring `BCryptPasswordEncoder`).
190+
- Password complexity enforced: min 8 chars, must include upper, lower, and digit.
191+
- Login attempts are rate limited; excessive failures return `RATE_LIMITED` (HTTP 429).
190192

191193
### Logout
192194
`POST /auth/logout` clears server session and sends an expired cookie.
@@ -201,7 +203,7 @@ All non-2xx errors:
201203
```json
202204
{ "error": "<CODE>", "message": "Human readable explanation" }
203205
```
204-
Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `INTERNAL_ERROR`.
206+
Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `RATE_LIMITED`, `INTERNAL_ERROR`.
205207

206208
| Code | Typical Trigger |
207209
|------|-----------------|
@@ -211,6 +213,7 @@ Current codes: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_E
211213
| EMAIL_EXISTS | Duplicate registration attempt |
212214
| VALIDATION_ERROR | Bean validation (fields, sizes, empty patch body) |
213215
| BAD_REQUEST | Explicit IllegalArgument / future semantics |
216+
| RATE_LIMITED | Too many requests (e.g., repeated failed logins) |
214217
| INTERNAL_ERROR | Uncaught exception (trace id logged) |
215218

216219
## Quality Gates
@@ -255,15 +258,15 @@ Use any HTTP client or browser:
255258

256259
## Sample cURL
257260
```bash
258-
# Register
261+
# Register (password must include upper, lower, digit and be >= 8 chars)
259262
curl -i -X POST http://localhost:8080/auth/register \
260263
-H 'Content-Type: application/json' \
261-
-d '{"email":"dev@example.com","password":"abcdef","displayName":"Dev"}'
264+
-d '{"email":"dev@example.com","password":"Abcdef12","displayName":"Dev"}'
262265

263266
# Login
264267
curl -i -X POST http://localhost:8080/auth/login \
265268
-H 'Content-Type: application/json' \
266-
-d '{"email":"dev@example.com","password":"abcdef"}'
269+
-d '{"email":"dev@example.com","password":"Abcdef12"}'
267270

268271
# Use cookie
269272
curl -i http://localhost:8080/me -H 'Cookie: SBSESSION=<uuid>'

TTD.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,18 @@ Name it something clear like “Protect main (PR + CI)” and you’re done.
8787
## 11. Completed Items Log (Append Here When Done)
8888
| Date | ID | PR | Notes |
8989
|------|----|----|-------|
90+
| 2025-10-16 | WK3-001 | | Password complexity validation enforced (register); tests added; README/API updated |
91+
| 2025-10-16 | WK3-002 | | Login rate limiting (5 attempts/5 min) with 429 RATE_LIMITED; tests added; logs at INFO |
92+
| 2025-10-16 | WK3-003 | | Session pruning scheduler (hourly) with JPQL bulk delete; integration test added |
93+
| 2025-10-16 | WK3-006 | | docs/RISKS.md created and linked; README references updated |
94+
| 2025-10-16 | WK3-007 | | JWT vs. opaque sessions spike committed (docs/spikes/jwt-session-tradeoffs.md) |
9095

9196
---
9297
# 12. Week 3 Blocking Items (2025-10-05)
9398
| ID | Item | Description | Priority | Notes |
9499
|----|------|-------------|----------|-------|
95-
| WK3-001 | Password Complexity Validation | Enforce password rules (min 8 chars, 1 upper, 1 lower, 1 digit) and map violations to VALIDATION_ERROR | P1 | Add tests and document in API |
96-
| WK3-002 | Basic Rate Limiting | Implement naive rate limiting for auth endpoints (5 attempts/5 min/email/IP), return RATE_LIMITED | P1 | Add INFO log and tests |
97-
| WK3-003 | Session Pruning Job | Scheduled task to delete expired sessions hourly | P1 | Add integration test |
98-
| WK3-004 | Migration Test in CI | Ensure migration test runs in CI using Testcontainers/Postgres | P1 | Document in instructions.md |
99-
| WK3-005 | Unit Test Coverage | Add/improve unit tests for UserService, SessionService, password validator (≥75%) | P1 | Document coverage status |
100-
| WK3-006 | Docs: instructions.md, RISKS.md | Create and link instructions.md and RISKS.md | P1 | Link from README and project board |
101-
| WK3-007 | JWT Spike Doc | Prepare JWT spike decision doc (pros/cons, migration plan) | P2 | Link from README and project board |
100+
| WK3-004 | Migration Test in CI | Ensure migration test runs in CI using Testcontainers/Postgres | P1 | Deferred under BACKEND_CI_GUARD; evaluate adding profile/matrix next week |
101+
| WK3-005 | Unit Test Coverage | Add/improve unit tests for UserService, SessionService, password validator (≥75%) | P1 | Current gate 70% passing; raise to 75% after adding tests |
102102

103103
---
104104
## Versioning & Continuous Delivery Checklist

docs/Architecture.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**FE (React+Vite) →** HTTP/JSON → **BE (Spring Boot 3, Java 21) →** JDBC → **Postgres (Neon)**
44

5-
- Auth: stub (session/JWT later)
5+
- Auth: opaque session cookie (UUID) with server-side lookup; registration enforces password complexity; login rate-limited; scheduled session pruning.
66
- Core entities: users, stories, trust_events
77
- Migrations: Flyway (V1 applied)
88
- Envs: DB_URL only (OpenAI later)
@@ -15,7 +15,7 @@
1515
| Frontend (SPA) | Netlify | https://app.sameboatplatform.org | Calls API subdomain (CORS & cookies). |
1616
| Root Domains | Registrar / DNS | sameboatplatform.org (+ .com redirect) | `.com` 301 → `.org`. Configure APEX + `www` as needed. |
1717
| Database | Neon Postgres (managed) | TLS required (jdbc `sslmode=require`) | Branching for previews later. |
18-
| Session Cookie | Browser (Set by API) | Domain: `.sameboatplatform.org` | Name `SBSESSION`, `Secure`, `HttpOnly`, `SameSite=Lax`. |
18+
| Session Cookie | Browser (Set by API) | Domain: `.sameboatplatform.org` | Name `SBSESSION`, `Secure` (prod), `HttpOnly`, `SameSite=Lax`. |
1919
| CORS Allowlist | Spring Config | https://app.sameboatplatform.org | Credentials (cookies) allowed; keep list minimal. |
2020

2121
### Environment Variables (Prod Example)
@@ -38,10 +38,13 @@ SAMEBOAT_CORS_ALLOWED_ORIGINS=https://app.sameboatplatform.org
3838
- Enforce HTTPS at Render & Netlify; HTTP → HTTPS redirect.
3939
- TLS enforced to Neon with `sslmode=require`.
4040
- Only whitelisted origin gets credentialed CORS; avoid wildcard origins.
41+
- Registration password policy: min 8 chars, includes upper/lower/digit.
42+
- Rate limiting on login (5 attempts / 5 min) returns 429 RATE_LIMITED.
43+
- Scheduled session pruning removes expired rows (hourly); expiry also enforced at request time.
4144

4245
### Future Enhancements
4346
- Add staging environment: `staging-api.sameboatplatform.org` + Neon branch database.
4447
- CDN caching layer for static assets (handled by Netlify). API remains dynamic.
45-
- Potential WAF / rate limiting at edge (Render or external service) for auth endpoints.
48+
- Potential WAF / distributed rate limiting (e.g., Redis) for multi-instance auth endpoints.
4649

4750
---

docs/RISKS.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Known Risks and Mitigations
2+
3+
This document lists current risks in the backend and how we plan to mitigate them. It’s a living file; update as features evolve.
4+
5+
## Authentication & Session Management
6+
- In-memory rate limiting
7+
- Risk: Rate limiter state is reset on application restart (risk of brute force during a restart) and can produce false positives behind NAT/shared IPs.
8+
- Mitigation: Threshold set conservatively (5 attempts/5 min). Consider moving to a distributed store (Redis) with IP + device fingerprinting for multi-instance environments. Add allowlist for known CI test users/domains.
9+
10+
- Session pruning schedule
11+
- Risk: If pruning job lags or fails, expired sessions could persist longer than intended, slightly increasing DB footprint.
12+
- Mitigation: Pruner runs hourly with transactional bulk delete. Expiry is also enforced at request time, so stale rows don’t grant access.
13+
14+
- Cookie security
15+
- Risk: Misconfiguration of cookie domain/flags can expose sessions to subdomains or non-TLS contexts.
16+
- Mitigation: Properties-driven; `Secure` and proper domain only in `prod`. CORS is strict to the SPA origin.
17+
18+
## Input Validation & Error Handling
19+
- Password complexity
20+
- Risk: Too lax passwords increase account takeover risk.
21+
- Mitigation: Enforced via Bean Validation (min 8, includes upper/lower/digit). Consider adding symbol requirement and breached password checks in future.
22+
23+
- Error envelope
24+
- Risk: Overly detailed errors leak information.
25+
- Mitigation: Centralized error envelope; `BAD_CREDENTIALS` is generic. Continue to avoid echoing sensitive inputs.
26+
27+
## Data & Migrations
28+
- Flyway immutability
29+
- Risk: Editing applied migrations causes drift.
30+
- Mitigation: Guard scripts + CI gate. Always add new migrations.
31+
32+
- Test vs. Prod DB behavior
33+
- Risk: H2 and Postgres have subtle differences.
34+
- Mitigation: Keep JPQL portable; use Testcontainers profile for schema verification.
35+
36+
## Operational
37+
- Single-node assumptions
38+
- Risk: In-memory components (rate limiter) don’t scale horizontally.
39+
- Mitigation: Plan Redis-backed rate limiting and session store scale-out when moving to multi-instance.
40+
41+
- Logging
42+
- Risk: Over-logging auth events could create noise or risk PII.
43+
- Mitigation: Minimal INFO logs; avoid payloads; consider structured logging in prod profile.
44+
45+
---
46+
Related: docs/spikes/jwt-session-tradeoffs.md for token strategy considerations.
47+

docs/api.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
# SameBoat API Reference (Auth & User Profile Slice)
22

3-
Status: Week 2 vertical slice (dev stub for auth password). This document covers implemented endpoints and response contracts.
3+
Status: Week 3 backend hardening complete (password complexity, rate limiting, session pruning). This document covers implemented endpoints and response contracts.
44

55
## Conventions
66
- All responses (success or error) are JSON.
77
- Authentication: opaque session cookie (primary name `SBSESSION`, legacy/alias accepted: `sb_session`) containing a UUID.
8-
- Dev default TTL: 7 days. Prod profile (see `application-prod.yml`) sets Secure cookie, domain `.sameboat.<tld>`, TTL 14 days.
8+
- Dev default TTL: 7 days. Prod profile sets `Secure` cookie, domain `.sameboatplatform.org`, TTL 14 days.
99
- Error envelope format:
1010
```json
1111
{ "error": "<CODE>", "message": "Human readable explanation" }
1212
```
13-
- Bio max length is 500 characters (intentional spec choice for Week 2).
13+
- Bio max length is 500 characters (intentional spec choice).
1414

15-
## Error Codes (401 / Auth Related)
15+
## Error Codes
1616
| Code | Meaning | Typical Source |
1717
|------|---------|----------------|
1818
| UNAUTHENTICATED | No / invalid / garbage cookie | EntryPoint / controllers |
1919
| BAD_CREDENTIALS | Bad email or password on login | /auth or /api/auth login |
2020
| SESSION_EXPIRED | Cookie valid but session expired | Filter -> EntryPoint |
2121
| EMAIL_EXISTS | Registration attempt with existing email (409) | /auth/register |
2222
| VALIDATION_ERROR | Body validation failure (400) | Controllers |
23+
| BAD_REQUEST | Explicit IllegalArgument (service) | Services / controllers |
24+
| RATE_LIMITED | Too many requests (e.g., repeated failed logins) (429) | /auth/login |
2325
| INTERNAL_ERROR | Unhandled exception (500) | Global handler |
2426

2527
## Data Models
@@ -40,7 +42,7 @@ Status: Week 2 vertical slice (dev stub for auth password). This document covers
4042
```json
4143
{ "error": "<CODE>", "message": "Human readable explanation" }
4244
```
43-
Current `error` codes now include: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `INTERNAL_ERROR`.
45+
Current `error` codes now include: `UNAUTHENTICATED`, `BAD_CREDENTIALS`, `SESSION_EXPIRED`, `EMAIL_EXISTS`, `VALIDATION_ERROR`, `BAD_REQUEST`, `RATE_LIMITED`, `INTERNAL_ERROR`.
4446

4547
## Authentication
4648
### POST /auth/login (also `/api/auth/login`)
@@ -58,15 +60,21 @@ Failure (401 BAD_CREDENTIALS):
5860
```json
5961
{ "error": "BAD_CREDENTIALS", "message": "Email or password is incorrect" }
6062
```
63+
Failure (429 RATE_LIMITED):
64+
```json
65+
{ "error": "RATE_LIMITED", "message": "Too many attempts; try again later" }
66+
```
6167

6268
### POST /auth/register (also `/api/auth/register`)
63-
Registers a new user (email must be unique; password ≥ 6 chars). Returns a session cookie and minimal body containing the userId.
69+
Registers a new user (email must be unique). Returns a session cookie and minimal body containing the userId.
70+
71+
Password policy: minimum 8 characters and must include at least one uppercase, one lowercase, and one digit.
6472

6573
Request:
6674
```json
6775
{
6876
"email": "dev@example.com",
69-
"password": "abcdef",
77+
"password": "Abcdef12",
7078
"displayName": "Dev"
7179
}
7280
```
@@ -79,9 +87,9 @@ Responses:
7987
```json
8088
{ "error": "EMAIL_EXISTS", "message": "Email already registered" }
8189
```
82-
- 400 VALIDATION_ERROR (e.g., password too short)
90+
- 400 VALIDATION_ERROR (e.g., password too weak)
8391
```json
84-
{ "error": "VALIDATION_ERROR", "message": "password size must be between 6 and 100" }
92+
{ "error": "VALIDATION_ERROR", "message": "password must be at least 8 characters and include upper, lower, and digit" }
8593
```
8694

8795
### POST /auth/logout (also `/api/auth/logout`)
@@ -122,6 +130,18 @@ Responses:
122130
- 400 Validation error → `VALIDATION_ERROR`
123131
- 401 If not authenticated / expired (distinct codes as above)
124132

133+
## Public Utility
134+
### GET /api/version
135+
Returns the deployed backend version. Public and unauthenticated.
136+
137+
Success (200):
138+
```json
139+
{ "version": "0.1.0" }
140+
```
141+
142+
Notes:
143+
- In production, the value reflects the JAR manifest when available; falls back to `project.version` during development/test.
144+
125145
## Error Handling Summary
126146
| Scenario | Status | error code | Notes |
127147
|----------|--------|------------|-------|
@@ -131,6 +151,7 @@ Responses:
131151
| Registration duplicate | 409 | EMAIL_EXISTS | Email normalized & already present |
132152
| Validation failure | 400 | VALIDATION_ERROR | Field constraints |
133153
| Empty PATCH body | 400 | VALIDATION_ERROR | Enforced explicitly |
154+
| Rate limited login attempts | 429 | RATE_LIMITED | 5 failures within 5 minutes |
134155
| Generic uncaught exception | 500 | INTERNAL_ERROR | Trace id logged server-side |
135156
| Illegal argument (service) | 400 | BAD_REQUEST | Future usage |
136157

@@ -139,19 +160,20 @@ Responses:
139160
- Validation: filter loads session by UUID; sets request attribute for expired → code `SESSION_EXPIRED`; missing/invalid → `UNAUTHENTICATED`.
140161
- Touch: `lastSeenAt` updated on authenticated requests.
141162
- Expiry: 7 days dev / 14 days prod.
163+
- Pruning: scheduled hourly job deletes expired sessions (server-side cleanup; expiry also enforced at request time).
142164

143165
## Example cURL Commands
144166
Login:
145167
```bash
146168
curl -i -X POST http://localhost:8080/auth/login \
147169
-H "Content-Type: application/json" \
148-
-d '{"email":"me@example.com","password":"dev"}'
170+
-d '{"email":"me@example.com","password":"Abcdef12"}'
149171
```
150172
Register:
151173
```bash
152174
curl -i -X POST http://localhost:8080/auth/register \
153175
-H "Content-Type: application/json" \
154-
-d '{"email":"me2@example.com","password":"abcdef","displayName":"Me Two"}'
176+
-d '{"email":"me2@example.com","password":"Abcdef12","displayName":"Me Two"}'
155177
```
156178
Expired (simulate): manually adjust DB row `expires_at` earlier and call `/me` with cookie to observe `SESSION_EXPIRED`.
157179

0 commit comments

Comments
 (0)