130 lines
5.7 KiB
Go
130 lines
5.7 KiB
Go
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
|
|
}
|