prompt 7
This commit is contained in:
125
internal/controllers/users_controller.go
Normal file
125
internal/controllers/users_controller.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
httpmw "trustcontact/internal/http/middleware"
|
||||
"trustcontact/internal/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UsersController struct {
|
||||
usersService *services.UsersService
|
||||
}
|
||||
|
||||
func NewUsersController(usersService *services.UsersService) *UsersController {
|
||||
return &UsersController{usersService: usersService}
|
||||
}
|
||||
|
||||
func (uc *UsersController) Index(c *fiber.Ctx) error {
|
||||
pageData, err := uc.queryPage(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewData := map[string]any{
|
||||
"Title": "Users",
|
||||
"NavSection": "private",
|
||||
"PageData": pageData,
|
||||
}
|
||||
for k, v := range localsTemplateData(c) {
|
||||
viewData[k] = v
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(
|
||||
"web/templates/layout.html",
|
||||
"web/templates/public/_flash.html",
|
||||
"web/templates/private/users/index.html",
|
||||
"web/templates/private/users/_table.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return executeLayout(c, tmpl, viewData)
|
||||
}
|
||||
|
||||
func (uc *UsersController) Table(c *fiber.Ctx) error {
|
||||
pageData, err := uc.queryPage(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewData := map[string]any{"PageData": pageData}
|
||||
tmpl, err := template.ParseFiles("web/templates/private/users/_table.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html", "utf-8")
|
||||
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), "users_table", viewData)
|
||||
}
|
||||
|
||||
func (uc *UsersController) Modal(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("invalid user id")
|
||||
}
|
||||
|
||||
user, err := uc.usersService.GetByID(uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return c.Status(fiber.StatusNotFound).SendString("user not found")
|
||||
}
|
||||
|
||||
viewData := map[string]any{"User": user}
|
||||
tmpl, err := template.ParseFiles("web/templates/private/users/_modal.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type("html", "utf-8")
|
||||
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), "users_modal", viewData)
|
||||
}
|
||||
|
||||
func (uc *UsersController) queryPage(c *fiber.Ctx) (*services.UsersPage, error) {
|
||||
page := parseIntOrDefault(c.Query("page"), 1)
|
||||
pageSize := parseIntOrDefault(c.Query("pageSize"), 10)
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
sort := c.Query("sort", "id")
|
||||
dir := c.Query("dir", "asc")
|
||||
|
||||
pageData, err := uc.usersService.List(services.UsersQuery{
|
||||
Q: q,
|
||||
Sort: sort,
|
||||
Dir: dir,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list users: %w", err)
|
||||
}
|
||||
|
||||
return pageData, nil
|
||||
}
|
||||
|
||||
func parseIntOrDefault(value string, fallback int) int {
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func executeLayout(c *fiber.Ctx, tmpl *template.Template, viewData map[string]any) error {
|
||||
httpmw.SetTemplateData(c, "NavSection", viewData["NavSection"])
|
||||
c.Type("html", "utf-8")
|
||||
return tmpl.ExecuteTemplate(c.Response().BodyWriter(), filepath.Base("web/templates/layout.html"), viewData)
|
||||
}
|
||||
@@ -18,30 +18,35 @@ func Seed(database *gorm.DB) error {
|
||||
|
||||
seedUsers := []models.User{
|
||||
{
|
||||
Name: "Admin User",
|
||||
Email: "admin@example.com",
|
||||
Role: models.RoleAdmin,
|
||||
EmailVerified: true,
|
||||
PasswordHash: passwordHash,
|
||||
},
|
||||
{
|
||||
Name: "Normal User",
|
||||
Email: "user@example.com",
|
||||
Role: models.RoleUser,
|
||||
EmailVerified: true,
|
||||
PasswordHash: passwordHash,
|
||||
},
|
||||
{
|
||||
Name: "Demo One",
|
||||
Email: "demo1@example.com",
|
||||
Role: models.RoleUser,
|
||||
EmailVerified: true,
|
||||
PasswordHash: passwordHash,
|
||||
},
|
||||
{
|
||||
Name: "Demo Two",
|
||||
Email: "demo2@example.com",
|
||||
Role: models.RoleUser,
|
||||
EmailVerified: true,
|
||||
PasswordHash: passwordHash,
|
||||
},
|
||||
{
|
||||
Name: "Demo Three",
|
||||
Email: "demo3@example.com",
|
||||
Role: models.RoleUser,
|
||||
EmailVerified: true,
|
||||
@@ -62,6 +67,7 @@ func upsertUser(database *gorm.DB, user models.User) error {
|
||||
result := database.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "email"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"name",
|
||||
"role",
|
||||
"email_verified",
|
||||
"password_hash",
|
||||
|
||||
@@ -22,6 +22,7 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||
return fmt.Errorf("init auth service: %w", err)
|
||||
}
|
||||
authController := controllers.NewAuthController(authService)
|
||||
usersController := controllers.NewUsersController(services.NewUsersService(database))
|
||||
|
||||
app.Get("/healthz", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
@@ -39,10 +40,13 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||
app.Post("/forgot-password", authController.ForgotPassword)
|
||||
app.Get("/reset-password", authController.ShowResetPassword)
|
||||
app.Post("/reset-password", authController.ResetPassword)
|
||||
app.Get("/users", httpmw.RequireAuth(), usersController.Index)
|
||||
app.Get("/users/table", httpmw.RequireAuth(), usersController.Table)
|
||||
app.Get("/users/:id/modal", httpmw.RequireAuth(), usersController.Modal)
|
||||
|
||||
private := app.Group("/private", httpmw.RequireAuth())
|
||||
private.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.SendString("private area")
|
||||
return c.Redirect("/users")
|
||||
})
|
||||
|
||||
admin := app.Group("/admin", httpmw.RequireAuth(), httpmw.RequireAdmin())
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:120;index"`
|
||||
Email string `gorm:"size:320;uniqueIndex;not null"`
|
||||
PasswordHash string `gorm:"size:255;not null"`
|
||||
EmailVerified bool `gorm:"not null;default:false"`
|
||||
|
||||
@@ -2,6 +2,8 @@ package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"trustcontact/internal/models"
|
||||
|
||||
@@ -12,6 +14,14 @@ type UserRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type UserListParams struct {
|
||||
Query string
|
||||
Sort string
|
||||
Dir string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
func NewUserRepo(db *gorm.DB) *UserRepo {
|
||||
return &UserRepo{db: db}
|
||||
}
|
||||
@@ -55,3 +65,65 @@ func (r *UserRepo) UpdatePasswordHash(userID uint, passwordHash string) error {
|
||||
Where("id = ?", userID).
|
||||
Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
func (r *UserRepo) List(params UserListParams) ([]models.User, int64, error) {
|
||||
query := r.db.Model(&models.User{})
|
||||
|
||||
search := strings.TrimSpace(params.Query)
|
||||
if search != "" {
|
||||
like := "%" + strings.ToLower(search) + "%"
|
||||
query = query.Where("LOWER(name) LIKE ? OR LOWER(email) LIKE ?", like, like)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
orderBy := sanitizeSort(params.Sort)
|
||||
orderDir := sanitizeDir(params.Dir)
|
||||
orderClause := fmt.Sprintf("%s %s", orderBy, orderDir)
|
||||
|
||||
page := params.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := params.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var users []models.User
|
||||
if err := query.Order(orderClause).Limit(pageSize).Offset(offset).Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
func sanitizeSort(sort string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(sort)) {
|
||||
case "id":
|
||||
return "id"
|
||||
case "name":
|
||||
return "name"
|
||||
case "email":
|
||||
return "email"
|
||||
default:
|
||||
return "id"
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeDir(dir string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(dir)) {
|
||||
case "desc":
|
||||
return "desc"
|
||||
default:
|
||||
return "asc"
|
||||
}
|
||||
}
|
||||
|
||||
132
internal/services/users_service.go
Normal file
132
internal/services/users_service.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"trustcontact/internal/models"
|
||||
"trustcontact/internal/repo"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UsersService struct {
|
||||
users *repo.UserRepo
|
||||
}
|
||||
|
||||
type UsersQuery struct {
|
||||
Q string
|
||||
Sort string
|
||||
Dir string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type UsersPage struct {
|
||||
Users []models.User
|
||||
Total int64
|
||||
Page int
|
||||
PageSize int
|
||||
TotalPages int
|
||||
HasPrev bool
|
||||
HasNext bool
|
||||
PrevPage int
|
||||
NextPage int
|
||||
Sort string
|
||||
Dir string
|
||||
Q string
|
||||
}
|
||||
|
||||
func NewUsersService(database *gorm.DB) *UsersService {
|
||||
return &UsersService{users: repo.NewUserRepo(database)}
|
||||
}
|
||||
|
||||
func (s *UsersService) List(query UsersQuery) (*UsersPage, error) {
|
||||
page := query.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := query.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
sort := normalizeSort(query.Sort)
|
||||
dir := normalizeDir(query.Dir)
|
||||
|
||||
users, total, err := s.users.List(repo.UserListParams{
|
||||
Query: strings.TrimSpace(query.Q),
|
||||
Sort: sort,
|
||||
Dir: dir,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := 0
|
||||
if total > 0 {
|
||||
totalPages = int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
}
|
||||
if totalPages > 0 && page > totalPages {
|
||||
page = totalPages
|
||||
users, total, err = s.users.List(repo.UserListParams{
|
||||
Query: strings.TrimSpace(query.Q),
|
||||
Sort: sort,
|
||||
Dir: dir,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
hasPrev := page > 1
|
||||
hasNext := totalPages > 0 && page < totalPages
|
||||
|
||||
return &UsersPage{
|
||||
Users: users,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
HasPrev: hasPrev,
|
||||
HasNext: hasNext,
|
||||
PrevPage: max(1, page-1),
|
||||
NextPage: page + 1,
|
||||
Sort: sort,
|
||||
Dir: dir,
|
||||
Q: strings.TrimSpace(query.Q),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *UsersService) GetByID(id uint) (*models.User, error) {
|
||||
return s.users.FindByID(id)
|
||||
}
|
||||
|
||||
func normalizeSort(sort string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(sort)) {
|
||||
case "id", "name", "email":
|
||||
return strings.ToLower(strings.TrimSpace(sort))
|
||||
default:
|
||||
return "id"
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeDir(dir string) string {
|
||||
if strings.ToLower(strings.TrimSpace(dir)) == "desc" {
|
||||
return "desc"
|
||||
}
|
||||
return "asc"
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user