package api import ( "net/http" "strings" "time" "codeberg.org/SquidSE/ClusterCanvas/service/internal/settings" ) type userPublic struct { ID string `json:"id"` Username string `json:"username"` Enabled bool `json:"enabled"` TOTPConfirmed bool `json:"totp_confirmed"` GroupNames []string `json:"group_names"` CreatedAt time.Time `json:"created_at"` PasswordChangedAt time.Time `json:"password_changed_at"` IsAdmin bool `json:"is_admin"` CanDelete bool `json:"can_delete"` } type usersResponse struct { Users []userPublic `json:"users"` } func (app *App) usersGetHandler(writer http.ResponseWriter, request *http.Request) { if err := app.requireKey(); err != nil { writeJSON(writer, http.StatusServiceUnavailable, apiErrorResponse{Error: err.Error()}) return } store, err := settings.LoadPasswords(app.ConfigDir, app.Key) if err != nil { writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()}) return } writeJSON(writer, http.StatusOK, usersResponse{Users: toPublicUsers(store.Users)}) } func (app *App) usersDeleteHandler(writer http.ResponseWriter, request *http.Request) { if err := authorizeUsersWrite(request); err != nil { writeJSON(writer, http.StatusForbidden, apiErrorResponse{Error: err.Error()}) return } if err := app.requireKey(); err != nil { writeJSON(writer, http.StatusServiceUnavailable, apiErrorResponse{Error: err.Error()}) return } userID := strings.TrimSpace(request.URL.Query().Get("id")) if userID == "" { writeJSON(writer, http.StatusBadRequest, apiErrorResponse{Error: "missing user id"}) return } store, err := settings.LoadPasswords(app.ConfigDir, app.Key) if err != nil { writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()}) return } adminCount := countAdministrators(store.Users) remaining := make([]settings.UserCredential, 0, len(store.Users)) found := false for _, user := range store.Users { if user.ID != userID { remaining = append(remaining, user) continue } found = true if userIsAdministrator(user) && adminCount <= 1 { writeJSON(writer, http.StatusConflict, apiErrorResponse{ Error: "cannot delete the last administrator", }) return } } if !found { writeJSON(writer, http.StatusNotFound, apiErrorResponse{Error: "user not found"}) return } store.Users = remaining if err := settings.SavePasswords(app.ConfigDir, store, app.Key); err != nil { writeJSON(writer, http.StatusInternalServerError, apiErrorResponse{Error: err.Error()}) return } if app.Sessions != nil { _ = app.Sessions.InvalidateUserSessions(userID) } writeJSON(writer, http.StatusOK, usersResponse{Users: toPublicUsers(store.Users)}) } func authorizeUsersWrite(_ *http.Request) error { // TODO: integrate authz middleware. return nil } func toPublicUsers(users []settings.UserCredential) []userPublic { adminCount := countAdministrators(users) result := make([]userPublic, 0, len(users)) for _, user := range users { isAdmin := userIsAdministrator(user) groupNames := user.GroupNames if groupNames == nil { groupNames = []string{} } result = append(result, userPublic{ ID: user.ID, Username: user.Username, Enabled: user.Enabled, TOTPConfirmed: user.TOTPConfirmed, GroupNames: groupNames, CreatedAt: user.CreatedAt, PasswordChangedAt: user.PasswordChangedAt, IsAdmin: isAdmin, CanDelete: !(isAdmin && adminCount <= 1), }) } return result } func userIsAdministrator(user settings.UserCredential) bool { for _, groupName := range user.GroupNames { if groupName == settings.AdministratorsGroupName { return true } } return false } func countAdministrators(users []settings.UserCredential) int { count := 0 for _, user := range users { if userIsAdministrator(user) { count++ } } return count }