Skip to content

Update server.json - #3

Open
garyknight wants to merge 7 commits into
coldbox-templates:developmentfrom
loebelectric:development
Open

Update server.json#3
garyknight wants to merge 7 commits into
coldbox-templates:developmentfrom
loebelectric:development

Conversation

@garyknight

Copy link
Copy Markdown

Quickstart Step 9, reference port 8080, but the server.json didn't set it.

Type of change

  • Bug Fix

garyknight and others added 5 commits August 3, 2026 08:10
Quickstart Step 9, reference port 8080, but the server.json didn't set it.
Quickstart Step 3: box install. The installPaths in the box.json will double nest the installation of ColdBox, TestBox and all the required modules. Example below:

"installPaths":{
		"coldbox":"lib/coldbox/coldbox/",
        "testbox":"lib/testbox/testbox/",
        "qb":"lib/modules/qb/qb/"
    },

Removed all modules and modified the coldbox and testbox paths to just the lib/ path.
…"uuid")

Changed @Generator("uuid") to @Generator("guid") across all six ORM entity primary keys:

Setting.bx
User.bx
APIToken.bx
Permission.bx
RememberToken.bx
Role.bx
Root cause: The uuid strategy in bx-orm generates ColdFusion-style UUIDs (XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXXXXXX — 3 hyphens, 4 groups), which SQL Server's uniqueidentifier type rejects with:

SQLServerException: Conversion failed when converting from a character string to uniqueidentifier.

Because preFlightCheck() runs inside onAppInit, this caused the application to fail on every startup.

Fix: The guid strategy delegates primary key generation to SQL Server's native NEWID() function via SELECT NEWID() before each INSERT, producing a valid RFC 4122 uniqueidentifier value that bypasses the string conversion entirely.

Note for bx-orm maintainers: The underlying bug is that the uuid generator should produce java.util.UUID.randomUUID().toString() (RFC 4122, 4-hyphen) rather than createUUID() (CF legacy, 3-hyphen format). See the attached bug report for full analysis.
findAllWhere( criteria: { "user": userId_string } ) passed a raw string to a many-to-one association property. Hibernate's Criteria API cannot resolve a primitive string against an association — it throws before generating SQL, which propagated past the RestHandler and caused the framework to render the debug HTML error page. The browser received <!DOCTYPE html> where it expected JSON, producing the Unexpected token '<' error displayed in the Profile page.

Change: Replaced the findAllWhere call in APITokenService.listForUser with an HQL executeQuery that traverses the association explicitly via user.userId. This is database-agnostic and is the correct approach for querying through a many-to-one relationship in Hibernate.
Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
…PasswordChange

Bug 1 — securityService.isValidPassword() did not exist

doPasswordChange called securityService.isValidPassword( newPassword ) for server-side password complexity validation, but the method was never defined on SecurityService. This caused an immediate "method not found" runtime exception, which propagated past the RestHandler and rendered the debug HTML page. The browser received <!DOCTYPE html> where it expected JSON, triggering the generic "The request could not be completed" catch-block message.

Added isValidPassword() to SecurityService matching the same rules enforced by the client-side strength meter: minimum 8 characters, at least one uppercase letter, one lowercase letter, one digit, and one special character.

Bug 2 — setPassword() returns void, breaking the userService.save() call

userService.save( user: prc.authUser.setPassword( newPassword ), passwordChange: true ) passed the return value of the auto-generated property setter as the user argument. Since CFML/BoxLang property setters return void, user resolved to null, causing userService.save() to fail on the required argument. Fixed by separating the two statements:

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
Comment thread public/Application.bx
garyknight and others added 2 commits August 3, 2026 14:27
File changed: UsersForm.js

Problem
The Active, Pending, Inactive, and All count badges in the User Management listing were stale after a user was deleted or their status changed. The counts only reflected the state at initial page load and would not update until the browser was refreshed.

