admin page updates
This commit is contained in:
@@ -1,25 +1,371 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
"git.haelnorr.com/h/oslstats/internal/auditlog"
|
||||
"git.haelnorr.com/h/oslstats/internal/db"
|
||||
"git.haelnorr.com/h/oslstats/internal/roles"
|
||||
"git.haelnorr.com/h/oslstats/internal/throw"
|
||||
"git.haelnorr.com/h/oslstats/internal/validation"
|
||||
adminview "git.haelnorr.com/h/oslstats/internal/view/adminview"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// AdminRolesPage renders the full admin dashboard page with roles section
|
||||
func AdminRolesPage(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
// AdminRoles renders the full admin dashboard page with roles section
|
||||
func AdminRoles(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Load roles from database
|
||||
renderSafely(adminview.RolesPage(), s, r, w)
|
||||
var rolesList []*db.Role
|
||||
if ok := db.WithReadTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
var err error
|
||||
rolesList, err = db.ListAllRoles(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllRoles")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
renderSafely(adminview.RolesPage(rolesList), s, r, w)
|
||||
} else {
|
||||
renderSafely(adminview.RolesList(rolesList), s, r, w)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRolesList shows all roles (HTMX content replacement)
|
||||
func AdminRolesList(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
// AdminRoleCreateForm shows the create role form modal
|
||||
func AdminRoleCreateForm(s *hws.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Load roles from database
|
||||
renderSafely(adminview.RolesList(), s, r, w)
|
||||
renderSafely(adminview.RoleCreateForm(), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRoleCreate creates a new role
|
||||
func AdminRoleCreate(s *hws.Server, conn *bun.DB, audit *auditlog.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
name := getter.String("name").Required().Value
|
||||
displayName := getter.String("display_name").Required().Value
|
||||
description := getter.String("description").Value
|
||||
|
||||
if !getter.ValidateAndNotify(s, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
var rolesList []*db.Role
|
||||
var newRole *db.Role
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
newRole = &db.Role{
|
||||
Name: roles.Role(name),
|
||||
DisplayName: displayName,
|
||||
Description: description,
|
||||
IsSystem: false,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := db.Insert(tx, newRole).WithAudit(r, audit.Callback()).Exec(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.Insert")
|
||||
}
|
||||
|
||||
rolesList, err = db.ListAllRoles(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllRoles")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renderSafely(adminview.RolesList(rolesList), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRoleManage shows the role management modal with details and actions
|
||||
func AdminRoleManage(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
roleIDStr := r.PathValue("id")
|
||||
roleID, err := strconv.Atoi(roleIDStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var role *db.Role
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
var err error
|
||||
role, err = db.GetRoleByID(ctx, tx, roleID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetRoleByID")
|
||||
}
|
||||
if role == nil {
|
||||
return false, errors.New("role not found")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renderSafely(adminview.RoleManageModal(role), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRoleDeleteConfirm shows the delete confirmation dialog
|
||||
func AdminRoleDeleteConfirm(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
roleIDStr := r.PathValue("id")
|
||||
roleID, err := strconv.Atoi(roleIDStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var role *db.Role
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
var err error
|
||||
role, err = db.GetRoleByID(ctx, tx, roleID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetRoleByID")
|
||||
}
|
||||
if role == nil {
|
||||
return false, errors.New("role not found")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renderSafely(adminview.ConfirmDeleteRole(roleID, role.DisplayName), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRoleDelete deletes a role
|
||||
func AdminRoleDelete(s *hws.Server, conn *bun.DB, audit *auditlog.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
roleIDStr := r.PathValue("id")
|
||||
roleID, err := strconv.Atoi(roleIDStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var rolesList []*db.Role
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
// First check if role exists and get its details
|
||||
role, err := db.GetRoleByID(ctx, tx, roleID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetRoleByID")
|
||||
}
|
||||
if role == nil {
|
||||
return false, errors.New("role not found")
|
||||
}
|
||||
|
||||
// Check if it's a system role
|
||||
if role.IsSystem {
|
||||
return false, errors.New("cannot delete system roles")
|
||||
}
|
||||
|
||||
// Delete the role with audit logging
|
||||
err = db.DeleteByID[db.Role](tx, roleID).WithAudit(r, audit.Callback()).Delete(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.DeleteByID")
|
||||
}
|
||||
|
||||
// Reload roles
|
||||
rolesList, err = db.ListAllRoles(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllRoles")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renderSafely(adminview.RolesList(rolesList), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRolePermissionsModal shows the permissions management modal for a role
|
||||
func AdminRolePermissionsModal(s *hws.Server, conn *bun.DB) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
roleIDStr := r.PathValue("id")
|
||||
roleID, err := strconv.Atoi(roleIDStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var role *db.Role
|
||||
var allPermissions []*db.Permission
|
||||
var groupedPerms []adminview.PermissionsByResource
|
||||
var rolePermIDs map[int]bool
|
||||
|
||||
if ok := db.WithNotifyTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
// Load role with permissions
|
||||
var err error
|
||||
role, err = db.GetRoleWithPermissions(ctx, tx, roleID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetRoleWithPermissions")
|
||||
}
|
||||
if role == nil {
|
||||
return false, errors.New("role not found")
|
||||
}
|
||||
|
||||
// Load all permissions
|
||||
allPermissions, err = db.ListAllPermissions(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllPermissions")
|
||||
}
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Group permissions by resource
|
||||
permsByResource := make(map[string][]*db.Permission)
|
||||
for _, perm := range allPermissions {
|
||||
permsByResource[perm.Resource] = append(permsByResource[perm.Resource], perm)
|
||||
}
|
||||
|
||||
// Convert to sorted slice
|
||||
for resource, perms := range permsByResource {
|
||||
groupedPerms = append(groupedPerms, adminview.PermissionsByResource{
|
||||
Resource: resource,
|
||||
Permissions: perms,
|
||||
})
|
||||
}
|
||||
sort.Slice(groupedPerms, func(i, j int) bool {
|
||||
return groupedPerms[i].Resource < groupedPerms[j].Resource
|
||||
})
|
||||
|
||||
// Create map of current role permissions for checkbox state
|
||||
rolePermIDs = make(map[int]bool)
|
||||
for _, perm := range role.Permissions {
|
||||
rolePermIDs[perm.ID] = true
|
||||
}
|
||||
|
||||
renderSafely(adminview.RolePermissionsModal(role, groupedPerms, rolePermIDs), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
// AdminRolePermissionsUpdate updates the permissions for a role
|
||||
func AdminRolePermissionsUpdate(s *hws.Server, conn *bun.DB, audit *auditlog.Logger) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
roleIDStr := r.PathValue("id")
|
||||
roleID, err := strconv.Atoi(roleIDStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user := db.CurrentUser(r.Context())
|
||||
|
||||
getter, ok := validation.ParseFormOrNotify(s, w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get selected permission IDs from form
|
||||
permissionIDs := getter.IntList("permission_ids").Values()
|
||||
if !getter.ValidateAndNotify(s, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedPermIDs := make(map[int]bool)
|
||||
for _, id := range permissionIDs {
|
||||
selectedPermIDs[id] = true
|
||||
}
|
||||
|
||||
var rolesList []*db.Role
|
||||
if ok := db.WithWriteTx(s, w, r, conn, func(ctx context.Context, tx bun.Tx) (bool, error) {
|
||||
// Get role with current permissions
|
||||
role, err := db.GetRoleWithPermissions(ctx, tx, roleID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.GetRoleWithPermissions")
|
||||
}
|
||||
if role == nil {
|
||||
throw.NotFound(s, w, r, "Role not found")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get all permissions to know what exists
|
||||
allPermissions, err := db.ListAllPermissions(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllPermissions")
|
||||
}
|
||||
|
||||
// Build map of current permissions
|
||||
currentPermIDs := make(map[int]bool)
|
||||
for _, perm := range role.Permissions {
|
||||
currentPermIDs[perm.ID] = true
|
||||
}
|
||||
|
||||
var addedPerms []string
|
||||
var removedPerms []string
|
||||
|
||||
// Determine what to add and remove
|
||||
for _, perm := range allPermissions {
|
||||
hasNow := currentPermIDs[perm.ID]
|
||||
shouldHave := selectedPermIDs[perm.ID]
|
||||
|
||||
if shouldHave && !hasNow {
|
||||
// Add permission
|
||||
err := db.AddPermissionToRole(ctx, tx, roleID, perm.ID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.AddPermissionToRole")
|
||||
}
|
||||
addedPerms = append(addedPerms, string(perm.Name))
|
||||
} else if !shouldHave && hasNow {
|
||||
// Remove permission
|
||||
err := db.RemovePermissionFromRole(ctx, tx, roleID, perm.ID)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.RemovePermissionFromRole")
|
||||
}
|
||||
removedPerms = append(removedPerms, string(perm.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// Log the permission changes
|
||||
if len(addedPerms) > 0 || len(removedPerms) > 0 {
|
||||
details := map[string]any{
|
||||
"role_name": string(role.Name),
|
||||
}
|
||||
if len(addedPerms) > 0 {
|
||||
details["added_permissions"] = addedPerms
|
||||
}
|
||||
if len(removedPerms) > 0 {
|
||||
details["removed_permissions"] = removedPerms
|
||||
}
|
||||
err = audit.LogSuccess(ctx, tx, user, "update", "role_permissions", roleID, details, r)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "audit.LogSuccess")
|
||||
}
|
||||
}
|
||||
|
||||
// Reload roles
|
||||
rolesList, err = db.ListAllRoles(ctx, tx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "db.ListAllRoles")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
renderSafely(adminview.RolesList(rolesList), s, r, w)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user