9a5720e8ad
Branch Policy Check / Verify merge target (pull_request) Has been cancelled
Universal: PR Check / Branch Policy (pull_request) Has been cancelled
PR RC Release / Build RC Release (pull_request) Has been cancelled
Universal: PR Check / Validate PR (pull_request) Has been cancelled
Branch Cleanup / Delete merged branch (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Full namespace migration: update the Go module path and all import statements from git.mokoconsulting.tech to code.mokoconsulting.tech. Also updates all URL references in templates, workflows, configs, tests, and documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package admin
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
|
)
|
|
|
|
const tplBranding templates.TplName = "admin/branding"
|
|
|
|
// brandingImageDir returns the path to the custom branding images directory.
|
|
func brandingImageDir() string {
|
|
return filepath.Join(setting.CustomPath, "public", "assets", "img")
|
|
}
|
|
|
|
// Branding shows the admin branding page.
|
|
func Branding(ctx *context.Context) {
|
|
ctx.Data["Title"] = "Branding"
|
|
ctx.Data["PageIsAdminBranding"] = true
|
|
|
|
imgDir := brandingImageDir()
|
|
ctx.Data["HasNavIcon"] = fileExists(filepath.Join(imgDir, "logo-small.png"))
|
|
ctx.Data["HasLoginLogo"] = fileExists(filepath.Join(imgDir, "login-logo.png"))
|
|
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
|
|
|
|
ctx.Data["MetaDescription"] = setting.UI.Meta.Description
|
|
ctx.Data["MetaAuthor"] = setting.UI.Meta.Author
|
|
ctx.Data["HelpURL"] = setting.HelpURL
|
|
ctx.Data["SupportURL"] = setting.SupportURL
|
|
|
|
ctx.HTML(http.StatusOK, tplBranding)
|
|
}
|
|
|
|
// BrandingSettings handles the text branding form submission.
|
|
func BrandingSettings(ctx *context.Context) {
|
|
appName := ctx.FormString("app_name")
|
|
description := ctx.FormString("description")
|
|
helpURL := ctx.FormString("help_url")
|
|
supportURL := ctx.FormString("support_url")
|
|
author := ctx.FormString("author")
|
|
|
|
// Update in-memory settings
|
|
if appName != "" {
|
|
setting.AppName = appName
|
|
}
|
|
if description != "" {
|
|
setting.UI.Meta.Description = description
|
|
}
|
|
setting.HelpURL = helpURL
|
|
setting.SupportURL = supportURL
|
|
if author != "" {
|
|
setting.UI.Meta.Author = author
|
|
}
|
|
|
|
// Persist to app.ini
|
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
if err != nil {
|
|
ctx.Flash.Error("Failed to load config: " + err.Error())
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
if appName != "" {
|
|
cfg.Section("").Key("APP_NAME").SetValue(appName)
|
|
}
|
|
if description != "" {
|
|
cfg.Section("ui.meta").Key("DESCRIPTION").SetValue(description)
|
|
}
|
|
cfg.Section("").Key("HELP_URL").SetValue(helpURL)
|
|
cfg.Section("").Key("SUPPORT_URL").SetValue(supportURL)
|
|
if author != "" {
|
|
cfg.Section("ui.meta").Key("AUTHOR").SetValue(author)
|
|
}
|
|
|
|
if err := cfg.SaveTo(setting.CustomConf); err != nil {
|
|
ctx.Flash.Error("Failed to save config: " + err.Error())
|
|
log.Error("SaveTo %s: %v", setting.CustomConf, err)
|
|
} else {
|
|
ctx.Flash.Success("Branding settings saved")
|
|
log.Info("Branding settings updated: AppName=%s, Author=%s", appName, author)
|
|
}
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
}
|
|
|
|
// BrandingUpload handles branding image uploads.
|
|
func BrandingUpload(ctx *context.Context) {
|
|
imageType := ctx.FormString("type")
|
|
if imageType == "" {
|
|
ctx.Flash.Error("No image type specified")
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
var filename string
|
|
switch imageType {
|
|
case "nav-icon":
|
|
filename = "logo-small.png"
|
|
case "login-logo":
|
|
filename = "login-logo.png"
|
|
case "favicon":
|
|
filename = "favicon.png"
|
|
default:
|
|
ctx.Flash.Error("Invalid image type: " + imageType)
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
file, header, err := ctx.Req.FormFile("file")
|
|
if err != nil {
|
|
ctx.Flash.Error("Upload failed: " + err.Error())
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
if header.Size > 2*1024*1024 {
|
|
ctx.Flash.Error("File too large (max 2MB)")
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
imgDir := brandingImageDir()
|
|
if err := os.MkdirAll(imgDir, 0o755); err != nil {
|
|
ctx.Flash.Error("Failed to create image directory")
|
|
log.Error("MkdirAll %s: %v", imgDir, err)
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
destPath := filepath.Join(imgDir, filename)
|
|
dest, err := os.Create(destPath)
|
|
if err != nil {
|
|
ctx.Flash.Error("Failed to save image")
|
|
log.Error("Create %s: %v", destPath, err)
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
defer dest.Close()
|
|
|
|
if _, err := io.Copy(dest, file); err != nil {
|
|
ctx.Flash.Error("Failed to write image")
|
|
log.Error("Copy to %s: %v", destPath, err)
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
// Remove SVG override if present
|
|
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
|
if fileExists(svgPath) {
|
|
os.Remove(svgPath)
|
|
}
|
|
|
|
ctx.Flash.Success("Branding image updated: " + imageType)
|
|
log.Info("Branding image uploaded: %s (%d bytes)", filename, header.Size)
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
}
|
|
|
|
// BrandingReset removes a custom branding image, reverting to the built-in default.
|
|
func BrandingReset(ctx *context.Context) {
|
|
imageType := ctx.FormString("type")
|
|
|
|
var filename string
|
|
switch imageType {
|
|
case "nav-icon":
|
|
filename = "logo-small.png"
|
|
case "login-logo":
|
|
filename = "login-logo.png"
|
|
case "favicon":
|
|
filename = "favicon.png"
|
|
default:
|
|
ctx.Flash.Error("Invalid image type")
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
return
|
|
}
|
|
|
|
path := filepath.Join(brandingImageDir(), filename)
|
|
if fileExists(path) {
|
|
if err := os.Remove(path); err != nil {
|
|
ctx.Flash.Error("Failed to remove custom image")
|
|
log.Error("Remove %s: %v", path, err)
|
|
} else {
|
|
ctx.Flash.Success("Reset to default: " + imageType)
|
|
log.Info("Branding reset to default: %s", filename)
|
|
}
|
|
} else {
|
|
ctx.Flash.Info("Already using default: " + imageType)
|
|
}
|
|
|
|
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|