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_DEMO_ACCOUNT_EMAIL", Value: app.config.DemoAccountEmail}, {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) }