Files
eng-pad-server/internal/db/migrations.go
Kyle Isom 286b886c06 Implement Phase 2: password auth (Argon2id + bearer tokens)
- Argon2id password hashing and verification with configurable params
- Bearer token generation (32-byte random), SHA-256 hashed storage,
  TTL-based expiry
- User creation and authentication helpers
- auth_tokens table added to migrations
- 6 tests: hash/verify, wrong password, create/auth user, token
  create/validate, token expiry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:49:07 -07:00

124 lines
3.6 KiB
Go

package db
import (
"database/sql"
"fmt"
)
var migrations = []struct {
name string
sql string
}{
{
name: "001_initial_schema",
sql: `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS webauthn_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
credential_id BLOB NOT NULL UNIQUE,
public_key BLOB NOT NULL,
name TEXT NOT NULL,
sign_count INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS notebooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
remote_id INTEGER NOT NULL,
title TEXT NOT NULL,
page_size TEXT NOT NULL,
synced_at INTEGER NOT NULL,
UNIQUE(user_id, remote_id)
);
CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notebook_id INTEGER NOT NULL REFERENCES notebooks(id) ON DELETE CASCADE,
remote_id INTEGER NOT NULL,
page_number INTEGER NOT NULL,
UNIQUE(notebook_id, remote_id)
);
CREATE TABLE IF NOT EXISTS strokes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
pen_size REAL NOT NULL,
color INTEGER NOT NULL,
style TEXT NOT NULL DEFAULT 'plain',
point_data BLOB NOT NULL,
stroke_order INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS share_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notebook_id INTEGER NOT NULL REFERENCES notebooks(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
expires_at INTEGER,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY,
applied_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_notebooks_user ON notebooks(user_id);
CREATE INDEX IF NOT EXISTS idx_pages_notebook ON pages(notebook_id);
CREATE INDEX IF NOT EXISTS idx_strokes_page ON strokes(page_id);
CREATE INDEX IF NOT EXISTS idx_share_links_token ON share_links(token);
CREATE INDEX IF NOT EXISTS idx_webauthn_user ON webauthn_credentials(user_id);
CREATE TABLE IF NOT EXISTS auth_tokens (
token_hash TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user ON auth_tokens(user_id);
`,
},
}
func Migrate(database *sql.DB) error {
// Ensure schema_migrations table exists
_, err := database.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
name TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)`)
if err != nil {
return fmt.Errorf("create schema_migrations: %w", err)
}
for _, m := range migrations {
var count int
err := database.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE name = ?", m.name).Scan(&count)
if err != nil {
return fmt.Errorf("check migration %s: %w", m.name, err)
}
if count > 0 {
continue
}
if _, err := database.Exec(m.sql); err != nil {
return fmt.Errorf("apply migration %s: %w", m.name, err)
}
if _, err := database.Exec(
"INSERT INTO schema_migrations (name, applied_at) VALUES (?, strftime('%s','now'))",
m.name,
); err != nil {
return fmt.Errorf("record migration %s: %w", m.name, err)
}
}
return nil
}