Initial commit
CI / test (push) Canceled after 0s

This commit is contained in:
2026-09-12 22:22:17 +02:00
commit 904d14b64c
314 changed files with 31884 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
package api
import (
"crypto/rand"
"errors"
"net/http"
"net/url"
"strings"
"time"
"gardomatic.kleiax.de/internal/auth"
"gardomatic.kleiax.de/internal/platform/validate"
"gardomatic.kleiax.de/internal/storage"
)
const adminInvitationTTL = 3 * 24 * time.Hour
func (app *application) listAdminUsersHandler(w http.ResponseWriter, r *http.Request) {
users, err := app.models.Users.GetAll()
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
if err := app.writeJSON(w, http.StatusOK, envelope{"users": users}, nil); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) inviteAdminUserHandler(w http.ResponseWriter, r *http.Request) {
var input struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
input.Name = strings.TrimSpace(input.Name)
input.Email = strings.ToLower(strings.TrimSpace(input.Email))
candidate := storage.User{Name: input.Name, Email: input.Email, Activated: false}
if passwordErr := candidate.Password.Set(rand.Text()); passwordErr != nil {
app.serverErrorResponse(w, r, passwordErr)
return
}
v := validate.New()
if storage.ValidateUser(v, candidate); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
user, err := app.models.Users.GetByEmail(input.Email)
switch {
case err == nil && user.Activated:
app.failedValidationResponse(w, r, map[string]string{"email": "a user with this email address already exists"})
return
case err == nil:
// Re-sending for an inactive account lets an administrator recover from
// an earlier delivery failure without creating a duplicate account.
case errors.Is(err, storage.ErrRecordNotFound):
user, err = app.models.Users.Insert(candidate)
if err != nil {
if errors.Is(err, storage.ErrDuplicateEmail) {
app.failedValidationResponse(w, r, map[string]string{"email": "a user with this email address already exists"})
} else {
app.serverErrorResponse(w, r, err)
}
return
}
default:
app.serverErrorResponse(w, r, err)
return
}
if err = app.models.Tokens.DeleteAllForUser(auth.ScopeActivation, user.ID); err != nil {
app.serverErrorResponse(w, r, err)
return
}
token, err := app.models.Tokens.New(user.ID, adminInvitationTTL, auth.ScopeActivation)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
activationURL := strings.TrimRight(app.config.WebBaseURL, "/") + "/activate?token=" + url.QueryEscape(token.Plaintext) + "&set-password=1"
if err = app.mailer.Send(user.Email, "user_invitation.tmpl", map[string]any{"name": user.Name, "activationURL": activationURL}); err != nil {
app.serverErrorResponse(w, r, err)
return
}
if err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) updateAdminUserRoleHandler(w http.ResponseWriter, r *http.Request) {
userID, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
actor, _ := app.contextGetAuthenticatedUser(r)
if actor.ID == userID {
app.permissionDeniedResponse(w, r)
return
}
var input struct {
Role storage.ApplicationRole `json:"role"`
}
if err := app.readJSON(w, r, &input); err != nil {
app.badRequestResponse(w, r, err)
return
}
role, err := app.models.Roles.Get(string(input.Role))
if err != nil || role.Scope != storage.RoleScopeApplication {
app.failedValidationResponse(w, r, map[string]string{"role": "must be an application role"})
return
}
user, err := app.models.Users.UpdateRole(userID, input.Role)
if err != nil {
if errors.Is(err, storage.ErrRecordNotFound) {
app.notFoundResponse(w, r)
} else {
app.serverErrorResponse(w, r, err)
}
return
}
if err := app.writeJSON(w, http.StatusOK, envelope{"user": user}, nil); err != nil {
app.serverErrorResponse(w, r, err)
}
}
func (app *application) deleteAdminUserHandler(w http.ResponseWriter, r *http.Request) {
userID, err := app.readIDParam(r)
if err != nil {
app.notFoundResponse(w, r)
return
}
actor, _ := app.contextGetAuthenticatedUser(r)
if actor.ID == userID {
app.permissionDeniedResponse(w, r)
return
}
if err = app.models.Users.Delete(userID); err != nil {
switch {
case errors.Is(err, storage.ErrRecordNotFound):
app.notFoundResponse(w, r)
case errors.Is(err, storage.ErrConflict):
app.conflictResponse(w, r)
default:
app.serverErrorResponse(w, r, err)
}
return
}
w.WriteHeader(http.StatusNoContent)
}