Release: auth system, admin RBAC, and production Supabase setup #70
@@ -1,11 +1,11 @@
|
||||
---
|
||||
# nuzlocke-tracker-1y09
|
||||
title: Enforce feature branch workflow for agents
|
||||
status: todo
|
||||
status: completed
|
||||
type: task
|
||||
priority: high
|
||||
created_at: 2026-03-20T20:48:21Z
|
||||
updated_at: 2026-03-20T20:59:21Z
|
||||
updated_at: 2026-03-20T21:01:47Z
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
# nuzlocke-tracker-he1n
|
||||
title: Add local GoTrue container for dev auth testing
|
||||
status: todo
|
||||
status: in-progress
|
||||
type: feature
|
||||
priority: normal
|
||||
created_at: 2026-03-20T20:57:04Z
|
||||
updated_at: 2026-03-20T20:57:04Z
|
||||
updated_at: 2026-03-20T21:08:22Z
|
||||
---
|
||||
|
||||
## Problem
|
||||
@@ -34,17 +35,17 @@ GoTrue will use the existing `db` PostgreSQL container, creating its own `auth`
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Research GoTrue Docker image and required env vars (JWT secret, DB connection, SMTP disabled, etc.)
|
||||
- [ ] Add `gotrue` service to `docker-compose.yml` using the existing `db` container
|
||||
- [ ] Configure GoTrue to use the same PostgreSQL with its own `auth` schema
|
||||
- [ ] Set local JWT secret (e.g. `super-secret-jwt-token-for-local-dev`) shared between GoTrue and the backend
|
||||
- [ ] Update `.env.example` with local GoTrue defaults (`SUPABASE_URL=http://localhost:9999`, local JWT secret, local anon key)
|
||||
- [ ] Update `frontend/src/lib/supabase.ts` to use `http://localhost:9999` in dev (GoTrue's local port)
|
||||
- [ ] Verify backend JWT verification works with GoTrue-issued tokens (same HS256 + shared secret)
|
||||
- [x] Research GoTrue Docker image and required env vars (JWT secret, DB connection, SMTP disabled, etc.)
|
||||
- [x] Add `gotrue` service to `docker-compose.yml` using the existing `db` container
|
||||
- [x] Configure GoTrue to use the same PostgreSQL with its own `auth` schema
|
||||
- [x] Set local JWT secret (e.g. `super-secret-jwt-token-for-local-dev`) shared between GoTrue and the backend
|
||||
- [x] Update `.env.example` with local GoTrue defaults (`SUPABASE_URL=http://localhost:9999`, local JWT secret, local anon key)
|
||||
- [x] Update `frontend/src/lib/supabase.ts` to use `http://localhost:9999` in dev (GoTrue's local port)
|
||||
- [x] Verify backend JWT verification works with GoTrue-issued tokens (same HS256 + shared secret)
|
||||
- [ ] Test email/password signup and login flow end-to-end locally
|
||||
- [ ] Verify OAuth buttons gracefully handle missing providers in local dev (show disabled state or helpful message)
|
||||
- [ ] Update `docker-compose.yml` healthcheck for GoTrue readiness
|
||||
- [ ] Document the local auth setup in README or contributing guide
|
||||
- [x] Verify OAuth buttons gracefully handle missing providers in local dev (show disabled state or helpful message)
|
||||
- [x] Update `docker-compose.yml` healthcheck for GoTrue readiness
|
||||
- [x] Document the local auth setup in README or contributing guide
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
],
|
||||
"PreCompact": [
|
||||
{ "hooks": [{ "type": "command", "command": "beans prime" }] }
|
||||
],
|
||||
"PreToolCall": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash -c 'if echo \"$TOOL_INPUT\" | grep -q \"git commit\"; then BRANCH=$(git branch --show-current); if [ \"$BRANCH\" = \"develop\" ] || [ \"$BRANCH\" = \"main\" ]; then echo \"BLOCK: Cannot commit directly to $BRANCH. Create a feature branch first: git checkout -b feature/<name>\"; exit 2; fi; fi'"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
19
.env.example
19
.env.example
@@ -3,11 +3,20 @@ DEBUG=true
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke
|
||||
|
||||
# Supabase Auth (backend)
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=your-anon-key
|
||||
SUPABASE_JWT_SECRET=your-jwt-secret
|
||||
# For local dev with GoTrue container:
|
||||
SUPABASE_URL=http://localhost:9999
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
|
||||
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# For production, replace with your Supabase cloud values:
|
||||
# SUPABASE_URL=https://your-project.supabase.co
|
||||
# SUPABASE_ANON_KEY=your-anon-key
|
||||
# SUPABASE_JWT_SECRET=your-jwt-secret
|
||||
|
||||
# Frontend settings (used by Vite)
|
||||
VITE_API_URL=http://localhost:8000
|
||||
VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
VITE_SUPABASE_ANON_KEY=your-anon-key
|
||||
# For local dev with GoTrue container:
|
||||
VITE_SUPABASE_URL=http://localhost:9999
|
||||
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
|
||||
# For production, replace with your Supabase cloud values:
|
||||
# VITE_SUPABASE_URL=https://your-project.supabase.co
|
||||
# VITE_SUPABASE_ANON_KEY=your-anon-key
|
||||
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -1,12 +1,11 @@
|
||||
# Branching Strategy
|
||||
|
||||
- **Never commit directly to `main`.** `main` is always production-ready.
|
||||
- Day-to-day work happens on `develop`.
|
||||
- New work is done on `feature/*` branches off `develop`.
|
||||
- Merge flow: `feature/*` → `develop` → `main`.
|
||||
- **Squash merge** `feature/*` into `develop` (one clean commit per feature).
|
||||
- **Merge commit** `develop` into `main` (marks deploy points).
|
||||
- Always `git pull` the target branch before merging into it.
|
||||
- **Never commit directly to `develop` or `main`.** Always create a `feature/*` branch first.
|
||||
- When starting an **epic**, create `feature/<epic-title-slug>` off `develop`
|
||||
- When starting a **standalone task/bug** (no parent epic), create `feature/<task-title-slug>` off `develop`
|
||||
- Each task within an epic gets its own commit(s) on the epic's feature branch
|
||||
- Branch naming: use a kebab-case slug of the bean title (e.g., `feature/add-auth-system`)
|
||||
- When the epic/task is complete, squash merge into `develop`
|
||||
|
||||
# Pre-commit Hooks
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -14,15 +14,29 @@ A full-stack Nuzlocke run tracker for Pokemon games.
|
||||
docker compose up
|
||||
```
|
||||
|
||||
This starts three services:
|
||||
This starts four services:
|
||||
|
||||
| Service | URL |
|
||||
|------------|--------------------------|
|
||||
| Service | URL |
|
||||
|------------|---------------------------|
|
||||
| Frontend | http://localhost:5173 |
|
||||
| API | http://localhost:8000 |
|
||||
| API Docs | http://localhost:8000/docs|
|
||||
| API | http://localhost:8080 |
|
||||
| API Docs | http://localhost:8080/docs|
|
||||
| GoTrue | http://localhost:9999 |
|
||||
| PostgreSQL | localhost:5432 |
|
||||
|
||||
### Local Authentication
|
||||
|
||||
The stack includes a local GoTrue container for auth testing. Email/password signup and login work out of the box with auto-confirmation (no email verification needed).
|
||||
|
||||
**OAuth providers (Google, Discord) are disabled in local dev.** The login/signup pages show OAuth buttons as disabled with a tooltip explaining this. For OAuth testing, deploy to an environment with Supabase cloud configured.
|
||||
|
||||
The local JWT secret and anon key are pre-configured in `.env.example` and `docker-compose.yml`. Copy `.env.example` to `.env` before starting:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up
|
||||
```
|
||||
|
||||
### Run Migrations
|
||||
|
||||
```bash
|
||||
|
||||
@@ -12,9 +12,14 @@ services:
|
||||
environment:
|
||||
- DEBUG=true
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke
|
||||
# Auth - must match GoTrue's JWT secret
|
||||
- SUPABASE_URL=http://gotrue:9999
|
||||
- SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
gotrue:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
@@ -29,8 +34,12 @@ services:
|
||||
- ./frontend/index.html:/app/index.html:cached
|
||||
environment:
|
||||
- VITE_API_URL=http://localhost:8080
|
||||
# Local GoTrue auth
|
||||
- VITE_SUPABASE_URL=http://localhost:9999
|
||||
- VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
|
||||
depends_on:
|
||||
- api
|
||||
- gotrue
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
@@ -43,6 +52,7 @@ services:
|
||||
- POSTGRES_DB=nuzlocke
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./docker/init:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
@@ -50,5 +60,43 @@ services:
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
gotrue:
|
||||
image: supabase/gotrue:v2.186.0
|
||||
ports:
|
||||
- "9999:9999"
|
||||
environment:
|
||||
# API settings
|
||||
- GOTRUE_API_HOST=0.0.0.0
|
||||
- GOTRUE_API_PORT=9999
|
||||
- API_EXTERNAL_URL=http://localhost:9999
|
||||
- GOTRUE_SITE_URL=http://localhost:5173
|
||||
# Database
|
||||
- GOTRUE_DB_DRIVER=postgres
|
||||
- GOTRUE_DB_DATABASE_URL=postgres://postgres:postgres@db:5432/nuzlocke?sslmode=disable
|
||||
# JWT - must match backend's SUPABASE_JWT_SECRET
|
||||
- GOTRUE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
|
||||
- GOTRUE_JWT_AUD=authenticated
|
||||
- GOTRUE_JWT_EXP=3600
|
||||
- GOTRUE_JWT_ADMIN_ROLES=service_role
|
||||
# Email auth (auto-confirm for local dev)
|
||||
- GOTRUE_EXTERNAL_EMAIL_ENABLED=true
|
||||
- GOTRUE_MAILER_AUTOCONFIRM=true
|
||||
- GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=false
|
||||
# Disable external OAuth providers (not configured locally)
|
||||
- GOTRUE_EXTERNAL_GOOGLE_ENABLED=false
|
||||
- GOTRUE_EXTERNAL_DISCORD_ENABLED=false
|
||||
# Disable phone auth
|
||||
- GOTRUE_EXTERNAL_PHONE_ENABLED=false
|
||||
- GOTRUE_SMS_AUTOCONFIRM=false
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -6,7 +6,8 @@ const supabaseAnonKey = import.meta.env['VITE_SUPABASE_ANON_KEY'] ?? ''
|
||||
function createSupabaseClient(): SupabaseClient {
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
// Return a stub client for tests/dev without Supabase configured
|
||||
return createClient('http://localhost:54321', 'stub-key')
|
||||
// Uses port 9999 to match local GoTrue container
|
||||
return createClient('http://localhost:9999', 'stub-key')
|
||||
}
|
||||
return createClient(supabaseUrl, supabaseAnonKey)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState } from 'react'
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
const isLocalDev = import.meta.env['VITE_SUPABASE_URL']?.includes('localhost') ?? false
|
||||
|
||||
export function Login() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -108,7 +110,9 @@ export function Login() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
|
||||
disabled={isLocalDev}
|
||||
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -133,7 +137,9 @@ export function Login() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDiscordLogin}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
|
||||
disabled={isLocalDev}
|
||||
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
@@ -141,6 +147,11 @@ export function Login() {
|
||||
Discord
|
||||
</button>
|
||||
</div>
|
||||
{isLocalDev && (
|
||||
<p className="text-center text-xs text-text-tertiary">
|
||||
OAuth providers are not available in local development. Use email/password instead.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-center text-sm text-text-secondary">
|
||||
Don't have an account?{' '}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
const isLocalDev = import.meta.env['VITE_SUPABASE_URL']?.includes('localhost') ?? false
|
||||
|
||||
export function Signup() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -172,7 +174,9 @@ export function Signup() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignup}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
|
||||
disabled={isLocalDev}
|
||||
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -197,7 +201,9 @@ export function Signup() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDiscordSignup}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
|
||||
disabled={isLocalDev}
|
||||
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
|
||||
@@ -205,6 +211,11 @@ export function Signup() {
|
||||
Discord
|
||||
</button>
|
||||
</div>
|
||||
{isLocalDev && (
|
||||
<p className="text-center text-xs text-text-tertiary">
|
||||
OAuth providers are not available in local development. Use email/password instead.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-center text-sm text-text-secondary">
|
||||
Already have an account?{' '}
|
||||
|
||||
Reference in New Issue
Block a user