forgejo-tickets/internal/models/models.go

136 lines
5.6 KiB
Go

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"`
SSOPublicKey *string `json:"sso_public_key"`
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
}