package httpapi import ( "log/slog" "net/http" "strconv" "github.com/google/uuid" "github.com/syncova/syncova/packages/auth" "github.com/syncova/syncova/packages/platform/logging" ) // Grenzen der Seitengröße. // // Eine Obergrenze verhindert, dass ein einzelner Aufruf die Datenbank und den // Speicher des Dienstes belastet. const ( // defaultPageSize ist die Seitengröße ohne ausdrückliche Angabe. defaultPageSize = 50 // maxPageSize ist die größte erlaubte Seitengröße. maxPageSize = 200 ) // userHandler bedient die Benutzerverwaltung (SYNCOVA_API.md §4). type userHandler struct { // authService ist die Domänenlogik der Identitätsverwaltung. authService *auth.Service // logger protokolliert technische Fehler. logger *slog.Logger } // createUserRequest ist der Rumpf von POST /users. type createUserRequest struct { // Username ist der gewünschte Anmeldename. Username string `json:"username"` // Email ist die optionale Mailadresse. Email string `json:"email"` // Password ist das Anfangspasswort. Password string `json:"password"` // Roles sind die zuzuweisenden Rollennamen. Roles []string `json:"roles"` } // updateUserRequest ist der Rumpf von PATCH /users/{id}. // // Alle Felder sind Zeiger: nur ausdrücklich gesendete Werte werden geändert. type updateUserRequest struct { // Email ist die neue Mailadresse. Email *string `json:"email"` // Status ist der neue Kontozustand. Status *string `json:"status"` // Password ist das neue Passwort. Password *string `json:"password"` // Roles sind die neuen Rollen; sie ersetzen die bisherigen vollständig. Roles []string `json:"roles"` } // handleListUsers bedient GET /users. func (handler *userHandler) handleListUsers(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) page, pageSize := parsePagination(request) searchTerm := request.URL.Query().Get("search") loadedUsers, totalCount, listError := handler.authService.ListUsers(request.Context(), searchTerm, page, pageSize) if listError != nil { WriteError(responseWriter, request, requestLogger, NewInternalError(listError)) return } WritePaginatedSuccess(responseWriter, request, loadedUsers, PaginationMeta{ Page: page, PageSize: pageSize, Total: totalCount, }) } // handleCreateUser bedient POST /users. func (handler *userHandler) handleCreateUser(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) var createPayload createUserRequest if decodeError := decodeJSONBody(request, &createPayload); decodeError != nil { WriteError(responseWriter, request, requestLogger, decodeError) return } createdUser, createError := handler.authService.CreateUser(request.Context(), auth.CreateUserRequest{ Username: createPayload.Username, Email: createPayload.Email, Password: createPayload.Password, RoleNames: createPayload.Roles, }, actingUser, RequestContextFrom(request)) if createError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(createError)) return } WriteSuccess(responseWriter, request, http.StatusCreated, createdUser) } // handleGetUser bedient GET /users/{id}. func (handler *userHandler) handleGetUser(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) userID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } loadedUser, loadError := handler.authService.GetUser(request.Context(), userID) if loadError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(loadError)) return } WriteSuccess(responseWriter, request, http.StatusOK, loadedUser) } // handleUpdateUser bedient PATCH /users/{id}. func (handler *userHandler) handleUpdateUser(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) userID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } var updatePayload updateUserRequest if decodeError := decodeJSONBody(request, &updatePayload); decodeError != nil { WriteError(responseWriter, request, requestLogger, decodeError) return } // Der Zustand wird gegen die erlaubten Werte geprüft, bevor er die Domäne erreicht. var newStatus *auth.UserStatus if updatePayload.Status != nil { parsedStatus := auth.UserStatus(*updatePayload.Status) switch parsedStatus { case auth.UserStatusActive, auth.UserStatusDisabled, auth.UserStatusLocked: newStatus = &parsedStatus default: WriteError(responseWriter, request, requestLogger, NewValidationError("Der Status muss active, disabled oder locked sein.")) return } } updatedUser, updateError := handler.authService.UpdateUser(request.Context(), userID, auth.UpdateUserRequest{ Email: updatePayload.Email, Status: newStatus, Password: updatePayload.Password, RoleNames: updatePayload.Roles, }, actingUser, RequestContextFrom(request)) if updateError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(updateError)) return } WriteSuccess(responseWriter, request, http.StatusOK, updatedUser) } // handleDeleteUser bedient DELETE /users/{id}. func (handler *userHandler) handleDeleteUser(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) userID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } if deleteError := handler.authService.DeleteUser(request.Context(), userID, actingUser, RequestContextFrom(request)); deleteError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(deleteError)) return } WriteSuccess(responseWriter, request, http.StatusNoContent, nil) } // handleDisableUserMFA bedient POST /users/{id}/mfa/disable. // // Der Weg dient dem Fall, dass ein Benutzer sein Gerät verloren hat. func (handler *userHandler) handleDisableUserMFA(responseWriter http.ResponseWriter, request *http.Request) { requestLogger := logging.WithContext(request.Context(), handler.logger) actingUser, _ := AuthenticatedUserFromContext(request.Context()) userID, parseError := parsePathUUID(request, "id") if parseError != nil { WriteError(responseWriter, request, requestLogger, parseError) return } if disableError := handler.authService.DisableMFA(request.Context(), userID, actingUser, RequestContextFrom(request)); disableError != nil { WriteError(responseWriter, request, requestLogger, translateAuthError(disableError)) return } WriteSuccess(responseWriter, request, http.StatusOK, map[string]any{"mfa_enabled": false}) } // parsePagination liest Seitennummer und Seitengröße aus der Anfrage. func parsePagination(request *http.Request) (page int, pageSize int) { page = 1 pageSize = defaultPageSize if rawPage := request.URL.Query().Get("page"); rawPage != "" { if parsedPage, parseError := strconv.Atoi(rawPage); parseError == nil && parsedPage > 0 { page = parsedPage } } if rawPageSize := request.URL.Query().Get("page_size"); rawPageSize != "" { if parsedPageSize, parseError := strconv.Atoi(rawPageSize); parseError == nil && parsedPageSize > 0 { // Eine übergroße Angabe wird gekappt statt abgelehnt: der Aufrufer // erhält Daten, und der Dienst bleibt geschützt. pageSize = min(parsedPageSize, maxPageSize) } } return page, pageSize } // parsePathUUID liest eine UUID aus dem Anfragepfad. func parsePathUUID(request *http.Request, parameterName string) (uuid.UUID, *APIError) { rawValue := request.PathValue(parameterName) parsedUUID, parseError := uuid.Parse(rawValue) if parseError != nil { return uuid.Nil, NewBadRequestError("Die angegebene Kennung ist keine gültige UUID.") } return parsedUUID, nil }