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" ) // 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) { var pageOpts *db.PageOpts if r.Method == "GET" { pageOpts = pageOptsFromQuery(s, w, r) } else { pageOpts = pageOptsFromForm(s, w, r) } if pageOpts == nil { return } var rolesList *db.List[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.GetRoles(ctx, tx, pageOpts) if err != nil { return false, errors.Wrap(err, "db.GetRoles") } return true, nil }); !ok { return } if r.Method == "GET" { renderSafely(adminview.RolesPage(rolesList), s, r, w) } else { renderSafely(adminview.RolesList(rolesList), s, r, w) } }) } // AdminRoleCreateForm shows the create role form modal func AdminRoleCreateForm(s *hws.Server) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 } pageOpts := pageOptsFromForm(s, w, r) if pageOpts == nil { return } var rolesList *db.List[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.GetRoles(ctx, tx, pageOpts) if err != nil { return false, errors.Wrap(err, "db.GetRoles") } 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 } pageOpts := pageOptsFromForm(s, w, r) if pageOpts == nil { return } var rolesList *db.List[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.GetRoles(ctx, tx, pageOpts) if err != nil { return false, errors.Wrap(err, "db.GetRoles") } 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 } pageOpts := pageOptsFromForm(s, w, r) if pageOpts == nil { return } var rolesList *db.List[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.GetRoles(ctx, tx, pageOpts) if err != nil { return false, errors.Wrap(err, "db.GetRoles") } return true, nil }); !ok { return } renderSafely(adminview.RolesList(rolesList), s, r, w) }) }