Add admin activity management

This commit is contained in:
troogs
2026-10-01 17:58:41 +02:00
parent 84ed5a7a28
commit 944eed3eb3
7 changed files with 664 additions and 5 deletions
+197 -1
View File
@@ -21,6 +21,7 @@ type Activity struct {
Name string `json:"name"`
Icon string `json:"icon"`
Type string `json:"type"`
Disabled bool `json:"disabled,omitempty"`
CustomDescription string `json:"customDescription,omitempty"`
}
@@ -83,6 +84,7 @@ func main() {
http.HandleFunc("/api/admin/logout", handleAdminLogout)
http.HandleFunc("/api/admin/dates", handleAdminDates)
http.HandleFunc("/api/admin/dates/bulk", handleAdminBulkDates)
http.HandleFunc("/api/admin/activities", handleAdminActivities)
http.HandleFunc("/admin", handleAdminPage)
// Serve static files from webDir directory
@@ -117,12 +119,158 @@ func handlePublicSettings(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, PublicSettings{
Activities: settings.Activities,
Activities: enabledActivities(settings.Activities),
AvailableDates: availableDatesFromToday(settings.AvailableDates),
Names: settings.Names,
})
}
func handleAdminActivities(w http.ResponseWriter, r *http.Request) {
if !isAdminRequest(r) {
writeJSON(w, http.StatusUnauthorized, Response{Success: false, Message: "Anmeldung erforderlich."})
return
}
switch r.Method {
case http.MethodGet:
settings, err := readSettings()
if err != nil {
http.Error(w, "Settings could not be loaded", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, settings.Activities)
case http.MethodPost:
var activity Activity
if err := json.NewDecoder(r.Body).Decode(&activity); err != nil {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Ungültige Aktivität."})
return
}
if err := validateActivity(activity); err != nil {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
if err := updateActivities(func(activities []Activity) ([]Activity, error) {
for _, existing := range activities {
if strings.EqualFold(existing.Name, activity.Name) {
return nil, fmt.Errorf("activity already exists")
}
}
return append(activities, activity), nil
}); err != nil {
if err.Error() == "activity already exists" {
writeJSON(w, http.StatusConflict, Response{Success: false, Message: "Diese Aktivität existiert bereits."})
return
}
http.Error(w, "Activity could not be saved", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusCreated, Response{Success: true, Message: "Aktivität hinzugefügt."})
case http.MethodPut:
var request struct {
OriginalName string `json:"originalName"`
Activity
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.OriginalName == "" {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Ungültige Aktivität."})
return
}
if err := validateActivity(request.Activity); err != nil && request.Type != "custom" {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
if request.Type == "custom" && (strings.TrimSpace(request.Name) == "" || len([]rune(request.Name)) > 100) {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Bitte gib einen Namen mit maximal 100 Zeichen ein."})
return
}
updated := false
if err := updateActivities(func(activities []Activity) ([]Activity, error) {
for index := range activities {
if activities[index].Name == request.OriginalName {
if activities[index].Type == "custom" {
if request.Icon != activities[index].Icon || request.Type != "custom" {
return nil, fmt.Errorf("custom activity fields are read-only")
}
for otherIndex, other := range activities {
if otherIndex != index && strings.EqualFold(other.Name, request.Name) {
return nil, fmt.Errorf("activity already exists")
}
}
activities[index].Name = request.Name
activities[index].Disabled = request.Disabled
updated = true
break
}
for otherIndex, other := range activities {
if otherIndex != index && strings.EqualFold(other.Name, request.Name) {
return nil, fmt.Errorf("activity already exists")
}
}
activities[index] = request.Activity
updated = true
break
}
}
if !updated {
return nil, fmt.Errorf("activity does not exist")
}
return activities, nil
}); err != nil {
switch err.Error() {
case "activity already exists":
writeJSON(w, http.StatusConflict, Response{Success: false, Message: "Diese Aktivität existiert bereits."})
case "activity does not exist":
writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Aktivität wurde nicht gefunden."})
case "custom activity fields are read-only":
writeJSON(w, http.StatusForbidden, Response{Success: false, Message: "Typ und Icon dieser Aktivität können nicht geändert werden."})
default:
http.Error(w, "Activity could not be saved", http.StatusInternalServerError)
}
return
}
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Aktivität aktualisiert."})
case http.MethodDelete:
var request struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.Name == "" {
writeJSON(w, http.StatusBadRequest, Response{Success: false, Message: "Aktivität wurde nicht angegeben."})
return
}
deleted := false
if err := updateActivities(func(activities []Activity) ([]Activity, error) {
filtered := make([]Activity, 0, len(activities))
for _, activity := range activities {
if activity.Name == request.Name {
if activity.Type == "custom" {
return nil, fmt.Errorf("activity cannot be deleted")
}
deleted = true
continue
}
filtered = append(filtered, activity)
}
if !deleted {
return nil, fmt.Errorf("activity does not exist")
}
return filtered, nil
}); err != nil {
if err.Error() == "activity does not exist" {
writeJSON(w, http.StatusNotFound, Response{Success: false, Message: "Aktivität wurde nicht gefunden."})
return
}
if err.Error() == "activity cannot be deleted" {
writeJSON(w, http.StatusForbidden, Response{Success: false, Message: "Diese Aktivität kann nicht gelöscht werden."})
return
}
http.Error(w, "Activity could not be saved", http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, Response{Success: true, Message: "Aktivität gelöscht."})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleAdminLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -389,6 +537,54 @@ func updateAvailableDates(update func([]AvailableDate) ([]AvailableDate, error))
return os.Rename(temporaryPath, getSettingsFilePath())
}
func updateActivities(update func([]Activity) ([]Activity, error)) error {
settingsMu.Lock()
defer settingsMu.Unlock()
settings, err := readSettingsFile()
if err != nil {
return err
}
activities, err := update(settings.Activities)
if err != nil {
return err
}
settings.Activities = activities
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
temporaryPath := getSettingsFilePath() + ".tmp"
if err := os.WriteFile(temporaryPath, data, 0600); err != nil {
return err
}
return os.Rename(temporaryPath, getSettingsFilePath())
}
func validateActivity(activity Activity) error {
if strings.TrimSpace(activity.Name) == "" || len([]rune(activity.Name)) > 100 {
return fmt.Errorf("Bitte gib einen Namen mit maximal 100 Zeichen ein.")
}
if strings.TrimSpace(activity.Icon) == "" || len([]rune(activity.Icon)) > 8 {
return fmt.Errorf("Bitte wähle ein Icon aus.")
}
if activity.Type != "short" && activity.Type != "long" {
return fmt.Errorf("Bitte wähle Kurz oder Lang.")
}
return nil
}
func enabledActivities(activities []Activity) []Activity {
enabled := make([]Activity, 0, len(activities))
for _, activity := range activities {
if !activity.Disabled {
enabled = append(enabled, activity)
}
}
return enabled
}
func isValidAvailableDate(availableDate AvailableDate) bool {
if availableDate.Type != "short" && availableDate.Type != "long" {
return false