417 lines
16 KiB
Go
417 lines
16 KiB
Go
package web
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"gardomatic.kleiax.de/lib/client"
|
|
)
|
|
|
|
type adminRoleForm struct {
|
|
Role string `form:"role"`
|
|
}
|
|
|
|
type adminUserInviteForm struct {
|
|
Name string `form:"name"`
|
|
Email string `form:"email"`
|
|
}
|
|
|
|
type roleSettingsForm struct {
|
|
Name string `form:"name"`
|
|
Scope string `form:"scope"`
|
|
Label string `form:"label"`
|
|
Permissions []string `form:"permissions"`
|
|
}
|
|
|
|
func applicationPermissionOptions() []permissionOption {
|
|
return []permissionOption{{"gardens:create", "Gärten anlegen"}, {"global_species:write", "Globale Pflanzen verwalten"}, {"users:manage", "Nutzerrollen verwalten"}, {"application_settings:write", "Instanzeinstellungen verwalten"}, {"roles:manage", "Rollen verwalten"}}
|
|
}
|
|
|
|
func gardenPermissionOptions() []permissionOption {
|
|
return []permissionOption{
|
|
{"garden:read", "Garten sehen"}, {"garden:update", "Garten bearbeiten"}, {"garden:delete", "Garten löschen"}, {"members:write", "Mitglieder verwalten"}, {"species:write", "Pflanzenarten verwalten"}, {"content:write", "Tagebuch und Zuordnungen bearbeiten"},
|
|
{"plants:create", "Pflanzen anlegen"}, {"plants:read:own", "Eigene Pflanzen sehen"}, {"plants:read:other", "Andere Pflanzen sehen"}, {"plants:update:own", "Eigene Pflanzen bearbeiten"}, {"plants:update:other", "Andere Pflanzen bearbeiten"}, {"plants:delete:own", "Eigene Pflanzen löschen"}, {"plants:delete:other", "Andere Pflanzen löschen"},
|
|
{"locations:create", "Orte anlegen"}, {"locations:read:own", "Eigene Orte sehen"}, {"locations:read:other", "Andere Orte sehen"}, {"locations:update:own", "Eigene Orte bearbeiten"}, {"locations:update:other", "Andere Orte bearbeiten"}, {"locations:delete:own", "Eigene Orte löschen"}, {"locations:delete:other", "Andere Orte löschen"},
|
|
{"tasks:create", "Aufgaben anlegen"}, {"tasks:read:own", "Eigene Aufgaben sehen"}, {"tasks:read:other", "Andere Aufgaben sehen"}, {"tasks:update:own", "Eigene Aufgaben bearbeiten"}, {"tasks:update:other", "Andere Aufgaben bearbeiten"}, {"tasks:delete:own", "Eigene Aufgaben löschen"}, {"tasks:delete:other", "Andere Aufgaben löschen"}, {"tasks:complete:own", "Eigene Aufgaben erledigen"}, {"tasks:complete:other", "Andere Aufgaben erledigen"},
|
|
}
|
|
}
|
|
|
|
type adminSpeciesCategoryForm struct {
|
|
Name string `form:"name"`
|
|
SortOrder int `form:"sort_order"`
|
|
Active bool `form:"active"`
|
|
Lifecycle string `form:"lifecycle"`
|
|
}
|
|
|
|
type adminTaskPriorityForm struct {
|
|
Name string `form:"name"`
|
|
Value int `form:"value"`
|
|
SortOrder int `form:"sort_order"`
|
|
Active bool `form:"active"`
|
|
}
|
|
|
|
type adminApplicationSettingsForm struct {
|
|
LifecycleStatusEnabled bool `form:"lifecycle_status_enabled"`
|
|
LifecycleRemovalMonth int `form:"lifecycle_removal_month"`
|
|
LifecycleRemovalDay int `form:"lifecycle_removal_day"`
|
|
Timezone string `form:"timezone"`
|
|
}
|
|
|
|
type adminTestMailForm struct {
|
|
Email string `form:"email"`
|
|
}
|
|
|
|
func (app *application) requireAdmin(next http.Handler) http.Handler {
|
|
return app.requireActivatedUser(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok || !user.IsAdmin() {
|
|
app.clientError(w, http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}))
|
|
}
|
|
|
|
func (app *application) requireApplicationPermission(permission string, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
user, ok := userFromContext(r.Context())
|
|
if !ok || !user.Can(permission) {
|
|
app.clientError(w, http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (app *application) admin(w http.ResponseWriter, r *http.Request) {
|
|
apiClient := client.FromContext(r.Context())
|
|
users, _, err := apiClient.AdminUsers(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
categories, _, err := client.FromContext(r.Context()).AdminSpeciesCategories(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
priorities, _, err := client.FromContext(r.Context()).AdminTaskPriorities(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
settings, _, err := client.FromContext(r.Context()).AdminApplicationSettings(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
environmentVariables, _, err := apiClient.AdminEnvironment(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
applicationRoles, gardenRoles, _, err := apiClient.AdminRoles(r.Context())
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
data := app.newTemplateData(r)
|
|
if !app.loadOptionalGarden(w, r, data) {
|
|
return
|
|
}
|
|
data.AdminUsers, data.SpeciesCategories, data.TaskPriorities = users, categories, priorities
|
|
data.ApplicationRoles, data.GardenRoles = applicationRoles, gardenRoles
|
|
data.ApplicationPermissions, data.GardenPermissions = applicationPermissionOptions(), gardenPermissionOptions()
|
|
data.RoleEditors = []roleEditorData{
|
|
globalRoleEditor("roles-settings", "Instanzrollen", "Diese Rollen gelten für die gesamte Serverinstanz.", "application", applicationRoles, data.ApplicationPermissions, data.CSRFToken),
|
|
globalRoleEditor("garden-role-templates", "Gartenrollen", "Diese Vorlagen gelten in allen Gärten und können je Garten überschrieben werden.", "garden", gardenRoles, data.GardenPermissions, data.CSRFToken),
|
|
}
|
|
for i := range data.RoleEditors {
|
|
data.RoleEditors[i].Garden = data.Garden
|
|
}
|
|
data.ApplicationSettings = &settings
|
|
data.EnvironmentVariables = append(environmentVariables, app.webEnvironmentVariables()...)
|
|
if r.URL.Query().Get("test-mail") == "sent" {
|
|
data.Flash = "Die Testmail wurde versendet."
|
|
}
|
|
if r.URL.Query().Get("test-mail") == "invalid" {
|
|
data.Flash = "Bitte gib eine gültige Empfängeradresse für die Testmail ein."
|
|
}
|
|
if r.URL.Query().Get("invitation") == "sent" {
|
|
data.Flash = "Die Einladung wurde versendet."
|
|
}
|
|
if r.URL.Query().Get("invitation") == "duplicate" {
|
|
data.Flash = "Für diese E-Mail-Adresse existiert bereits ein aktiver Account."
|
|
}
|
|
if r.URL.Query().Get("invitation") == "invalid" {
|
|
data.Flash = "Die Einladung ist ungültig. Bitte prüfe Name und E-Mail-Adresse."
|
|
}
|
|
if r.URL.Query().Get("deletion") == "done" {
|
|
data.Flash = "Der Nutzer wurde gelöscht und seine Kontodaten wurden anonymisiert."
|
|
}
|
|
if r.URL.Query().Get("deletion") == "owner" {
|
|
data.Flash = "Der Nutzer besitzt noch mindestens einen Garten. Übertrage zuerst das Eigentum."
|
|
}
|
|
app.render(w, http.StatusOK, "admin.tmpl", data)
|
|
}
|
|
|
|
func (app *application) adminUserInvitePost(w http.ResponseWriter, r *http.Request) {
|
|
var form adminUserInviteForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
input := client.AdminUserInviteInput{Name: strings.TrimSpace(form.Name), Email: strings.TrimSpace(form.Email)}
|
|
if _, _, err := client.FromContext(r.Context()).InviteAdminUser(r.Context(), input); err != nil {
|
|
var apiError *client.APIError
|
|
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusUnprocessableEntity {
|
|
status := "invalid"
|
|
if apiError.Validation["email"] == "a user with this email address already exists" {
|
|
status = "duplicate"
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#users-settings", "invitation", status), http.StatusSeeOther)
|
|
return
|
|
}
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#users-settings", "invitation", "sent"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminUserDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := app.readPathID(r, "userID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
if _, err = client.FromContext(r.Context()).DeleteAdminUser(r.Context(), userID); err != nil {
|
|
var apiError *client.APIError
|
|
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusConflict {
|
|
http.Redirect(w, r, adminPath(r, "#users-settings", "deletion", "owner"), http.StatusSeeOther)
|
|
return
|
|
}
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#users-settings", "deletion", "done"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) webEnvironmentVariables() []client.EnvironmentVariable {
|
|
return []client.EnvironmentVariable{
|
|
{Component: "Web", Name: "GARDOMATIC_ENV", Value: app.config.Env},
|
|
{Component: "Web", Name: "GARDOMATIC_WEB_HOST", Value: app.config.Host},
|
|
{Component: "Web", Name: "GARDOMATIC_WEB_PORT", Value: fmt.Sprint(app.config.Port)},
|
|
{Component: "Web", Name: "GARDOMATIC_API_BASE_URL", Value: app.config.APIBaseURL},
|
|
{Component: "Web", Name: "GARDOMATIC_SESSION_COOKIE_NAME", Value: app.config.SessionCookieName},
|
|
{Component: "Web", Name: "GARDOMATIC_COOKIE_SECURE", Value: fmt.Sprint(app.config.CookieSecure)},
|
|
}
|
|
}
|
|
|
|
func (app *application) adminRoleCreatePost(w http.ResponseWriter, r *http.Request) {
|
|
var form roleSettingsForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _, err := client.FromContext(r.Context()).CreateAdminRole(r.Context(), client.RoleInput{Name: strings.TrimSpace(form.Name), Scope: form.Scope, Label: strings.TrimSpace(form.Label), Permissions: form.Permissions})
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, adminRoleEditorAnchor(form.Scope)), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminRoleUpdatePost(w http.ResponseWriter, r *http.Request) {
|
|
var form roleSettingsForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, _, err := client.FromContext(r.Context()).UpdateAdminRole(r.Context(), form.Name, client.RoleInput{Label: strings.TrimSpace(form.Label), Permissions: form.Permissions})
|
|
if err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, adminRoleEditorAnchor(form.Scope)), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminRoleDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
var form roleSettingsForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := client.FromContext(r.Context()).DeleteAdminRole(r.Context(), form.Name); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, adminRoleEditorAnchor(form.Scope)), http.StatusSeeOther)
|
|
}
|
|
|
|
func adminRoleEditorAnchor(scope string) string {
|
|
if scope == "garden" {
|
|
return "#garden-role-templates"
|
|
}
|
|
return "#roles-settings"
|
|
}
|
|
|
|
func (app *application) adminApplicationSettingsPost(w http.ResponseWriter, r *http.Request) {
|
|
var form adminApplicationSettingsForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
timezone := strings.TrimSpace(form.Timezone)
|
|
input := client.ApplicationSettingsInput{
|
|
LifecycleStatusEnabled: &form.LifecycleStatusEnabled,
|
|
LifecycleRemovalMonth: &form.LifecycleRemovalMonth,
|
|
LifecycleRemovalDay: &form.LifecycleRemovalDay,
|
|
Timezone: &timezone,
|
|
}
|
|
if _, _, err := client.FromContext(r.Context()).UpdateAdminApplicationSettings(r.Context(), input); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#lifecycle-settings"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminTestMailPost(w http.ResponseWriter, r *http.Request) {
|
|
var form adminTestMailForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, err := client.FromContext(r.Context()).SendAdminTestMail(r.Context(), strings.TrimSpace(form.Email)); err != nil {
|
|
var apiError *client.APIError
|
|
if errors.As(err, &apiError) && apiError.StatusCode == http.StatusUnprocessableEntity {
|
|
http.Redirect(w, r, adminPath(r, "#mail-settings", "test-mail", "invalid"), http.StatusSeeOther)
|
|
return
|
|
}
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#mail-settings", "test-mail", "sent"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminTaskPriorityCreatePost(w http.ResponseWriter, r *http.Request) {
|
|
var form adminTaskPriorityForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
name, active := strings.TrimSpace(form.Name), true
|
|
if _, _, err := client.FromContext(r.Context()).CreateAdminTaskPriority(r.Context(), client.TaskPriorityInput{Name: &name, Value: &form.Value, SortOrder: &form.SortOrder, Active: &active}); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#task-priorities"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminTaskPriorityUpdatePost(w http.ResponseWriter, r *http.Request) {
|
|
id, err := app.readPathID(r, "priorityID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
var form adminTaskPriorityForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(form.Name)
|
|
if _, _, err := client.FromContext(r.Context()).UpdateAdminTaskPriority(r.Context(), id, client.TaskPriorityInput{Name: &name, Value: &form.Value, SortOrder: &form.SortOrder, Active: &form.Active}); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#task-priorities"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminTaskPriorityDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
id, err := app.readPathID(r, "priorityID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
if _, err := client.FromContext(r.Context()).DeleteAdminTaskPriority(r.Context(), id); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#task-priorities"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminSpeciesCategoryCreatePost(w http.ResponseWriter, r *http.Request) {
|
|
var form adminSpeciesCategoryForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
name, active := strings.TrimSpace(form.Name), true
|
|
input := client.SpeciesCategoryInput{Name: &name, SortOrder: &form.SortOrder, Active: &active, Lifecycle: &form.Lifecycle}
|
|
if _, _, err := client.FromContext(r.Context()).CreateAdminSpeciesCategory(r.Context(), input); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#species-categories"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminSpeciesCategoryUpdatePost(w http.ResponseWriter, r *http.Request) {
|
|
categoryID, err := app.readPathID(r, "categoryID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
var form adminSpeciesCategoryForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
name := strings.TrimSpace(form.Name)
|
|
input := client.SpeciesCategoryInput{Name: &name, SortOrder: &form.SortOrder, Active: &form.Active, Lifecycle: &form.Lifecycle}
|
|
if _, _, err := client.FromContext(r.Context()).UpdateAdminSpeciesCategory(r.Context(), categoryID, input); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#species-categories"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminSpeciesCategoryDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
categoryID, err := app.readPathID(r, "categoryID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
if _, err := client.FromContext(r.Context()).DeleteAdminSpeciesCategory(r.Context(), categoryID); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, "#species-categories"), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) adminUserRolePost(w http.ResponseWriter, r *http.Request) {
|
|
userID, err := app.readPathID(r, "userID")
|
|
if err != nil {
|
|
app.notFound(w)
|
|
return
|
|
}
|
|
var form adminRoleForm
|
|
if err := app.decodePostForm(r, &form); err != nil {
|
|
app.clientError(w, http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, _, err := client.FromContext(r.Context()).UpdateAdminUserRole(r.Context(), userID, form.Role); err != nil {
|
|
app.handleAPIError(w, r, err)
|
|
return
|
|
}
|
|
http.Redirect(w, r, adminPath(r, ""), http.StatusSeeOther)
|
|
}
|
|
|
|
func (app *application) settings(w http.ResponseWriter, r *http.Request) {
|
|
data := app.newTemplateData(r)
|
|
if !app.loadOptionalGarden(w, r, data) {
|
|
return
|
|
}
|
|
app.render(w, http.StatusOK, "settings.tmpl", data)
|
|
}
|