Files
apocapoc-api/internal/infrastructure/persistence/sqlite/migrations.go
T
david f37c1ac19b Improve email verification flow and error handling
- Send verification email before creating user to prevent orphaned accounts
- Detect SMTP authentication errors and fail fast without retries
- Add field-level validation errors for better frontend UX
- Configure docker-compose with explicit environment variables
- Document dollar sign escaping in .env.example (use $$)
- Implement welcome email on successful verification
- Clean up unnecessary comments
2025-11-28 11:32:51 +01:00

143 lines
3.7 KiB
Go

package sqlite
import (
"database/sql"
"fmt"
)
func RunMigrations(db *sql.DB) error {
migrations := []string{
createUsersTable,
createHabitsTable,
createHabitEntriesTable,
createRefreshTokensTable,
createPasswordResetTokensTable,
createIndexes,
}
for _, migration := range migrations {
if _, err := db.Exec(migration); err != nil {
return err
}
}
if err := addEmailVerificationColumns(db); err != nil {
return err
}
return nil
}
func addEmailVerificationColumns(db *sql.DB) error {
columns := []struct {
name string
definition string
}{
{"email_verified", "ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT 0"},
{"email_verification_token", "ALTER TABLE users ADD COLUMN email_verification_token TEXT"},
{"email_verification_expiry", "ALTER TABLE users ADD COLUMN email_verification_expiry DATETIME"},
}
for _, col := range columns {
exists, err := columnExists(db, "users", col.name)
if err != nil {
return err
}
if !exists {
if _, err := db.Exec(col.definition); err != nil {
return err
}
}
}
return nil
}
func columnExists(db *sql.DB, table, column string) (bool, error) {
query := fmt.Sprintf("SELECT COUNT(*) FROM pragma_table_info('%s') WHERE name = ?", table)
var count int
err := db.QueryRow(query, column).Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
timezone TEXT DEFAULT 'UTC',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
const createHabitsTable = `
CREATE TABLE IF NOT EXISTS habits (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
type TEXT CHECK(type IN ('BOOLEAN', 'COUNTER', 'VALUE')),
frequency TEXT CHECK(frequency IN ('DAILY', 'WEEKLY', 'MONTHLY')),
specific_days TEXT,
specific_dates TEXT,
carry_over BOOLEAN DEFAULT 0,
is_negative BOOLEAN DEFAULT 0,
target_value REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
archived_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`
const createHabitEntriesTable = `
CREATE TABLE IF NOT EXISTS habit_entries (
id TEXT PRIMARY KEY,
habit_id TEXT NOT NULL,
scheduled_date DATE NOT NULL,
completed_at DATETIME NOT NULL,
value REAL,
FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
UNIQUE(habit_id, scheduled_date)
);
`
const createRefreshTokensTable = `
CREATE TABLE IF NOT EXISTS refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
revoked_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`
const createPasswordResetTokensTable = `
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
used_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`
const createIndexes = `
CREATE INDEX IF NOT EXISTS idx_habits_user ON habits(user_id);
CREATE INDEX IF NOT EXISTS idx_habits_active ON habits(user_id, archived_at);
CREATE INDEX IF NOT EXISTS idx_entries_habit ON habit_entries(habit_id);
CREATE INDEX IF NOT EXISTS idx_entries_scheduled ON habit_entries(scheduled_date);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user ON password_reset_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token);
`