Files
jmiller bab29a83fa refactor: rename Go module path MokoConsulting/MokoGitea -> MokoConsulting/MokoGIT
Sweep all .go imports + go.mod, plus module-path refs in .golangci.yml,
Dockerfile, swagger templates, locale example, and repo/wiki URLs
(MokoGitea-Fork -> MokoGIT). Part of the MokoGIT rebrand.

Claude-Session: https://claude.ai/code/session_01Bqe7fAuHQeiLueYfeHFrHw
2026-07-14 15:31:19 -05:00

248 lines
7.0 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/MokoGIT/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/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
}
// The nav icon doubles as the app icon (logo.png) — used as the PWA / web
// manifest icon and the navbar fallback — so branding the nav icon brands
// the app icon too. Write the uploaded image to both in a single pass. See #773.
targets := []string{filename}
if imageType == "nav-icon" {
targets = append(targets, "logo.png")
}
// Write the uploaded image to each target independently (rewinding the
// stream between targets) so a failure on a later target never leaves an
// earlier one truncated/empty.
for i, name := range targets {
if i > 0 {
if _, err := file.Seek(0, io.SeekStart); err != nil {
ctx.Flash.Error("Failed to write image")
log.Error("Seek branding image %s: %v", name, err)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
}
destPath := filepath.Join(imgDir, name)
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
}
if _, err := io.Copy(dest, file); err != nil {
dest.Close()
ctx.Flash.Error("Failed to write image")
log.Error("Copy to %s: %v", destPath, err)
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
return
}
dest.Close()
// Remove any SVG override so the uploaded PNG takes effect.
svgPath := filepath.Join(imgDir, name[:len(name)-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
}
// The nav icon also sets the app icon (logo.png), so resetting it reverts
// both back to the built-in default. See #773.
targets := []string{filename}
if imageType == "nav-icon" {
targets = append(targets, "logo.png")
}
removedAny := false
failed := false
for _, name := range targets {
path := filepath.Join(brandingImageDir(), name)
if !fileExists(path) {
continue
}
if err := os.Remove(path); err != nil {
ctx.Flash.Error("Failed to remove custom image")
log.Error("Remove %s: %v", path, err)
failed = true
} else {
removedAny = true
log.Info("Branding reset to default: %s", name)
}
}
switch {
case failed:
// error flash already set
case removedAny:
ctx.Flash.Success("Reset to default: " + imageType)
default:
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
}