Use party manager permissions in Users app - #376
Conversation
Adds a local-only login page and auth path so the Users app can run against a local Moqui instance during development. Keep this as a logically separate commit so it can be dropped before preparing the real app-change PR.
There was a problem hiding this comment.
Code Review
This pull request introduces support for local Moqui login, including a new LocalLogin view, updated routing guards, and Moqui-specific API integrations in UserService. It also refines permission controls across several components (UserDetails, ContactActionsPopover, and SecurityGroupActionsPopover) using newly defined permission actions and rules. Feedback on these changes highlights a potential invalid URL generation in normalizeMoquiBaseUrl when all URL sources are empty, an optimization opportunity to avoid an extra API call during permission pagination, and a bug in getExpirationTime where ISO date strings are not parsed correctly.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const normalizeMoquiBaseUrl = (instanceUrl?: string): string => { | ||
| const url = (instanceUrl || store.getters['user/getInstanceUrl'] || process.env.VUE_APP_LOCAL_MOQUI_URL || '').trim(); | ||
| const baseUrl = url | ||
| .replace(/\/rest\/s1.*$/, '') | ||
| .replace(/\/api\/?$/, '') | ||
| .replace(/\/+$/, ''); | ||
|
|
||
| return baseUrl.startsWith('http') ? `${baseUrl}/rest/s1/` : `https://${baseUrl}.hotwax.io/rest/s1/`; | ||
| } |
There was a problem hiding this comment.
If instanceUrl, user/getInstanceUrl, and VUE_APP_LOCAL_MOQUI_URL are all empty or undefined, url will be an empty string. This causes baseUrl to be empty, and since it does not start with 'http', the function returns https://.hotwax.io/rest/s1/, which is an invalid URL.
We should handle the empty url case gracefully by returning an empty string.
| const normalizeMoquiBaseUrl = (instanceUrl?: string): string => { | |
| const url = (instanceUrl || store.getters['user/getInstanceUrl'] || process.env.VUE_APP_LOCAL_MOQUI_URL || '').trim(); | |
| const baseUrl = url | |
| .replace(/\/rest\/s1.*$/, '') | |
| .replace(/\/api\/?$/, '') | |
| .replace(/\/+$/, ''); | |
| return baseUrl.startsWith('http') ? `${baseUrl}/rest/s1/` : `https://${baseUrl}.hotwax.io/rest/s1/`; | |
| } | |
| const normalizeMoquiBaseUrl = (instanceUrl?: string): string => { | |
| const url = (instanceUrl || store.getters['user/getInstanceUrl'] || process.env.VUE_APP_LOCAL_MOQUI_URL || '').trim(); | |
| if (!url) return ''; | |
| const baseUrl = url | |
| .replace(/\/rest\/s1.*$/, '') | |
| .replace(/\/api\/?$/, '') | |
| .replace(/\/+$/, ''); | |
| return baseUrl.startsWith('http') ? `${baseUrl}/rest/s1/` : `https://${baseUrl}.hotwax.io/rest/s1/`; | |
| } |
| if (resp.status === 200 && resp.data.docs?.length && !hasError(resp)) { | ||
| serverPermissions.push(...resp.data.docs.map((permission: any) => permission.permissionId)); | ||
| viewIndex++; | ||
| } else { | ||
| resp = null; | ||
| } |
There was a problem hiding this comment.
When fetching user permissions, if the returned page of permissions is not full (i.e., resp.data.docs.length < viewSize), we have already reached the end of the results. We can terminate the loop immediately instead of making an extra API call that will return an empty list. This reduces unnecessary network requests.
if (resp.status === 200 && resp.data.docs?.length && !hasError(resp)) {
serverPermissions.push(...resp.data.docs.map((permission: any) => permission.permissionId));
if (resp.data.docs.length < viewSize) {
resp = null;
} else {
viewIndex++;
}
} else {
resp = null;
}| const getExpirationTime = (expirationTime?: string | number) => { | ||
| const parsedExpiration = Number(expirationTime); | ||
| return Number.isFinite(parsedExpiration) && parsedExpiration > Date.now() | ||
| ? parsedExpiration | ||
| : Date.now() + ONE_DAY_IN_MS; | ||
| } |
There was a problem hiding this comment.
Using Number(expirationTime) will return NaN if expirationTime is an ISO date string (which is a common format for expiration times). We should use Date.parse() to support parsing both ISO date strings and numeric timestamps correctly.
const getExpirationTime = (expirationTime?: string | number) => {
if (!expirationTime) return Date.now() + ONE_DAY_IN_MS;
const parsedExpiration = typeof expirationTime === 'number' ? expirationTime : Date.parse(expirationTime);
return Number.isFinite(parsedExpiration) && parsedExpiration > Date.now()
? parsedExpiration
: Date.now() + ONE_DAY_IN_MS;
}
Business summary
Closes #375.
Visible SGC_USER permissions in the Users app should map to real Users app behavior instead of appearing as unused administrative noise. This PR wires existing party manager and party security assignment permissions into Users app access, profile/contact actions, and security group assignment while preserving the current SECURITY_* gates for security maintenance.
Changes
USERS_LIST_VIEW OR PARTYMGR_VIEW OR PARTYMGR_ADMIN.Validation
npm run lintpassed with one existing warning insrc/store/modules/permission/actions.ts: unuseddispatch.npm run buildwas run by the worker before my amend and failed on existing$routertyping errors in unrelated views (CreateUser.vue,LocalLogin.vue,Permissions.vue,UserConfirmation.vue,UserQuickSetup.vue).npm run test:unitis not available in this repo; Vue CLI reportscommand "test:unit" does not exist.