package models import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) type TokenType string const ( TokenTypeVerifyEmail TokenType = "verify_email" TokenTypeResetPassword TokenType = "reset_password" ) type User struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` Email string `gorm:"uniqueIndex;not null" json:"email"` PasswordHash *string `json:"-"` Name string `gorm:"not null" json:"name"` EmailVerified bool `gorm:"not null;default:false" json:"email_verified"` Approved bool `gorm:"not null;default:false" json:"approved"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` } type UserRepo struct { UserID uuid.UUID `gorm:"type:uuid;not null;primaryKey"` RepoID uuid.UUID `gorm:"type:uuid;not null;primaryKey"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` Repo Repo `gorm:"foreignKey:RepoID;constraint:OnDelete:CASCADE"` } type OAuthAccount struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"` Provider string `gorm:"not null" json:"provider"` ProviderUserID string `gorm:"not null" json:"provider_user_id"` Email string `gorm:"not null" json:"email"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` } func (OAuthAccount) TableName() string { return "oauth_accounts" } type Session struct { Token string `gorm:"primaryKey" json:"token"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"` Data []byte `gorm:"not null" json:"data"` ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"` } type Repo struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` Name string `gorm:"not null" json:"name"` Slug string `gorm:"uniqueIndex;not null" json:"slug"` ForgejoOwner string `gorm:"not null" json:"forgejo_owner"` ForgejoRepo string `gorm:"not null" json:"forgejo_repo"` WebhookSecret string `gorm:"not null" json:"webhook_secret"` WebhookVerified bool `gorm:"not null;default:false" json:"webhook_verified"` WebhookVerifiedAt *time.Time `json:"webhook_verified_at"` Active bool `gorm:"not null;default:true" json:"active"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` } type Ticket struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey"` UserID uuid.UUID `gorm:"type:uuid;not null;index"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE"` RepoID uuid.UUID `gorm:"type:uuid;not null;index"` Repo Repo `gorm:"foreignKey:RepoID;constraint:OnDelete:CASCADE"` ForgejoIssueNumber int64 `gorm:"not null"` CreatedAt time.Time `gorm:"not null;default:now()"` } type EmailToken struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"` TokenHash string `gorm:"uniqueIndex;not null" json:"token_hash"` TokenType TokenType `gorm:"type:token_type;not null" json:"token_type"` ExpiresAt time.Time `gorm:"not null" json:"expires_at"` UsedAt *time.Time `json:"used_at"` } // AutoMigrate runs GORM auto-migration for all models. func AutoMigrate(db *gorm.DB) error { // Create enum types if they don't exist db.Exec("DO $$ BEGIN CREATE TYPE token_type AS ENUM ('verify_email', 'reset_password'); EXCEPTION WHEN duplicate_object THEN null; END $$;") // Migration: Drop ticket_comments table (no longer used) db.Exec("DROP TABLE IF EXISTS ticket_comments") // Migration: Drop removed columns from tickets db.Exec("ALTER TABLE tickets DROP COLUMN IF EXISTS title") db.Exec("ALTER TABLE tickets DROP COLUMN IF EXISTS description") db.Exec("ALTER TABLE tickets DROP COLUMN IF EXISTS status") db.Exec("ALTER TABLE tickets DROP COLUMN IF EXISTS updated_at") // Migration: Delete any tickets without a Forgejo issue number, then make it NOT NULL db.Exec("DELETE FROM tickets WHERE forgejo_issue_number IS NULL") db.Exec("ALTER TABLE tickets ALTER COLUMN forgejo_issue_number SET NOT NULL") // Drop the old partial unique index db.Exec("DROP INDEX IF EXISTS idx_tickets_repo_forgejo_issue") // Drop the old ticket_status enum type (no longer used) db.Exec("DROP TYPE IF EXISTS ticket_status") if err := db.AutoMigrate( &User{}, &OAuthAccount{}, &Session{}, &Repo{}, &Ticket{}, &EmailToken{}, &UserRepo{}, ); err != nil { return err } // Create unique composite index for oauth_accounts db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_provider_user ON oauth_accounts(provider, provider_user_id)") // Create unique index for ticket forgejo issue lookup db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_tickets_repo_forgejo_issue ON tickets(repo_id, forgejo_issue_number)") // Approve all existing verified users so they aren't locked out db.Exec("UPDATE users SET approved = true WHERE approved = false AND email_verified = true") return nil }