135 lines
5.5 KiB
Go
135 lines
5.5 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"`
|
|
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
|
|
}
|