Files
2026-09-16 05:36:36 +02:00

156 lines
7.4 KiB
Go

package web
import (
"fmt"
"net/url"
"strings"
)
// webRoutes is the single source of truth for browser-facing dynamic paths.
// Parameter values are passed to webPath in the order in which they occur here.
var webRoutes = map[string]string{
"home": "/",
"healthcheck": "/healtcheck",
"privacy": "/datenschutz",
"login": "/login",
"activate": "/activate",
"logout": "/logout",
"account": "/account",
"account.profile": "/account/profile",
"account.password": "/account/password",
"account.email": "/account/email",
"account.email-confirm": "/account/email-confirm",
"account.session.delete": "/account/sessions/:sessionID/delete",
"settings": "/settings",
"admin": "/admin",
"admin.user.invite": "/admin/user-invite",
"admin.user.role": "/admin/users/:userID/role",
"admin.user.delete": "/admin/users/:userID/delete",
"admin.role.new": "/admin/roles/new",
"admin.role.update": "/admin/roles/update",
"admin.role.delete": "/admin/roles/delete",
"admin.application-settings": "/admin/application-settings",
"admin.test-mail": "/admin/test-mail",
"admin.species-category.new": "/admin/species-category-new",
"admin.species-category.edit": "/admin/species-category-edit/:categoryID",
"admin.species-category.delete": "/admin/species-category-delete/:categoryID",
"admin.task-priority.new": "/admin/task-priority-new",
"admin.task-priority.edit": "/admin/task-priority-edit/:priorityID",
"admin.task-priority.delete": "/admin/task-priority-delete/:priorityID",
"gardens": "/gardens",
"garden.new": "/gardens/new",
"garden.edit": "/gardens/edit/:gardenID",
"garden.delete": "/gardens/delete/:gardenID",
"garden.dashboard": "/g/:gardenID",
"garden.search": "/g/:gardenID/search",
"garden.journal": "/g/:gardenID/journal",
"garden.pinboard": "/g/:gardenID/pinboard",
"garden.images": "/g/:gardenID/images",
"garden.image.data": "/g/:gardenID/images/:imageID/data",
"journal.new": "/g/:gardenID/journal/new",
"journal.edit": "/g/:gardenID/journal/edit/:entryID",
"journal.delete": "/g/:gardenID/journal/delete/:entryID",
"journal.attachment": "/g/:gardenID/journal/attachments/:entryID/:attachmentID",
"pinboard.new": "/g/:gardenID/pinboard/new",
"pinboard.edit": "/g/:gardenID/pinboard/edit/:entryID",
"pinboard.delete": "/g/:gardenID/pinboard/delete/:entryID",
"pinboard.attachment": "/g/:gardenID/pinboard/attachments/:entryID/:attachmentID",
"garden.members": "/g/:gardenID/members",
"garden.invite.new": "/g/:gardenID/member-invite-new",
"garden.member.role": "/g/:gardenID/members/:userID/role",
"garden.member.delete": "/g/:gardenID/members/:userID/delete",
"garden.member.transfer": "/g/:gardenID/members/:userID/transfer",
"garden.invite.delete": "/g/:gardenID/member-invite-delete/:inviteID",
"garden.role.update": "/g/:gardenID/roles/update",
"garden.role.new": "/g/:gardenID/roles/new",
"garden.role.delete": "/g/:gardenID/roles/delete",
"invite": "/invite",
"plants": "/g/:gardenID/plants",
"plant.new": "/g/:gardenID/plants/new",
"plant.edit": "/g/:gardenID/plants/edit/:plantID",
"plant.delete": "/g/:gardenID/plants/delete/:plantID",
"plant.status": "/g/:gardenID/plants/status/:plantID",
"plant.template-opt-out": "/g/:gardenID/plant-template-opt-out/:plantID/:templateID",
"plant.task-row": "/g/:gardenID/plant-task-row",
"plant.template-tasks": "/g/:gardenID/plant-template-tasks",
"plant.template-task": "/g/:gardenID/plant-template-task/:templateID",
"plant.location.new": "/g/:gardenID/plant-location-new/:plantID",
"plant.location.edit": "/g/:gardenID/plant-location-edit/:plantID/:assignmentID",
"plant.location.delete": "/g/:gardenID/plant-location-delete/:plantID/:assignmentID",
"locations": "/g/:gardenID/locations",
"location.new": "/g/:gardenID/locations/new",
"location.view": "/g/:gardenID/locations/view/:locationID",
"location.edit": "/g/:gardenID/locations/edit/:locationID",
"location.delete": "/g/:gardenID/locations/delete/:locationID",
"species": "/g/:gardenID/species",
"species.new": "/g/:gardenID/species/new",
"species.edit": "/g/:gardenID/species/edit/:speciesID",
"species.delete": "/g/:gardenID/species/delete/:speciesID",
"species.care.new": "/g/:gardenID/species-care/:speciesID/new",
"species.care.edit": "/g/:gardenID/species-care/:speciesID/edit/:instructionID",
"species.care.delete": "/g/:gardenID/species-care/:speciesID/delete/:instructionID",
"tasks": "/g/:gardenID/tasks",
"tasks.calendar": "/g/:gardenID/tasks/calendar",
"task.new": "/g/:gardenID/tasks/new",
"task.edit": "/g/:gardenID/tasks/edit/:taskID",
"task.complete": "/g/:gardenID/tasks/complete/:taskID",
"task.reopen": "/g/:gardenID/tasks/reopen/:taskID",
"task.delete": "/g/:gardenID/tasks/delete/:taskID",
"task-template.new": "/g/:gardenID/task-templates/new",
"task-template.edit": "/g/:gardenID/task-templates/edit/:templateID",
"task-template.delete": "/g/:gardenID/task-templates/delete/:templateID",
}
func routePath(name string) string {
pattern, ok := webRoutes[name]
if !ok {
panic("unknown web route: " + name)
}
return pattern
}
func webPath(name string, params ...any) string {
parts := strings.Split(routePath(name), "/")
paramIndex := 0
for i, part := range parts {
if !strings.HasPrefix(part, ":") {
continue
}
if paramIndex >= len(params) {
panic(fmt.Sprintf("route %q needs more parameters", name))
}
parts[i] = url.PathEscape(fmt.Sprint(params[paramIndex]))
paramIndex++
}
if paramIndex != len(params) {
panic(fmt.Sprintf("route %q received too many parameters", name))
}
return strings.Join(parts, "/")
}
func pathWithQuery(path string, pairs ...any) string {
if len(pairs)%2 != 0 {
panic("pathWithQuery requires key/value pairs")
}
values := url.Values{}
for i := 0; i < len(pairs); i += 2 {
values.Set(fmt.Sprint(pairs[i]), fmt.Sprint(pairs[i+1]))
}
if len(values) == 0 {
return path
}
return path + "?" + values.Encode()
}
func safeReturnPath(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
target, err := url.Parse(value)
if err != nil || target.IsAbs() || target.Host != "" || !strings.HasPrefix(target.Path, "/") || strings.HasPrefix(target.Path, "//") || strings.Contains(target.Path, `\`) {
return ""
}
return target.RequestURI()
}