Files
kandev/apps/backend/internal/auth/store/schema.go
T
João Salavisa 481359ddb2 feat(auth): organization units, tenancy, roles and scopes for team access (#2919)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:40:43 +00:00

78 lines
2.5 KiB
Go

package store
import (
"fmt"
"github.com/kandev/kandev/internal/db"
"github.com/kandev/kandev/internal/db/dialect"
)
// initSchema creates the auth tables. The DDL is shared between SQLite and
// Postgres with per-driver timestamp-type substitution (same pattern as
// internal/runtimeflags). All statements are replay-safe (ADR 0027).
func (s *Store) initSchema() error {
timestampType := "DATETIME"
if dialect.IsPostgres(s.db.DriverName()) {
timestampType = "TIMESTAMPTZ"
}
statements := []string{
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS auth_identities (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'local',
subject TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL DEFAULT '',
created_at %s NOT NULL,
updated_at %s NOT NULL,
UNIQUE (user_id, provider),
UNIQUE (provider, subject)
)`, timestampType, timestampType),
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS auth_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token_sha256 TEXT NOT NULL UNIQUE,
created_at %s NOT NULL,
expires_at %s NOT NULL,
last_seen_at %s NOT NULL,
user_agent TEXT NOT NULL DEFAULT '',
ip TEXT NOT NULL DEFAULT ''
)`, timestampType, timestampType, timestampType),
`CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id)`,
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS auth_api_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
prefix TEXT NOT NULL,
token_sha256 TEXT NOT NULL UNIQUE,
created_at %s NOT NULL,
last_used_at %s,
expires_at %s
)`, timestampType, timestampType, timestampType),
`CREATE INDEX IF NOT EXISTS idx_auth_api_tokens_user ON auth_api_tokens(user_id)`,
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS auth_invites (
id TEXT PRIMARY KEY,
token_sha256 TEXT NOT NULL UNIQUE,
email TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'member',
created_by TEXT NOT NULL,
created_at %s NOT NULL,
expires_at %s NOT NULL,
used_by TEXT,
used_at %s
)`, timestampType, timestampType, timestampType),
}
for _, stmt := range statements {
if _, err := s.db.Exec(stmt); err != nil {
return fmt.Errorf("auth schema: %w", err)
}
}
// An invite mints a member of exactly one organization, so it carries the
// minting admin's org. CREATE TABLE IF NOT EXISTS is a no-op on an
// existing database, so the column also needs an ADD COLUMN (ADR 0027).
db.NewMigrateLogger(s.db, nil).Apply(
"auth_invites.org_id",
`ALTER TABLE auth_invites ADD COLUMN org_id TEXT NOT NULL DEFAULT ''`,
)
return nil
}