@@ -0,0 +1,416 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user