183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|||
|
|
|
|||
|
|
package admin
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"sourcedock.dev/petrbalvin/volumen/internal/store"
|
|||
|
|
"sourcedock.dev/petrbalvin/volumen/internal/tokens"
|
|||
|
|
"sourcedock.dev/petrbalvin/volumen/internal/users"
|
|||
|
|
"sourcedock.dev/petrbalvin/volumen/internal/webhooks"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// roleOption is one <option> of a role select.
|
|||
|
|
type roleOption struct {
|
|||
|
|
Value string
|
|||
|
|
Selected bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// userRow is one entry of the users panel.
|
|||
|
|
type userRow struct {
|
|||
|
|
Username string
|
|||
|
|
Display string
|
|||
|
|
Initial string
|
|||
|
|
Role string
|
|||
|
|
Photo string
|
|||
|
|
IsSelf bool
|
|||
|
|
RoleOptions []roleOption
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// hookRow is one configured webhook endpoint.
|
|||
|
|
type hookRow struct {
|
|||
|
|
Index string
|
|||
|
|
URL string
|
|||
|
|
// Managed is false for a hook declared in config.toml, which the
|
|||
|
|
// settings forms may test but not change.
|
|||
|
|
Managed bool
|
|||
|
|
Enabled bool
|
|||
|
|
Signed bool
|
|||
|
|
EventsText string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// deliveryRow is one webhook delivery log entry.
|
|||
|
|
type deliveryRow struct {
|
|||
|
|
Timestamp string
|
|||
|
|
Event string
|
|||
|
|
HookURL string
|
|||
|
|
OK bool
|
|||
|
|
StatusCode int
|
|||
|
|
Error string
|
|||
|
|
Attempts int
|
|||
|
|
|
|||
|
|
// Result is the pre-formatted failure text ("failed (N attempts)"),
|
|||
|
|
// translated by the caller that knows the request's language.
|
|||
|
|
Result string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// tokenRow is one API token table row.
|
|||
|
|
type tokenRow struct {
|
|||
|
|
Name string
|
|||
|
|
CreatedDay string
|
|||
|
|
LastUsedDay string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// mediaRow is one media library tile.
|
|||
|
|
type mediaRow struct {
|
|||
|
|
Name string
|
|||
|
|
URL string
|
|||
|
|
SizeKB string
|
|||
|
|
// Dimensions is the header-carried pixel size ("1920 × 1080"), or
|
|||
|
|
// "" when the container did not yield one.
|
|||
|
|
Dimensions string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func roleOptions(current string) []roleOption {
|
|||
|
|
out := make([]roleOption, 0, len(users.Roles))
|
|||
|
|
for _, role := range users.Roles {
|
|||
|
|
out = append(out, roleOption{Value: role, Selected: role == current})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func userRows(current string, list []*users.User) []userRow {
|
|||
|
|
out := make([]userRow, 0, len(list))
|
|||
|
|
for _, user := range list {
|
|||
|
|
display := user.Name
|
|||
|
|
if display == "" {
|
|||
|
|
display = user.Username
|
|||
|
|
}
|
|||
|
|
out = append(out, userRow{
|
|||
|
|
Username: user.Username,
|
|||
|
|
Display: display,
|
|||
|
|
Initial: firstUpper(display, "?"),
|
|||
|
|
Role: user.Role,
|
|||
|
|
Photo: user.Photo,
|
|||
|
|
IsSelf: user.Username == current,
|
|||
|
|
RoleOptions: roleOptions(user.Role),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// hookRows renders the manager's merged hook list; the first static
|
|||
|
|
// count came from config.toml and the rest are the admin's to manage.
|
|||
|
|
func hookRows(hooks []webhooks.Webhook, staticCount int) []hookRow {
|
|||
|
|
out := make([]hookRow, 0, len(hooks))
|
|||
|
|
for i, hook := range hooks {
|
|||
|
|
eventsText := "all"
|
|||
|
|
if len(hook.Events) > 0 {
|
|||
|
|
eventsText = strings.Join(hook.Events, ", ")
|
|||
|
|
}
|
|||
|
|
out = append(out, hookRow{
|
|||
|
|
Index: fmt.Sprintf("%d", i),
|
|||
|
|
URL: hook.URL,
|
|||
|
|
Managed: i >= staticCount,
|
|||
|
|
Enabled: hook.Enabled,
|
|||
|
|
Signed: hook.Secret != "",
|
|||
|
|
EventsText: eventsText,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func deliveryRows(list []webhooks.Delivery) []deliveryRow {
|
|||
|
|
out := make([]deliveryRow, 0, len(list))
|
|||
|
|
for _, d := range list {
|
|||
|
|
out = append(out, deliveryRow{
|
|||
|
|
Timestamp: d.Timestamp,
|
|||
|
|
Event: d.Event,
|
|||
|
|
HookURL: d.HookURL,
|
|||
|
|
OK: d.Status == "ok",
|
|||
|
|
StatusCode: d.StatusCode,
|
|||
|
|
Error: d.Error,
|
|||
|
|
Attempts: d.Attempts,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func tokenRows(list []tokens.Token) []tokenRow {
|
|||
|
|
out := make([]tokenRow, 0, len(list))
|
|||
|
|
for _, token := range list {
|
|||
|
|
lastUsed := "never"
|
|||
|
|
if len(token.LastUsed) >= 10 {
|
|||
|
|
lastUsed = token.LastUsed[:10]
|
|||
|
|
}
|
|||
|
|
created := token.Created
|
|||
|
|
if len(created) >= 10 {
|
|||
|
|
created = created[:10]
|
|||
|
|
}
|
|||
|
|
out = append(out, tokenRow{
|
|||
|
|
Name: token.Name,
|
|||
|
|
CreatedDay: created,
|
|||
|
|
LastUsedDay: lastUsed,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func mediaRows(list []store.Media) []mediaRow {
|
|||
|
|
out := make([]mediaRow, 0, len(list))
|
|||
|
|
for _, item := range list {
|
|||
|
|
out = append(out, mediaRow{
|
|||
|
|
Name: item.Name,
|
|||
|
|
URL: item.URL,
|
|||
|
|
SizeKB: fmt.Sprintf("%.1f", float64(item.Size)/1024),
|
|||
|
|
Dimensions: pixelSize(item.Width, item.Height),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// pixelSize renders the header-carried pixel size, empty when the
|
|||
|
|
// container did not yield one.
|
|||
|
|
func pixelSize(width, height int) string {
|
|||
|
|
if width <= 0 || height <= 0 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%d × %d", width, height)
|
|||
|
|
}
|