Add admin activity management
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user