Add user management

This commit is contained in:
2026-05-02 14:31:19 +02:00
parent 24ade563db
commit 5997a29d92
20 changed files with 983 additions and 2 deletions
+269
View File
@@ -0,0 +1,269 @@
package handlers
import (
"database/sql"
"errors"
"log"
"quay/app/models"
"quay/app/repository"
"quay/internal/security"
"time"
"github.com/gofiber/fiber/v3"
)
type UserHandler struct {
Repo repository.UserRepository
}
func NewUserHandler(repo repository.UserRepository) *UserHandler {
return &UserHandler{Repo: repo}
}
func (h *UserHandler) GetAllUsers(c fiber.Ctx) error {
users, err := h.Repo.GetAllUsers()
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
log.Println("Error getting user: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while getting site",
})
}
if users == nil {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
for i := range users {
users[i].HashedPassword = ""
}
return c.JSON(users)
}
// GetUserById godoc
// @Summary Get user by ID
// @Description Get a single user by its ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} models.User
// @Failure 404 {object} models.APIError
// @Failure 500 {object} models.APIError
func (h *UserHandler) GetUserById(c fiber.Ctx) error {
id := c.Params("id")
user, err := h.Repo.GetUserById(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
log.Println("Error getting user: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while getting site",
})
}
if user == nil {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
user.HashedPassword = ""
return c.JSON(user)
}
// GetUserByName godoc
// @Summary Get user by Name
// @Description Get a single user by its Name
// @Tags Users
// @Accept json
// @Produce json
// @Param name path string true "User Name"
// @Success 200 {object} models.User
// @Failure 404 {object} models.APIError
// @Failure 500 {object} models.APIError
func (h *UserHandler) GetUserByName(c fiber.Ctx) error {
name := c.Params("name")
user, err := h.Repo.GetUserByName(name)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
log.Println("Error getting user: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while getting site",
})
}
if user == nil {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
user.HashedPassword = ""
return c.JSON(user)
}
// CreateUser godoc
// @Summary Create a new user
// @Description Create a new user with the provided name, role, and password
// @Tags Users
// @Accept json
// @Produce json
// @Success 200 {object} models.User
// @Failure 400 {object} models.APIError
// @Failure 409 {object} models.APIError
// @Failure 500 {object} models.APIError
func (h *UserHandler) CreateUser(c fiber.Ctx) error {
var createUserRequest models.CreateUserRequest
if err := c.Bind().Body(&createUserRequest); err != nil {
log.Println("Error parsing body: ", err)
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
Message: "Invalid request body",
})
}
if err := models.ValidateCreateUserRequest(&createUserRequest); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
Message: "Invalid request body: " + err.Error(),
})
}
hashedPassword, err := security.HashPassword(createUserRequest.Password)
if err != nil {
log.Println("Error hashing password: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while hashing password",
})
}
user := models.User{
Name: createUserRequest.Name,
Role: createUserRequest.Role,
HashedPassword: hashedPassword,
CreatedAt: time.Now().UTC().Format(time.DateTime),
}
err = h.Repo.CreateUser(&user)
if err != nil {
log.Println("Error creating user: ", err)
if errors.Is(err, repository.ErrUserAlreadyExists) {
return c.Status(fiber.StatusConflict).JSON(&models.APIError{
Message: "User name already taken",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while creating user",
})
}
return c.JSON(user)
}
// UpdateUser godoc
// @Summary Update an existing user
// @Description Update an existing user with the provided name, role, and password
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 200 {object} models.User
// @Failure 400 {object} models.APIError
// @Failure 404 {object} models.APIError
// @Failure 409 {object} models.APIError
// @Failure 500 {object} models.APIError
func (h *UserHandler) UpdateUser(c fiber.Ctx) error {
id := c.Params("id")
var updateUserRequest models.UpdateUserRequest
if err := c.Bind().Body(&updateUserRequest); err != nil {
log.Println("Error parsing body: ", err)
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
Message: "Invalid request body",
})
}
if err := models.ValidateUpdateUserRequest(&updateUserRequest); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{
Message: "Invalid request body: " + err.Error(),
})
}
user := models.User{
ID: id,
Name: updateUserRequest.Name,
Role: updateUserRequest.Role,
}
err := h.Repo.UpdateUser(&user)
if err != nil {
log.Println("Error updating user: ", err)
if errors.Is(err, repository.ErrUserAlreadyExists) {
return c.Status(fiber.StatusConflict).JSON(&models.APIError{
Message: "User name already taken",
})
}
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while updating user",
})
}
return c.SendStatus(fiber.StatusNoContent)
}
// DeleteUser godoc
// @Summary Delete a user
// @Description Delete a user by its ID
// @Tags Users
// @Accept json
// @Produce json
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Failure 404 {object} models.APIError
// @Failure 500 {object} models.APIError
func (h *UserHandler) DeleteUser(c fiber.Ctx) error {
id := c.Params("id")
if _, err := h.Repo.GetUserById(id); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "User not found",
})
}
log.Println("Error checking user before delete: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while deleting user",
})
}
err := h.Repo.DeleteUser(id)
if err != nil {
log.Println("Error deleting user: ", err)
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{
Message: "Unexpected error while deleting user",
})
}
return c.SendStatus(fiber.StatusNoContent)
}
+51
View File
@@ -0,0 +1,51 @@
package models
import "errors"
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
HashedPassword string `json:"hashed_password"`
CreatedAt string `json:"created_at"`
}
type CreateUserRequest struct {
Name string `json:"name"`
Role string `json:"role"`
Password string `json:"password"`
}
func ValidateCreateUserRequest(req *CreateUserRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.Role == "" {
return errors.New("role is required")
}
if req.Role != "admin" && req.Role != "user" {
return errors.New("role must be either 'admin' or 'user'")
}
if req.Password == "" {
return errors.New("password is required")
}
return nil
}
type UpdateUserRequest struct {
Name string `json:"name"`
Role string `json:"role"`
}
func ValidateUpdateUserRequest(req *UpdateUserRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.Role == "" {
return errors.New("role is required")
}
if req.Role != "admin" && req.Role != "user" {
return errors.New("role must be either 'admin' or 'user'")
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
package repository
import (
"errors"
"quay/app/models"
)
var ErrUserAlreadyExists = errors.New("user already exists")
type UserRepository interface {
GetAllUsers() ([]models.User, error)
GetUserById(id string) (*models.User, error)
GetUserByName(name string) (*models.User, error)
CreateUser(user *models.User) error
UpdateUser(user *models.User) error
DeleteUser(id string) error
}
+11 -1
View File
@@ -18,9 +18,11 @@ import (
func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) {
siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db))
deploymentRepository := database.NewSQLiteDeploymentRepository(db)
userRepository := database.NewSQLiteUserRepository(db)
deploySiteHandler := handlers.NewDeploySiteHandler(envCfg, siteRepository, deploymentRepository)
siteHandler := handlers.NewSiteHandler(siteRepository)
deploySiteHandler := handlers.NewDeploySiteHandler(envCfg, siteRepository, deploymentRepository)
userHandler := handlers.NewUserHandler(userRepository)
deploymentsHandler := handlers.NewDeploymentHandler(deploymentRepository)
api := app.Group("/api/v1", middleware.APIHostGuard(envCfg.DashboardHost))
@@ -61,6 +63,14 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
api.Get("/sites/:id/deployments", deploymentsHandler.GetDeploymentsBySite)
// Users
api.Get("/users", userHandler.GetAllUsers)
api.Get("/users/:id", userHandler.GetUserById)
api.Get("/users/by-name/:name", userHandler.GetUserByName)
api.Post("/users", userHandler.CreateUser)
api.Put("/users/:id", userHandler.UpdateUser)
api.Delete("/users/:id", userHandler.DeleteUser)
api.Use(func(c fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(&models.APIError{
Message: "Endpoint not found",