readysite / website / controllers / users.go
6.0 KB
users.go
package controllers

import (
	"fmt"
	"net/http"

	"github.com/readysite/readysite/pkg/application"
	"github.com/readysite/readysite/website/internal/access"
	"github.com/readysite/readysite/website/internal/helpers"
	"github.com/readysite/readysite/website/models"
)

// Users returns the users controller.
func Users() (string, *UsersController) {
	return "users", &UsersController{}
}

// UsersController handles admin user management.
type UsersController struct {
	application.BaseController
}

// Setup registers routes.
func (c *UsersController) Setup(app *application.App) {
	c.BaseController.Setup(app)
}

// Handle implements Controller interface with value receiver for request isolation.
func (c UsersController) Handle(r *http.Request) application.Controller {
	c.Request = r
	return &c
}

// User returns the current user from path parameter.
func (c *UsersController) User() *models.User {
	if c.Request == nil {
		return nil
	}
	id := c.PathValue("id")
	if id == "" {
		return nil
	}
	user, err := models.Users.Get(id)
	if err != nil {
		return nil
	}
	return user
}

// Users returns all users.
func (c *UsersController) Users() []*models.User {
	users, _ := models.Users.Search("ORDER BY CreatedAt DESC")
	return users
}

// UserPagination returns pagination state for users list.
func (c *UsersController) UserPagination() *helpers.Pagination {
	total := models.Users.Count("")
	return helpers.NewPagination(c.Request, total)
}

// PaginatedUsers returns paginated users.
func (c *UsersController) PaginatedUsers() []*models.User {
	p := c.UserPagination()
	users, _ := models.Users.Search("ORDER BY CreatedAt DESC LIMIT ? OFFSET ?", p.PageSize, p.Offset)
	return users
}

// IsNewUser returns true if creating a new user.
func (c *UsersController) IsNewUser() bool {
	if c.Request == nil {
		return true
	}
	id := c.PathValue("id")
	return id == ""
}

// Roles returns available user roles.
func (c *UsersController) Roles() []string {
	return []string{"admin", "user", "viewer"}
}

// Create creates a new user.
func (c *UsersController) Create(w http.ResponseWriter, r *http.Request) {
	email := r.FormValue("email")
	name := r.FormValue("name")
	password := r.FormValue("password")
	role := r.FormValue("role")

	if email == "" {
		c.RenderError(w, r, fmt.Errorf("Email is required"))
		return
	}

	if password == "" {
		c.RenderError(w, r, fmt.Errorf("Password is required"))
		return
	}

	if len(password) < 8 {
		c.RenderError(w, r, fmt.Errorf("Password must be at least 8 characters"))
		return
	}

	// Default role to "user" if not specified or invalid
	if role != "admin" && role != "user" && role != "viewer" {
		role = "user"
	}

	// Check if email already exists
	existing, _ := models.Users.First("WHERE Email = ?", email)
	if existing != nil {
		c.RenderError(w, r, fmt.Errorf("A user with this email already exists"))
		return
	}

	// Hash password
	hash, err := access.HashPassword(password)
	if err != nil {
		c.RenderError(w, r, fmt.Errorf("Failed to hash password"))
		return
	}

	user := &models.User{
		Email:        email,
		Name:         name,
		PasswordHash: hash,
		Role:         role,
	}

	id, err := models.Users.Insert(user)
	if err != nil {
		c.RenderError(w, r, fmt.Errorf("Failed to create user: %w", err))
		return
	}

	// Audit log
	adminUser := access.GetUserFromJWT(r)
	adminID := ""
	if adminUser != nil {
		adminID = adminUser.ID
	}
	helpers.AuditCreate(r, adminID, helpers.ResourceUser, id, email)

	w.Header().Set("HX-Trigger", `{"showToast":"User created"}`)
	c.Refresh(w, r)
}

// Update updates an existing user.
func (c *UsersController) Update(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	email := r.FormValue("email")
	name := r.FormValue("name")
	password := r.FormValue("password")
	role := r.FormValue("role")

	user, err := models.Users.Get(id)
	if err != nil {
		c.RenderError(w, r, fmt.Errorf("User not found"))
		return
	}

	if email == "" {
		c.RenderError(w, r, fmt.Errorf("Email is required"))
		return
	}

	// Check if email is taken by another user
	existing, _ := models.Users.First("WHERE Email = ? AND ID != ?", email, id)
	if existing != nil {
		c.RenderError(w, r, fmt.Errorf("A user with this email already exists"))
		return
	}

	// Validate role
	if role != "admin" && role != "user" && role != "viewer" {
		role = user.Role // Keep existing role if invalid
	}

	user.Email = email
	user.Name = name
	user.Role = role

	// Only update password if provided
	if password != "" {
		if len(password) < 8 {
			c.RenderError(w, r, fmt.Errorf("Password must be at least 8 characters"))
			return
		}

		hash, err := access.HashPassword(password)
		if err != nil {
			c.RenderError(w, r, fmt.Errorf("Failed to hash password"))
			return
		}
		user.PasswordHash = hash
	}

	if err := models.Users.Update(user); err != nil {
		c.RenderError(w, r, fmt.Errorf("Failed to update user: %w", err))
		return
	}

	// Audit log
	adminUser := access.GetUserFromJWT(r)
	adminID := ""
	if adminUser != nil {
		adminID = adminUser.ID
	}
	helpers.AuditUpdate(r, adminID, helpers.ResourceUser, id, email, nil)

	w.Header().Set("HX-Trigger", `{"showToast":"User saved"}`)
	c.Refresh(w, r)
}

// Delete deletes a user.
func (c *UsersController) Delete(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")

	user, err := models.Users.Get(id)
	if err != nil {
		c.RenderError(w, r, fmt.Errorf("User not found"))
		return
	}

	// Prevent deleting yourself
	adminUser := access.GetUserFromJWT(r)
	if adminUser != nil && adminUser.ID == id {
		c.RenderError(w, r, fmt.Errorf("You cannot delete your own account"))
		return
	}

	// Prevent deleting the last admin
	if user.Role == "admin" {
		adminCount := models.Users.Count("WHERE Role = 'admin'")
		if adminCount <= 1 {
			c.RenderError(w, r, fmt.Errorf("Cannot delete the last admin user"))
			return
		}
	}

	if err := models.Users.Delete(user); err != nil {
		c.RenderError(w, r, fmt.Errorf("Failed to delete user: %w", err))
		return
	}

	// Audit log
	adminID := ""
	if adminUser != nil {
		adminID = adminUser.ID
	}
	helpers.AuditDelete(r, adminID, helpers.ResourceUser, id, user.Email)

	c.Redirect(w, r, "/admin/users")
}
← Back