package models import ( "time" "github.com/google/uuid" "gorm.io/gorm" ) type TicketStatus string const ( TicketStatusOpen TicketStatus = "open" TicketStatusInProgress TicketStatus = "in_progress" TicketStatusClosed TicketStatus = "closed" ) 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"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` } 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" json:"id"` UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"` RepoID uuid.UUID `gorm:"type:uuid;not null;index" json:"repo_id"` Repo Repo `gorm:"foreignKey:RepoID;constraint:OnDelete:CASCADE" json:"-"` Title string `gorm:"not null" json:"title"` Description string `gorm:"not null" json:"description"` Status TicketStatus `gorm:"type:ticket_status;not null;default:'open';index" json:"status"` ForgejoIssueNumber *int64 `json:"forgejo_issue_number"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` } type TicketComment struct { ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"` TicketID uuid.UUID `gorm:"type:uuid;not null;index" json:"ticket_id"` Ticket Ticket `gorm:"foreignKey:TicketID;constraint:OnDelete:CASCADE" json:"-"` UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"` Body string `gorm:"not null" json:"body"` ForgejoCommentID *int64 `json:"forgejo_comment_id"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` } 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. // Note: enum types and partial indexes must be created via SQL migrations. func AutoMigrate(db *gorm.DB) error { // Create enum types if they don't exist db.Exec("DO $$ BEGIN CREATE TYPE ticket_status AS ENUM ('open', 'in_progress', 'closed'); EXCEPTION WHEN duplicate_object THEN null; END $$;") db.Exec("DO $$ BEGIN CREATE TYPE token_type AS ENUM ('verify_email', 'reset_password'); EXCEPTION WHEN duplicate_object THEN null; END $$;") if err := db.AutoMigrate( &User{}, &OAuthAccount{}, &Session{}, &Repo{}, &Ticket{}, &TicketComment{}, &EmailToken{}, ); 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 partial 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) WHERE forgejo_issue_number IS NOT NULL") return nil }