Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f69212859a | |||
| 4ef4aeb04a | |||
| 4ec61ec260 | |||
| b5defc2a4a | |||
| d77713dd77 |
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,118 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"git.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["HasLogo"] = fileExists(filepath.Join(imgDir, "logo.png"))
|
||||
ctx.Data["HasFavicon"] = fileExists(filepath.Join(imgDir, "favicon.png"))
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBranding)
|
||||
}
|
||||
|
||||
// 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 "logo":
|
||||
filename = "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()
|
||||
|
||||
// Validate file size (max 2MB)
|
||||
if header.Size > 2*1024*1024 {
|
||||
ctx.Flash.Error("File too large (max 2MB)")
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/branding")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the custom image directory exists
|
||||
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
|
||||
}
|
||||
|
||||
// Write the file
|
||||
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
|
||||
}
|
||||
|
||||
// Also remove SVG override if present (PNG should take priority)
|
||||
svgPath := filepath.Join(imgDir, filename[:len(filename)-4]+".svg")
|
||||
if fileExists(svgPath) {
|
||||
os.Remove(svgPath)
|
||||
log.Info("Removed SVG override: %s", 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")
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: 25},
|
||||
}
|
||||
|
||||
members, _, err := organization.FindOrgMembers(ctx, opts)
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgMembers", err)
|
||||
return
|
||||
@@ -102,6 +102,14 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
const orgOverviewTeamsLimit = 5
|
||||
ctx.Data["OrgOverviewMembers"] = members
|
||||
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["NumMembers"] = len(members)
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
|
||||
ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
|
||||
ctx.Data["IsPublicMember"] = func(uid int64) bool {
|
||||
return membersIsPublic[uid]
|
||||
}
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
|
||||
@@ -765,6 +765,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
})
|
||||
|
||||
m.Group("/branding", func() {
|
||||
m.Get("", admin.Branding)
|
||||
m.Post("/upload", admin.BrandingUpload)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
m.Get("/stats", admin.MonitorStats)
|
||||
m.Get("/cron", admin.CronTasks)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
{{template "admin/layout_head" (dict "pageClass" "admin branding")}}
|
||||
<div class="admin-setting-content">
|
||||
<h4 class="ui top attached header">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>Upload custom branding images. Changes take effect immediately — no restart required.</p>
|
||||
|
||||
<div class="ui three stackable cards">
|
||||
<!-- Nav Icon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Nav Icon</div>
|
||||
<div class="meta">Top-left corner (30x30px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo-small.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="nav-icon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasNavIcon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Logo -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Login Logo</div>
|
||||
<div class="meta">Login page and homepage (wide format recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/logo.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="logo">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasLogo}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favicon -->
|
||||
<div class="card">
|
||||
<div class="content">
|
||||
<div class="header">Favicon</div>
|
||||
<div class="meta">Browser tab icon (256x256px recommended)</div>
|
||||
</div>
|
||||
<div class="image tw-p-4 tw-text-center" style="background: var(--color-body);">
|
||||
<img src="{{AssetUrlPrefix}}/img/favicon.png?v={{ctx.CspScriptNonce}}" style="max-height: 64px;">
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/branding/upload" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="type" value="favicon">
|
||||
<div class="ui action input tw-w-full">
|
||||
<input type="file" name="file" accept="image/png,image/svg+xml,image/x-icon" required>
|
||||
<button type="submit" class="ui primary button">{{svg "octicon-upload" 14}} Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
{{if .HasFavicon}}<span class="ui green label tw-mt-2">Custom</span>{{else}}<span class="ui grey label tw-mt-2">Default</span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_tail" .}}
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui fluid vertical menu">
|
||||
<div class="ui fluid vertical menu" style="text-align: left !important;">
|
||||
<div class="header item">{{ctx.Locale.Tr "admin.settings"}}</div>
|
||||
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminDashboard .PageIsAdminSelfCheck}}open{{end}}>
|
||||
@@ -84,6 +84,9 @@
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
<a class="{{if .PageIsAdminBranding}}active {{end}}item" href="{{AppSubUrl}}/-/admin/branding">
|
||||
{{svg "octicon-paintbrush" 16}} Branding
|
||||
</a>
|
||||
<details class="item toggleable-item" {{if or .PageIsAdminConfig}}open{{end}}>
|
||||
<summary>{{ctx.Locale.Tr "admin.config"}}</summary>
|
||||
<div class="menu">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="navbar-left">
|
||||
<!-- the logo -->
|
||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true" onerror="this.src='{{AssetUrlPrefix}}/img/logo.png'">
|
||||
</a>
|
||||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
|
||||
<div class="tw-mb-8 tw-px-8">
|
||||
<div class="center">
|
||||
<img class="logo" width="220" height="220" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.png" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
{{AppName}}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="ui container tw-flex">
|
||||
<div class="item tw-flex-1">
|
||||
<a href="{{AppSubUrl}}/" aria-label="{{ctx.Locale.Tr "home_title"}}">
|
||||
<img width="30" height="30" src="https://mokoconsulting.tech/images/branding/logo.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo-small.png" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
</a>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
Reference in New Issue
Block a user