Root Cause
The refreshUsers() method fetches the latest user listing from /users/search after every mutating operation (delete, invite). The search endpoint already returns a fresh counts object in its response payload (result.data.counts), populated by userService.userCountsReport(). However, refreshUsers() only applied records and count from the response — it never read counts, so this.counts (which drives the tab badges) remained set to the value from the initial server-rendered payload.

Fix
Added a single line to refreshUsers() to apply the returned counts to component state:

The fallback to this.counts preserves the existing value on unexpected response shapes rather than zeroing out the badges.

Impact
No backend changes required — the API was already returning the correct data.
The Active, Pending, Inactive, and All tab counts now update immediately after any operation that triggers refreshUsers() (delete, invite).
No behavioral change for any other part of the page.

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
Overview
This PR completes the User Management section of the admin area. The existing listing page had partial functionality and the detail/edit page was non-functional. Changes span the handler, service layer, routing, views, and Alpine JS components.

Bug Fixes
Status tab counts stale after delete or invite
The Active / Pending / Inactive / All count badges were only set from the initial server-rendered payload and never refreshed. The /users/search endpoint already returned a fresh counts object — refreshUsers() simply wasn't reading it. One line added to apply result.data.counts after every fetch.

Manage User loaded JSON instead of a form (Users.show)
The show handler never called event.setView(), so ColdBox fell back to its JSON response format. Additionally, the view expected prc.selectedUser with derived display fields (name, initials, status, authMethod, roles as string array, permissions as string array), but the handler only set prc.user (the raw memento). Fixed by building prc.selectedUser with all required derived fields and adding event.setView("users/detail").

is2FactorAuth KeyNotFoundException on user detail
is2FactorAuth is not included in the User memento's defaultIncludes. The handler was accessing it directly on the memento struct after getMemento() had already discarded it. Fixed by reading entity.getIs2FactorAuth() before calling getMemento().

Hibernate could not resolve property: roles.role
The admin count query used criteria.eq("roles.role", "Admin") which Hibernate cannot resolve because the property name role collides with the entity name. Fixed by looking up the Admin Role entity first via roleService.findWhere(), then filtering on roles.roleId — the same path notation the existing search function already uses successfully.

New Features
Fully wired User Detail page
Created UserDetailForm.js Alpine component with form state, saveChanges() (PUT /users/:userId), toggleLock() (PATCH /users/:userId/lock), approveUser() (PATCH /users/:userId/verify), and deleteUser() (DELETE /users/:userId).
detail.bxm wrapped in x-data, all inputs bound via x-model, role select driven by Alpine x-for.
Added lock, verify handler actions and corresponding routes.
Reactive status chip
The Active / Pending / Inactive chip in the sidebar was server-rendered and never changed. A status getter was added to UserDetailForm that derives the value from reactive form.isActive and the stored verifiedAt — the chip updates immediately when the account is locked or approved.

Approve pending users
Admins can now approve pending users directly from the detail page. An "Approve User" button appears only when status === 'Pending' and disappears automatically after approval without a page reload.

Delete user from detail page
A "Delete User" button was added to the detail page, reusing the existing users/deleteConfirm modal. deleteTarget carries both userId and name to match the index page's pattern exactly. After successful deletion the user is redirected to users.

Remember selected status filter tab
The selected status filter (Active / Pending / Inactive / All) is now persisted to sessionStorage. Navigating to a user's detail page and returning to the listing restores the previously selected tab automatically. The value is scoped to the browser tab's lifetime.

Prevent deleting the only admin
userCountsReport() in UserService now includes an admins count (active users in the Admin role).
On the listing page, the delete button is hidden (x-show) when counts.admins <= 1 and the user has the Admin role.
On the detail page, UserDetailForm receives adminCount as a parameter and exposes an isLastAdmin getter; the delete button is hidden when it returns true.
No backend enforcement — if a second admin is later added the button reappears without any page reload.
Routes Added
Method	Path	Action
PATCH	/users/:userId/lock	Users.lock
PATCH	/users/:userId/verify	Users.verify

Co-Authored-By: Copilot <198982749+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants