bab29a83fa
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
248 lines
7.0 KiB
Go
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
|
|
}
|