Phase 3: Organization Public Profile Page - Pinned repositories with groups - Public members display with roles - API endpoints for pinned repos and groups Phase 4: Gitea Pages Foundation - Landing page templates (simple, docs, product, portfolio) - Custom domain support with verification - YAML configuration parser (.gitea/landing.yaml) - Repository settings UI for pages Phase 5: Enhanced Wiki System with V2 API - Full CRUD operations via v2 API - Full-text search with WikiIndex table - Link graph visualization - Wiki health metrics (orphaned, dead links, outdated) - Designed for external AI plugin integration - Developer guide for .NET integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
310 lines
9.0 KiB
Go
310 lines
9.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
)
|
|
|
|
// PagesTemplate represents the type of landing page template
|
|
type PagesTemplate string
|
|
|
|
const (
|
|
PagesTemplateSimple PagesTemplate = "simple"
|
|
PagesTemplateDocumentation PagesTemplate = "documentation"
|
|
PagesTemplateProduct PagesTemplate = "product"
|
|
PagesTemplatePortfolio PagesTemplate = "portfolio"
|
|
)
|
|
|
|
// SSLStatus represents the SSL certificate status
|
|
type SSLStatus string
|
|
|
|
const (
|
|
SSLStatusPending SSLStatus = "pending"
|
|
SSLStatusActive SSLStatus = "active"
|
|
SSLStatusExpiring SSLStatus = "expiring"
|
|
SSLStatusExpired SSLStatus = "expired"
|
|
SSLStatusError SSLStatus = "error"
|
|
)
|
|
|
|
func init() {
|
|
db.RegisterModel(new(PagesDomain))
|
|
db.RegisterModel(new(PagesConfig))
|
|
}
|
|
|
|
// PagesDomain represents a custom domain mapping for Gitea Pages
|
|
type PagesDomain struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
RepoID int64 `xorm:"INDEX NOT NULL"`
|
|
Domain string `xorm:"UNIQUE NOT NULL"`
|
|
Verified bool `xorm:"DEFAULT false"`
|
|
VerificationToken string `xorm:"VARCHAR(64)"`
|
|
SSLStatus SSLStatus `xorm:"VARCHAR(32) DEFAULT 'pending'"`
|
|
SSLCertExpiry timeutil.TimeStamp `xorm:"DEFAULT 0"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
VerifiedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
|
|
|
|
Repo *Repository `xorm:"-"`
|
|
}
|
|
|
|
// TableName returns the table name for PagesDomain
|
|
func (d *PagesDomain) TableName() string {
|
|
return "pages_domain"
|
|
}
|
|
|
|
// PagesConfig represents the cached configuration for a repository's landing page
|
|
type PagesConfig struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
RepoID int64 `xorm:"UNIQUE NOT NULL"`
|
|
Enabled bool `xorm:"DEFAULT false"`
|
|
Template PagesTemplate `xorm:"VARCHAR(32) DEFAULT 'simple'"`
|
|
ConfigJSON string `xorm:"TEXT"` // Cached parsed config from landing.yaml
|
|
ConfigHash string `xorm:"VARCHAR(64)"` // Hash for invalidation
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
|
|
|
Repo *Repository `xorm:"-"`
|
|
}
|
|
|
|
// TableName returns the table name for PagesConfig
|
|
func (c *PagesConfig) TableName() string {
|
|
return "pages_config"
|
|
}
|
|
|
|
// GenerateVerificationToken generates a random token for domain verification
|
|
func GenerateVerificationToken() (string, error) {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
// GetPagesDomainByID returns a pages domain by ID
|
|
func GetPagesDomainByID(ctx context.Context, id int64) (*PagesDomain, error) {
|
|
domain := new(PagesDomain)
|
|
has, err := db.GetEngine(ctx).ID(id).Get(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, ErrPagesDomainNotExist{ID: id}
|
|
}
|
|
return domain, nil
|
|
}
|
|
|
|
// GetPagesDomainByDomain returns a pages domain by domain name
|
|
func GetPagesDomainByDomain(ctx context.Context, domainName string) (*PagesDomain, error) {
|
|
domain := new(PagesDomain)
|
|
has, err := db.GetEngine(ctx).Where("domain = ?", strings.ToLower(domainName)).Get(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, ErrPagesDomainNotExist{Domain: domainName}
|
|
}
|
|
return domain, nil
|
|
}
|
|
|
|
// GetPagesDomainsByRepoID returns all custom domains for a repository
|
|
func GetPagesDomainsByRepoID(ctx context.Context, repoID int64) ([]*PagesDomain, error) {
|
|
domains := make([]*PagesDomain, 0, 5)
|
|
return domains, db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&domains)
|
|
}
|
|
|
|
// CreatePagesDomain creates a new custom domain for pages
|
|
func CreatePagesDomain(ctx context.Context, domain *PagesDomain) error {
|
|
// Normalize domain to lowercase
|
|
domain.Domain = strings.ToLower(domain.Domain)
|
|
|
|
// Generate verification token
|
|
token, err := GenerateVerificationToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
domain.VerificationToken = token
|
|
domain.SSLStatus = SSLStatusPending
|
|
|
|
_, err = db.GetEngine(ctx).Insert(domain)
|
|
return err
|
|
}
|
|
|
|
// UpdatePagesDomain updates a pages domain
|
|
func UpdatePagesDomain(ctx context.Context, domain *PagesDomain) error {
|
|
_, err := db.GetEngine(ctx).ID(domain.ID).AllCols().Update(domain)
|
|
return err
|
|
}
|
|
|
|
// DeletePagesDomain deletes a pages domain
|
|
func DeletePagesDomain(ctx context.Context, id int64) error {
|
|
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))
|
|
return err
|
|
}
|
|
|
|
// DeletePagesDomainsByRepoID deletes all pages domains for a repository
|
|
func DeletePagesDomainsByRepoID(ctx context.Context, repoID int64) error {
|
|
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(PagesDomain))
|
|
return err
|
|
}
|
|
|
|
// VerifyPagesDomain marks a domain as verified
|
|
func VerifyPagesDomain(ctx context.Context, id int64) error {
|
|
_, err := db.GetEngine(ctx).ID(id).Cols("verified", "verified_unix").Update(&PagesDomain{
|
|
Verified: true,
|
|
VerifiedUnix: timeutil.TimeStampNow(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetPagesConfigByRepoID returns the pages config for a repository
|
|
func GetPagesConfigByRepoID(ctx context.Context, repoID int64) (*PagesConfig, error) {
|
|
config := new(PagesConfig)
|
|
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, nil // No config means pages not enabled
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// CreatePagesConfig creates a new pages config for a repository
|
|
func CreatePagesConfig(ctx context.Context, config *PagesConfig) error {
|
|
_, err := db.GetEngine(ctx).Insert(config)
|
|
return err
|
|
}
|
|
|
|
// UpdatePagesConfig updates a pages config
|
|
func UpdatePagesConfig(ctx context.Context, config *PagesConfig) error {
|
|
_, err := db.GetEngine(ctx).ID(config.ID).AllCols().Update(config)
|
|
return err
|
|
}
|
|
|
|
// DeletePagesConfig deletes a pages config
|
|
func DeletePagesConfig(ctx context.Context, repoID int64) error {
|
|
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(PagesConfig))
|
|
return err
|
|
}
|
|
|
|
// EnablePages enables pages for a repository
|
|
func EnablePages(ctx context.Context, repoID int64, template PagesTemplate) error {
|
|
config, err := GetPagesConfigByRepoID(ctx, repoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if config == nil {
|
|
// Create new config
|
|
config = &PagesConfig{
|
|
RepoID: repoID,
|
|
Enabled: true,
|
|
Template: template,
|
|
}
|
|
return CreatePagesConfig(ctx, config)
|
|
}
|
|
|
|
// Update existing config
|
|
config.Enabled = true
|
|
config.Template = template
|
|
return UpdatePagesConfig(ctx, config)
|
|
}
|
|
|
|
// DisablePages disables pages for a repository
|
|
func DisablePages(ctx context.Context, repoID int64) error {
|
|
config, err := GetPagesConfigByRepoID(ctx, repoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if config == nil {
|
|
return nil // Already disabled
|
|
}
|
|
|
|
config.Enabled = false
|
|
return UpdatePagesConfig(ctx, config)
|
|
}
|
|
|
|
// IsPagesEnabled checks if pages is enabled for a repository
|
|
func IsPagesEnabled(ctx context.Context, repoID int64) (bool, error) {
|
|
config, err := GetPagesConfigByRepoID(ctx, repoID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return config != nil && config.Enabled, nil
|
|
}
|
|
|
|
// ErrPagesDomainNotExist represents a "pages domain not exist" error
|
|
type ErrPagesDomainNotExist struct {
|
|
ID int64
|
|
Domain string
|
|
}
|
|
|
|
func (err ErrPagesDomainNotExist) Error() string {
|
|
if err.Domain != "" {
|
|
return fmt.Sprintf("pages domain does not exist [domain: %s]", err.Domain)
|
|
}
|
|
return fmt.Sprintf("pages domain does not exist [id: %d]", err.ID)
|
|
}
|
|
|
|
// IsErrPagesDomainNotExist checks if an error is ErrPagesDomainNotExist
|
|
func IsErrPagesDomainNotExist(err error) bool {
|
|
_, ok := err.(ErrPagesDomainNotExist)
|
|
return ok
|
|
}
|
|
|
|
// ErrPagesDomainAlreadyExist represents a "pages domain already exist" error
|
|
type ErrPagesDomainAlreadyExist struct {
|
|
Domain string
|
|
}
|
|
|
|
func (err ErrPagesDomainAlreadyExist) Error() string {
|
|
return fmt.Sprintf("pages domain already exists [domain: %s]", err.Domain)
|
|
}
|
|
|
|
// IsErrPagesDomainAlreadyExist checks if an error is ErrPagesDomainAlreadyExist
|
|
func IsErrPagesDomainAlreadyExist(err error) bool {
|
|
_, ok := err.(ErrPagesDomainAlreadyExist)
|
|
return ok
|
|
}
|
|
|
|
// ErrPagesConfigNotExist represents a "pages config not exist" error
|
|
type ErrPagesConfigNotExist struct {
|
|
RepoID int64
|
|
}
|
|
|
|
func (err ErrPagesConfigNotExist) Error() string {
|
|
return fmt.Sprintf("pages config does not exist [repo_id: %d]", err.RepoID)
|
|
}
|
|
|
|
// IsErrPagesConfigNotExist checks if an error is ErrPagesConfigNotExist
|
|
func IsErrPagesConfigNotExist(err error) bool {
|
|
_, ok := err.(ErrPagesConfigNotExist)
|
|
return ok
|
|
}
|
|
|
|
// GetPagesConfig returns the pages config for a repository with proper error handling
|
|
func GetPagesConfig(ctx context.Context, repoID int64) (*PagesConfig, error) {
|
|
config := new(PagesConfig)
|
|
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, ErrPagesConfigNotExist{RepoID: repoID}
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// GetPagesDomains returns all custom domains for a repository
|
|
func GetPagesDomains(ctx context.Context, repoID int64) ([]*PagesDomain, error) {
|
|
return GetPagesDomainsByRepoID(ctx, repoID)
|
|
}
|