156 lines
7.4 KiB
Go
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()
|
|
}
|