Compare commits

...

12 Commits

Author SHA1 Message Date
wmantly 18119d54aa Merge pull request #110 from theta42/release/1.6.1
Release 1.6.1
2026-07-27 17:24:13 -04:00
wmantly 487e38f1a4 Release 1.6.1: remove native alert()/confirm() calls
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 17:21:37 -04:00
wmantly 2e011dd383 Merge pull request #109 from theta42/fix/no-native-dialogs
Remove all native alert()/confirm() calls
2026-07-27 16:52:39 -04:00
wmantly 3c12ebba16 Remove all native alert()/confirm() calls
Native confirm() dialogs block browser automation entirely (discovered
via a frozen tab while browser-testing the app.messages/app.modal
adoption), and native alert()/confirm() are visually inconsistent with
the rest of the UI. Replaced every call site with
app.messages.action/confirm/toast:

- directory.ejs: rotateSecret/deleteResource confirms and all inline
  save/add/remove-group/edge error alerts now target #resourceModal's
  actionMessage (or, for deleteResource — called from the outer table
  row, not the modal — the page's own card).
- impersonate_modal.ejs, onboarding.ejs: no local .actionMessage target
  exists on these pages, so their alerts became page-wide toasts.
- executive.ejs: two alerts in sendNotification's validation now use the
  existing $compose target; saveTos's alert now reuses the function's
  own msgEl inline-message element instead of introducing a second
  mechanism.
- users.ejs, profile.ejs, proxy's profile.ejs: toggleActive's alert
  (no row context available at the call site) became a toast;
  revokeInvite/revokeToken/rotateToken use the row/card element already
  in scope.
- app.js: removed app.user.remove and app.oauthClient.remove, which
  contained native confirm() guards and had zero callers anywhere in the
  app — dead code, deleted rather than converted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 16:50:07 -04:00
wmantly ffb2e99199 Merge pull request #108 from theta42/release/1.6.0
Release 1.6.0
2026-07-27 14:18:21 -04:00
wmantly 9d5f106863 Release 1.6.0: adopt @simpleworkjs/frontend messages/modal/validate
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:14:32 -04:00
wmantly 7f00d4c845 Merge pull request #107 from theta42/modernize/simpleworkjs-frontend
Adopt @simpleworkjs/frontend messages/modal/validate modules
2026-07-27 14:05:36 -04:00
wmantly 1d1d29d287 Adopt @simpleworkjs/frontend's messages/modal/validate modules
Replaces the vendored app.util.actionMessage/actionConfirm/alert (the
latter added ad hoc to fix "app.util.alert is not a function") with the
published @simpleworkjs/frontend package: app.messages.action/confirm,
app.modal.open, and app.validate.js (which also replaces the identical
vendored val.js). Gains real HTML-escaping on message content and a toast
fallback when there's no inline .actionMessage target, neither of which
the vendored code had.

app.api/app.auth/app.pubsub/app.socket in app-base.js are untouched —
they're app-specific (dual-mode callback/promise API, auth-token header
injection) and not something the generic frontend package's app.js
provides, so it isn't loaded here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:35:51 -04:00
wmantly 5665504bc1 Merge pull request #106 from theta42/fix/sshpublickey-oauth-parent
Fix sshPublicKey ObjectClassViolationError and blank OAuth parent dropdown
2026-07-26 23:09:57 -04:00
wmantly 2ac1c30112 Fix sshPublicKey ObjectClassViolationError and blank OAuth parent dropdown
- User.update/addSSHkey now ensure the ldapPublicKey objectClass is present
  before writing sshPublicKey, so accounts predating that objectClass
  (e.g. the bootstrap admin) no longer 500 on PUT /api/user/:uid.
- populateHostDropdown in directory.ejs was missing an `oauth` branch,
  leaving the parent-Service picker blank when adding an OAuth Integration.
- Bump to 1.5.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 22:38:48 -04:00
wmantly 04c18eaf30 Merge pull request #105 from theta42/docs/screenshots-refresh
docs: refresh screenshots for the unified UI
2026-07-26 16:31:46 -04:00
wmantly 6835074b8b docs: refresh screenshots for the unified UI, add directory.png
Screenshots were still showing the pre-unification nav (Dashboard/Sites/
Integrations); replace with the current Users/Groups/Directory/Executive
shell and add a directory.png for the new consolidated inventory page.
Fix a couple of stale "Integrations page" / "Sites" references in the
concept docs to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 16:26:11 -04:00
29 changed files with 166 additions and 362 deletions
+16
View File
@@ -4,6 +4,22 @@ All notable changes to this project are documented here. Format loosely
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions
correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`. correspond to git tags (`vX.Y.Z`) and `nodejs/package.json`'s `version`.
## [1.6.1] - 2026-07-27
### Fixed
- **Removed every native `alert()`/`confirm()` call**, replacing them with `app.messages.action`/`confirm`/`toast`. Native `confirm()` blocks all further browser events on the page (discovered live, mid browser-verification of the 1.6.0 `app.messages`/`app.modal` adoption, on `directory.ejs`'s "Rotate Client Secret" — it froze the whole tab). Also deleted `app.user.remove`/`app.oauthClient.remove` in `public/js/app.js`, which had native `confirm()` guards and zero callers anywhere in the app.
## [1.6.0] - 2026-07-27
### Changed
- **Adopted `@simpleworkjs/frontend`'s `app.messages`, `app.modal`, and `app.validate` modules**, replacing the vendored `app.util.actionMessage`/`actionConfirm`/`alert` in `public/lib/js/app-base.js` and the vendored `public/lib/js/val.js`. Message content is now HTML-escaped (the vendored `alert()` this replaces had no escaping), and `app.messages.action` falls back to a page-wide toast when there's no inline `.actionMessage` target. `app.api`/`app.auth`/`app.pubsub`/`app.socket` are untouched — they're app-specific (dual-mode callback/promise API, `auth-token` header injection) and not something the frontend package's generic `app.js` provides.
## [1.5.1] - 2026-07-27
### Fixed
- **`PUT /api/user/:uid` 500'd with `ObjectClassViolationError` (LDAP `0x41`) when setting `sshPublicKey`** on any account created before the `ldapPublicKey` auxiliary objectClass was added to new-user creation (e.g. the bootstrap `admin` account). `User.update`'s `sshPublicKey` handling and `User.addSSHkey` (`nodejs/models/user_ldap.js`) now add the `ldapPublicKey` objectClass first (ignoring `TypeOrValueExistsError` if already present), the same pattern already used for `dateOfBirth`/`theta42Person`.
- **OAuth Integration parent dropdown was blank.** `populateHostDropdown` in `nodejs/views/directory.ejs` only built options for `kind === 'host'` and `kind === 'service'` — there was no branch for `kind === 'oauth'`, so choosing "OAuth Integration" in the Directory's add-resource modal left the parent-Service picker empty except the placeholder. Added the missing branch.
## [1.5.0] - 2026-07-26 ## [1.5.0] - 2026-07-26
### Changed ### Changed
+1 -1
View File
@@ -46,7 +46,7 @@ on, just like anyone else's.
A **group** is just a named list of accounts, used to control access. This A **group** is just a named list of accounts, used to control access. This
app has a handful of built-in groups that grant admin powers (e.g. only app has a handful of built-in groups that grant admin powers (e.g. only
people in the `app_sso_admin` group can see the Users/Groups/Integrations people in the `app_sso_admin` group can see the Users/Groups/Directory/Executive
pages at all), but you can also make your own groups for any app you pages at all), but you can also make your own groups for any app you
connect — say, a group listing everyone who should be allowed into your connect — say, a group listing everyone who should be allowed into your
photo server. Once a group exists, add or remove members from the photo server. Once a group exists, add or remove members from the
+1 -1
View File
@@ -28,7 +28,7 @@ what matters practically is the handful of concepts below.
## What's a "client"? ## What's a "client"?
Every app you connect is registered here as a **client** — a single entry Every app you connect is registered here as a **client** — a single entry
on the Integrations page representing that one app. Registering a client in the Directory representing that one app. Registering a client
gives you a **Client ID** and **Client Secret**: think of these like a gives you a **Client ID** and **Client Secret**: think of these like a
username and password, but for the *app itself* rather than for a person. username and password, but for the *app itself* rather than for a person.
You paste them into the other app's own "Single Sign-On" or "OIDC" setup You paste them into the other app's own "Single Sign-On" or "OIDC" setup
+2
View File
@@ -58,6 +58,8 @@ Resources carry a flexible `metadata` JSON object that can store essential conte
The Directory Management interface provides a **Tree View** toggle that visually nests your resources, making it easy to comprehend your network topography at a glance. You can also filter, search, and sort your entire infrastructure inventory. From the tree view, you can click the green `+` icon next to any resource to instantly add a child resource beneath it. The Directory Management interface provides a **Tree View** toggle that visually nests your resources, making it easy to comprehend your network topography at a glance. You can also filter, search, and sort your entire infrastructure inventory. From the tree view, you can click the green `+` icon next to any resource to instantly add a child resource beneath it.
<a href="images/directory.png" target="_blank"><img src="images/directory.png" alt="Directory & inventory list view" width="80%"></a>
## Slug conventions ## Slug conventions
Slugs are the stable identifiers automation keys off, so the tooling around the SSO Manager follows a shared convention: Slugs are the stable identifiers automation keys off, so the tooling around the SSO Manager follows a shared convention:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 221 KiB

+3 -2
View File
@@ -22,10 +22,11 @@ one command).
## Screenshots ## Screenshots
<a href="images/dashboard.png" target="_blank"><img src="images/dashboard.png" alt="Dashboard" width="49%"></a> <a href="images/dashboard.png" target="_blank"><img src="images/dashboard.png" alt="Executive dashboard" width="49%"></a>
<a href="images/users.png" target="_blank"><img src="images/users.png" alt="User list" width="49%"></a> <a href="images/users.png" target="_blank"><img src="images/users.png" alt="User list" width="49%"></a>
<a href="images/groups.png" target="_blank"><img src="images/groups.png" alt="Groups" width="49%"></a> <a href="images/groups.png" target="_blank"><img src="images/groups.png" alt="Groups" width="49%"></a>
<a href="images/oauth-clients.png" target="_blank"><img src="images/oauth-clients.png" alt="OAuth clients" width="49%"></a> <a href="images/directory.png" target="_blank"><img src="images/directory.png" alt="Directory & inventory" width="49%"></a>
<a href="images/oauth-clients.png" target="_blank"><img src="images/oauth-clients.png" alt="OAuth client (edit view)" width="49%"></a>
*(click any screenshot to view full size)* *(click any screenshot to view full size)*
+2
View File
@@ -64,6 +64,8 @@ Clients are managed directly from the **Directory** tab in the web UI. They are
> All client-management actions use the standard Directory API (`/api/directory-admin/resources`) and are gated by the `app_sso_directory_admin` group. > All client-management actions use the standard Directory API (`/api/directory-admin/resources`) and are gated by the `app_sso_directory_admin` group.
<a href="images/oauth-clients.png" target="_blank"><img src="images/oauth-clients.png" alt="Editing an OAuth client resource" width="80%"></a>
## Scopes ## Scopes
| Scope | Claims / access | | Scope | Claims / access |
+26
View File
@@ -473,6 +473,19 @@ User.update = async function(data){
} }
if(data.sshPublicKey){ if(data.sshPublicKey){
// Ensure the auxiliary objectClass is present before setting the attribute
// -- accounts created before ldapPublicKey was added to addPosixAccount's
// objectclass list (e.g. the bootstrap admin) won't have it yet.
try {
await client.modify(this.dn, [
new Change({
operation: 'add',
modification: new Attribute({ type: 'objectClass', values: ['ldapPublicKey'] }),
}),
]);
} catch(e) {
if(e.name !== 'TypeOrValueExistsError') throw e;
}
await client.modify(this.dn, [ await client.modify(this.dn, [
new Change({ new Change({
operation: 'replace', operation: 'replace',
@@ -784,6 +797,19 @@ User.addSSHkey = async function(data) {
let result; let result;
try { try {
await withClient(async (client) => { await withClient(async (client) => {
// Ensure the auxiliary objectClass is present before setting the attribute
// -- accounts created before ldapPublicKey was added to addPosixAccount's
// objectclass list (e.g. the bootstrap admin) won't have it yet.
try {
await client.modify(user.dn, [
new Change({
operation: 'add',
modification: new Attribute({ type: 'objectClass', values: ['ldapPublicKey'] }),
}),
]);
} catch(e) {
if (e.name !== 'TypeOrValueExistsError') throw e;
}
await client.modify(user.dn, [ await client.modify(user.dn, [
new Change({ new Change({
operation: 'add', operation: 'add',
+12 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.5.0", "version": "1.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.5.0", "version": "1.5.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
@@ -14,6 +14,7 @@
"@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/conf": "^1.2.0", "@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/directory-schema": "^1.0.0", "@simpleworkjs/directory-schema": "^1.0.0",
"@simpleworkjs/frontend": "^0.2.5",
"@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/ldap": "^1.0.0",
"@simpleworkjs/orm": "^0.2.8", "@simpleworkjs/orm": "^0.2.8",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
@@ -1278,6 +1279,15 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@simpleworkjs/frontend": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/@simpleworkjs/frontend/-/frontend-0.2.5.tgz",
"integrity": "sha512-PxR7UVPv3gRpdF0WsuAZplF1vYvKsEJQevVPhz9d72U+69vP/OH3tlaAXjtO/apMHfhT1viOPw2gMVOrPSxYZw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@simpleworkjs/ldap": { "node_modules/@simpleworkjs/ldap": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/ldap/-/ldap-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@simpleworkjs/ldap/-/ldap-1.0.0.tgz",
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "t42-sso-manager", "name": "t42-sso-manager",
"version": "1.5.0", "version": "1.6.1",
"description": "A very simple LDAP management and SSO system", "description": "A very simple LDAP management and SSO system",
"author": [ "author": [
{ {
@@ -23,10 +23,11 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0", "@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/app-stack": "^1.0.0", "@simpleworkjs/app-stack": "^1.0.0",
"@simpleworkjs/ldap": "^1.0.0", "@simpleworkjs/conf": "^1.2.0",
"@simpleworkjs/directory-schema": "^1.0.0", "@simpleworkjs/directory-schema": "^1.0.0",
"@simpleworkjs/frontend": "^0.2.5",
"@simpleworkjs/ldap": "^1.0.0",
"@simpleworkjs/orm": "^0.2.8", "@simpleworkjs/orm": "^0.2.8",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
+2 -16
View File
@@ -67,13 +67,6 @@ app.user = (function(app){
}); });
} }
function remove(args, callack){
if(!confirm('Delete '+ args.uid+ 'user?')) return false;
app.api.delete('user/'+ args.uid, function(error, data){
callack(error, data);
});
}
function changePassword(args, callack){ function changePassword(args, callack){
app.api.put('users/'+ arg.uid || '', args, function(error, data){ app.api.put('users/'+ arg.uid || '', args, function(error, data){
callack(error, data); callack(error, data);
@@ -110,7 +103,7 @@ app.user = (function(app){
return m ? m[1] : dn; return m ? m[1] : dn;
} }
return {list, remove, createInvite, setActive, dnToUid}; return {list, createInvite, setActive, dnToUid};
})(app); })(app);
@@ -306,13 +299,6 @@ app.oauthClient = (function(app){
}); });
} }
function remove(args, callack){
if(!confirm('Delete OAuth client "' + args.client_id + '"?')) return false;
app.api.delete('oauth/client/' + args.client_id, function(error, data){
callack(error, data);
});
}
function update(args, callack){ function update(args, callack){
app.api.put('oauth/client/' + args.client_id, args, function(error, data){ app.api.put('oauth/client/' + args.client_id, args, function(error, data){
callack(error, data); callack(error, data);
@@ -325,7 +311,7 @@ app.oauthClient = (function(app){
}); });
} }
return { list, add, remove, update, rotateSecret }; return { list, add, update, rotateSecret };
})(app); })(app);
app.tos = (function(app){ app.tos = (function(app){
+18 -72
View File
@@ -363,7 +363,7 @@ app.auth = (function(app){
} }
if(requiredGroups && !await memberOf(requiredGroups, user)){ if(requiredGroups && !await memberOf(requiredGroups, user)){
app.util.actionMessage( app.messages.action(
`<h1> `<h1>
<i class="fa-solid fa-triangle-exclamation"></i> <i class="fa-solid fa-triangle-exclamation"></i>
<b>You do not have permission to be here.</b> <b>You do not have permission to be here.</b>
@@ -520,68 +520,15 @@ app.util = (function(app){
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}; };
function actionMessage(message, $targetPassed, type, callback){ // escapeHtml/actionMessage/actionConfirm moved to @simpleworkjs/frontend's
message = message || ''; // app.util.escapeHtml and app.messages.action/confirm.
function escapeHtml(s){
let $target = $targetPassed.closest('div.card').find('.actionMessage'); return String(s == null ? '' : s)
if(!$target.length) $target = $($targetPassed.find('.actionMessage')[0]); .replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
type = type || 'info'; .replace(/>/g, '&gt;')
callback = callback || function(){}; .replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
if($target.html() === message) return;
if($target.html()){
$target.slideUp('fast', function(){
$target.html('')
$target.removeClass (function(index, className){
return (className.match (/(^|\s)bg-\S+/g) || []).join(' ');
});
if(message) return actionMessage(message, $target, type, callback);
$target.hide()
})
}else{
if(type) $target.addClass('bg-' + type);
// Messages that bring their own buttons (actionConfirm) are left
// alone; everything else gets the standard dismiss button.
if(!message.includes('<button')) message = `
<span class="align-middle">${message}</span>
<button class="action-close btn btn-sm btn-outline-dark float-end">
<i class="fa-solid fa-xmark"></i>
</button>
`
$target.html(message).slideDown('fast');
}
setTimeout(callback,10)
}
function actionConfirm(message, $target, type, callback){
return new Promise((resolve, reject) =>{
let id = crypto.randomUUID();
message = `
<h4 class"align-middle" >
<i class="fa-solid fa-triangle-exclamation"></i>
<b>${message}</b>
<span class="float-end">
<button type="button" class="btn btn-success confirm-${id}" data-confirm="true">
<i class="fa-solid fa-circle-check"></i>
Confirm
</button>
<button type="button" class="btn btn-danger confirm-${id}">
<i class="fa-solid fa-circle-stop"></i>
Cancel
</button>
</span>
</h4>
`
actionMessage(message, $target, type);
$("body").on('click', `.confirm-${id}`, function(){
actionMessage('', $target, type);
resolve(!!$(this).data('confirm'));
});
});
} }
$.fn.serializeObject = function() { $.fn.serializeObject = function() {
@@ -640,8 +587,7 @@ app.util = (function(app){
return { return {
downloadFile: downloadFile, downloadFile: downloadFile,
getUrlParameter: getUrlParameter, getUrlParameter: getUrlParameter,
actionMessage: actionMessage, escapeHtml: escapeHtml,
actionConfirm,
} }
})(app); })(app);
@@ -696,9 +642,9 @@ $( document ).ready(async function(){
$(this).closest('.card').slideUp('fast'); $(this).closest('.card').slideUp('fast');
}); });
$('.actionMessage').on('click', 'button.action-close', function(event){ // action-close click handling is wired by @simpleworkjs/frontend's
app.util.actionMessage(null, $(this)); // app.messages.js (delegated on document, so it also covers messages
}); // rendered after this ready handler runs).
setInterval(()=>{ setInterval(()=>{
$('.momentFromNow').each((idx, el)=>{ $('.momentFromNow').each((idx, el)=>{
@@ -729,11 +675,11 @@ function formAJAX(btn){
var method = ($form.attr('method') || 'post').toLowerCase(); var method = ($form.attr('method') || 'post').toLowerCase();
if($form.validate && !$form.validate()){ if($form.validate && !$form.validate()){
app.util.actionMessage('Please fix the form errors.', $form, 'danger') app.messages.action('Please fix the form errors.', $form, 'danger')
return false; return false;
} }
app.util.actionMessage( app.messages.action(
`<div class="spinner-border" role="status"> `<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
</div>`, </div>`,
@@ -742,7 +688,7 @@ function formAJAX(btn){
); );
app.api[method]($form.attr('action'), formData, function(error, data){ app.api[method]($form.attr('action'), formData, function(error, data){
app.util.actionMessage(data.message, $form, error ? 'danger' : 'success'); //re-populate table app.messages.action(data.message, $form, error ? 'danger' : 'success'); //re-populate table
$form.validateClear(); $form.validateClear();
if(!error){ if(!error){
$form.trigger("reset"); $form.trigger("reset");
@@ -750,7 +696,7 @@ function formAJAX(btn){
}else{ }else{
console.log('formAJAX res error', error, data) console.log('formAJAX res error', error, data)
if(data && data.name === 'ObjectValidateError'){ if(data && data.name === 'ObjectValidateError'){
app.util.actionMessage('Please fix the form errors', $form, 'danger'); //re-populate table app.messages.action('Please fix the form errors', $form, 'danger'); //re-populate table
} }
if(data && data.keys){ if(data && data.keys){
console.log('form key errors', data.keys) console.log('form key errors', data.keys)
-201
View File
@@ -1,201 +0,0 @@
( function( $ ) {
var settings = {
rule: {
eq: function(value, options){
var compare = $('[name=' + options + ']').val();
if ( value != compare ) {
return "Miss-match";
}
}
},
};
$.fn.validate = function(event) {
// let thisSettings = $.extend(true, settings, settingsObj);
let hasErrors = false;
if(this.is('[validate]')) return this.validateField(event);
if(!this.attr('isValid')){
console.log('adding reset event')
this.on('reset', function(){
$(this).attr('isValid', false);
$(this).validateClear();
})
}
this.find('[validate]').each(function(){
if(!$(this).validateField()) hasErrors = true;
});
this.attr('isValid', !hasErrors);
if(hasErrors && event) event.preventDefault();
return !hasErrors;
};
$.fn.validateClear = function(){
$(this).find('input').each(function(){
$(this).removeClass('is-invalid');
$(this).removeClass('is-valid');
})
}
$.fn.validateField = function(){
var attr = this.attr('validate').split(':'); //array of params
var rule = attr[0];
var options = attr[1];
var value = this.val(); //link to input value
var message;
if(this.prop('disabled')) return true;
//checks if field is required, and length
if(!isNaN(options) && value.length < options){
message = `Must be ${options} characters`;
}
//checks if empty to stop processing
if(!isNaN(options) && value.length === 0) {
}else if(rule in settings.rule){
message = settings.rule[rule].apply(this, [value, options]);
}
this.validateMessage(message)
return !message;
}
$.fn.validateMessage = function(message){
if(message && message !== true){
this.closest('.form-group').find('b.invalid-feedback').html(message);
this.addClass('is-invalid');
}else{
this.removeClass('is-invalid');
this.addClass('is-valid');
}
return this;
};
jQuery.extend({
validateSettings: function( settingsObj ) {
$.extend( true, settings, settingsObj );
},
validateInit: function( ettingsObj ) {
$( '[action]' ).on( 'submit', function ( event, settingsObj ){
$( this ).validate( settingsObj, event );
});
}
});
}( jQuery ));
// Host / target validation, mirrored from the backend (utils/hostname_validate.js):
// a bare hostname or IPv4 address, no protocol / "/" / ":" / whitespace. The
// incoming host may be a wildcard ("*.example.com"); the target may not.
(function(){
var LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
// Either one bare label (Docker service names, /etc/hosts entries) or a
// dotted hostname with an alphabetic TLD.
var HOSTNAME = /^(?=.{1,253}$)(?:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)$/i;
var FORBIDDEN = /[\s/:]/;
function isIPv4( value ) {
var parts = value.split( '.' );
if ( parts.length !== 4 ) return false;
return parts.every( function( p ) {
return /^(0|[1-9]\d{0,2})$/.test( p ) && Number( p ) <= 255;
});
}
// Incoming-host pattern: labels may be normal, "*" (one fragment), or "**"
// (any number of fragments, incl. a bare "**" global catch-all).
function isHostPattern( value ) {
if ( value.length > 253 ) return false;
return value.split( '.' ).every( function( l ) {
return l === '*' || l === '**' || LABEL.test( l );
});
}
function forbidden( value ) {
return FORBIDDEN.test( value ) || value.includes( '://' );
}
// Incoming host: IPv4 or a wildcard host pattern.
function checkHost( value ) {
if ( typeof value !== 'string' || value.length === 0 ) return "Required";
if ( forbidden( value ) ) return 'No protocol, "/", or ":"';
if ( isIPv4( value ) || isHostPattern( value ) ) return;
return "Enter a valid host or wildcard (*, **)";
}
// Downstream target: IPv4 or a strict hostname, no wildcard.
function checkTarget( value ) {
if ( typeof value !== 'string' || value.length === 0 ) return "Required";
if ( forbidden( value ) ) return 'No protocol, "/", or ":"';
if ( isIPv4( value ) || HOSTNAME.test( value ) ) return;
return "Enter a valid hostname or IP";
}
$.validateSettings({
rule:{
ip: function( value ) {
value = value.split( '.' );
if ( value.length != 4 ) {
return "Malformed IP";
}
$.each( value, function( key, value ) {
if( value > 255 || value < 0 ) {
return "Malformed IP";
}
});
},
// Incoming host name — hostname, IPv4, or wildcard pattern (*, **).
host: function( value ) {
return checkHost( value );
},
// Downstream target — hostname or IPv4, no wildcard.
target: function( value ) {
return checkTarget( value );
},
// Back-compat alias (no wildcard).
hostname: function( value ) {
return checkTarget( value );
},
user: function( value ) {
var reg = /^[a-z0-9\_\-\@\.]{1,32}$/;
if ( reg.test( value ) === false ) {
return "Invalid";
}
},
// Mirrors utils/password_policy.js: >= 8 chars, and either 12+ chars
// or at least 3 of {lowercase, uppercase, number, symbol}.
password: function( value ) {
if ( typeof value !== 'string' || value.length < 8 ) {
return "Password must be at least 8 characters";
}
if ( value.length >= 12 ) return;
var classes = 0;
if ( /[a-z]/.test( value ) ) classes++;
if ( /[A-Z]/.test( value ) ) classes++;
if ( /[0-9]/.test( value ) ) classes++;
if ( /[^A-Za-z0-9]/.test( value ) ) classes++;
if ( classes < 3 ) {
return "Use 3 of: lowercase, uppercase, number, symbol (or 12+ chars)";
}
}
}
});
})();
+1 -1
View File
@@ -28,7 +28,7 @@ const values ={
// every deploy and isn't cache-busted/fingerprinted. // every deploy and isn't cache-busted/fingerprinted.
mountStaticModules(router, { mountStaticModules(router, {
root: path.join(__dirname, '..'), root: path.join(__dirname, '..'),
deps: ['bootstrap', 'mustache', 'jquery', '@fortawesome', 'moment', '@popper', 'jq-repeat'], deps: ['bootstrap', 'mustache', 'jquery', '@fortawesome', 'moment', '@popper', 'jq-repeat', '@simpleworkjs/frontend'],
}); });
// Public health endpoint for container/orchestration healthchecks. // Public health endpoint for container/orchestration healthchecks.
+25 -17
View File
@@ -369,7 +369,7 @@
renderTable(); renderTable();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to load data'); app.messages.toast('Failed to load data', 'danger');
} }
} }
@@ -572,6 +572,8 @@
$target.append($('<option>').val(r.id).text(r.name + ' (' + r.slug + ')')); $target.append($('<option>').val(r.id).text(r.name + ' (' + r.slug + ')'));
} else if (kind === 'service' && (r.kind === 'host' || r.kind === 'service')) { } else if (kind === 'service' && (r.kind === 'host' || r.kind === 'service')) {
$target.append($('<option>').val(r.id).text(r.name + ' (' + r.slug + ')')); $target.append($('<option>').val(r.id).text(r.name + ' (' + r.slug + ')'));
} else if (kind === 'oauth' && r.kind === 'service') {
$target.append($('<option>').val(r.id).text(r.name + ' (' + r.slug + ')'));
} }
}); });
if (selectedId) $target.val(selectedId); if (selectedId) $target.val(selectedId);
@@ -712,25 +714,26 @@
await loadResources(); await loadResources();
if (!id && data.kind === 'oauth' && res.results && res.results._raw_secret) { if (!id && data.kind === 'oauth' && res.results && res.results._raw_secret) {
app.util.alert('OAuth Secret', 'Save this client secret, it will not be shown again: <br><br><code>' + res.results._raw_secret + '</code>', 'success'); app.modal.open({title: 'OAuth Secret', bodyHtml: 'Save this client secret, it will not be shown again: <br><br><code>' + res.results._raw_secret + '</code>'});
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert(err.message || 'Failed to save'); app.messages.action(err.message || 'Failed to save', $('#resourceModal'), 'danger');
} }
} }
async function rotateSecret() { async function rotateSecret() {
const id = $('#res-id').val(); const id = $('#res-id').val();
if (!id) return; if (!id) return;
if (!confirm('Are you sure you want to rotate the OAuth secret? Any existing integrations using the old secret will break.')) return; const ok = await app.messages.confirm('Are you sure you want to rotate the OAuth secret? Any existing integrations using the old secret will break.', $('#resourceModal'), 'warning');
if (!ok) return;
try { try {
const res = await app.api.post(`directory-admin/resources/${id}/rotate-secret`); const res = await app.api.post(`directory-admin/resources/${id}/rotate-secret`);
app.util.alert('Secret Rotated', 'Save this NEW client secret, it will not be shown again: <br><br><code>' + res.secret + '</code>', 'success'); app.modal.open({title: 'Secret Rotated', bodyHtml: 'Save this NEW client secret, it will not be shown again: <br><br><code>' + res.secret + '</code>'});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert(err.message || 'Failed to rotate secret'); app.messages.action(err.message || 'Failed to rotate secret', $('#resourceModal'), 'danger');
} }
} }
@@ -739,7 +742,7 @@
const groupCn = $('#new-group-cn').val().trim(); const groupCn = $('#new-group-cn').val().trim();
const accessLevel = $('#new-group-level').val(); const accessLevel = $('#new-group-level').val();
if (!groupCn) return alert('Group CN is required'); if (!groupCn) return app.messages.action('Group CN is required', $('#resourceModal'), 'danger');
try { try {
const res = await app.api.post('directory-admin/groups', { const res = await app.api.post('directory-admin/groups', {
resourceId, resourceId,
@@ -751,10 +754,10 @@
$('#new-group-cn').val(''); $('#new-group-cn').val('');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to add group'); app.messages.action('Failed to add group', $('#resourceModal'), 'danger');
} }
} }
async function removeGroup(id) { async function removeGroup(id) {
try { try {
await app.api.delete('directory-admin/groups/' + id); await app.api.delete('directory-admin/groups/' + id);
@@ -762,7 +765,7 @@
refreshGroupsUI($('#res-id').val()); refreshGroupsUI($('#res-id').val());
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to remove group'); app.messages.action('Failed to remove group', $('#resourceModal'), 'danger');
} }
} }
@@ -772,7 +775,7 @@
const targetId = $('#new-edge-target').val(); const targetId = $('#new-edge-target').val();
const relation = $('#new-edge-relation').val().trim() || 'hosts'; const relation = $('#new-edge-relation').val().trim() || 'hosts';
if (!targetId) return alert('Select a target resource'); if (!targetId) return app.messages.action('Select a target resource', $('#resourceModal'), 'danger');
const data = { relation }; const data = { relation };
if (dir === 'parent') { if (dir === 'parent') {
@@ -790,10 +793,10 @@
$('#new-edge-target').val(''); $('#new-edge-target').val('');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to add edge'); app.messages.action('Failed to add edge', $('#resourceModal'), 'danger');
} }
} }
async function removeEdge(id) { async function removeEdge(id) {
try { try {
await app.api.delete('directory-admin/edges/' + id); await app.api.delete('directory-admin/edges/' + id);
@@ -801,18 +804,23 @@
refreshEdgesUI($('#res-id').val()); refreshEdgesUI($('#res-id').val());
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to remove edge'); app.messages.action('Failed to remove edge', $('#resourceModal'), 'danger');
} }
} }
async function deleteResource(id) { async function deleteResource(id) {
if (!confirm('Are you sure you want to delete this resource? All relationships will be destroyed.')) return; // Called from the outer table's row button, not from inside
// #resourceModal — target the page's own card so the confirm/error
// renders somewhere actually visible.
const $target = $('#resources-list');
const ok = await app.messages.confirm('Are you sure you want to delete this resource? All relationships will be destroyed.', $target, 'danger');
if (!ok) return;
try { try {
await app.api.delete('directory-admin/resources/' + id); await app.api.delete('directory-admin/resources/' + id);
await loadResources(); await loadResources();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Failed to delete'); app.messages.action('Failed to delete', $target, 'danger');
} }
} }
</script> </script>
+9 -4
View File
@@ -117,8 +117,8 @@
const msgEl = document.getElementById('notif-result'); const msgEl = document.getElementById('notif-result');
const $compose = $('#notif-subject').closest('.card-body'); const $compose = $('#notif-subject').closest('.card-body');
if (!subject || !message) { alert('Subject and message are required.'); return; } if (!subject || !message) { app.messages.action('Subject and message are required.', $compose, 'danger'); return; }
if (!filterCheck) { alert('Choose who to send this to.'); return; } if (!filterCheck) { app.messages.action('Choose who to send this to.', $compose, 'danger'); return; }
const filterType = filterCheck.value; const filterType = filterCheck.value;
let filter_value = ''; let filter_value = '';
@@ -129,7 +129,7 @@
// trying the form out — make it a deliberate, confirmed action. // trying the form out — make it a deliberate, confirmed action.
if (filterType === 'all' || filterType === 'all_active') { if (filterType === 'all' || filterType === 'all_active') {
const label = filterType === 'all' ? 'ALL users (including inactive)' : 'all ACTIVE users'; const label = filterType === 'all' ? 'ALL users (including inactive)' : 'all ACTIVE users';
const confirmed = await app.util.actionConfirm(`Send this notification to ${label}?`, $compose, 'warning'); const confirmed = await app.messages.confirm(`Send this notification to ${label}?`, $compose, 'warning');
if (!confirmed) return; if (!confirmed) return;
} }
@@ -179,7 +179,12 @@
const resetAcceptance = document.getElementById('tos-reset-acceptance').checked; const resetAcceptance = document.getElementById('tos-reset-acceptance').checked;
const msgEl = document.getElementById('tos-result'); const msgEl = document.getElementById('tos-result');
if (!content) { alert('Terms of Service text cannot be empty.'); return; } if (!content) {
msgEl.className = 'alert alert-danger mt-2';
msgEl.textContent = 'Terms of Service text cannot be empty.';
msgEl.style.display = '';
return;
}
app.tos.update({content, resetAcceptance}, function(error, data) { app.tos.update({content, resetAcceptance}, function(error, data) {
if (error) { if (error) {
+9 -9
View File
@@ -35,7 +35,7 @@
async function addedUser(message, group, user, $form){ async function addedUser(message, group, user, $form){
let data = await app.group.get(group); let data = await app.group.get(group);
$.scope.groupCard.update('cn', group, processGroup(data.results)); $.scope.groupCard.update('cn', group, processGroup(data.results));
app.util.actionMessage(message, $("#group-card-"+group), 'success'); app.messages.action(message, $("#group-card-"+group), 'success');
$('a[href="#'+$form.closest('.tab-pane').attr('id')+'"]').tab('show'); $('a[href="#'+$form.closest('.tab-pane').attr('id')+'"]').tab('show');
setTimeout(function(group){ setTimeout(function(group){
$("body,html").animate({ scrollTop: $("#group-card-" + group).offset().top }, 0); $("body,html").animate({ scrollTop: $("#group-card-" + group).offset().top }, 0);
@@ -73,44 +73,44 @@
async function removeMember(groupCN, uid, btn) { async function removeMember(groupCN, uid, btn) {
const $item = $(btn).closest('li'); const $item = $(btn).closest('li');
$item.addClass('list-group-item-warning'); $item.addClass('list-group-item-warning');
const confirmed = await app.util.actionConfirm(`Remove "${uid}" from "${groupCN}"?`, $item, 'warning'); const confirmed = await app.messages.confirm(`Remove "${uid}" from "${groupCN}"?`, $item, 'warning');
if (!confirmed) { $item.removeClass('list-group-item-warning'); return; } if (!confirmed) { $item.removeClass('list-group-item-warning'); return; }
try { try {
const data = await app.api.delete(`group/${groupCN}/${uid}`); const data = await app.api.delete(`group/${groupCN}/${uid}`);
const groupData = await app.group.get(groupCN); const groupData = await app.group.get(groupCN);
$.scope.groupCard.update('cn', groupCN, processGroup(groupData.results)); $.scope.groupCard.update('cn', groupCN, processGroup(groupData.results));
app.util.actionMessage(data.message, $('#group-card-' + groupCN), 'success'); app.messages.action(data.message, $('#group-card-' + groupCN), 'success');
} catch(e) { } catch(e) {
$item.removeClass('list-group-item-warning'); $item.removeClass('list-group-item-warning');
app.util.actionMessage(e.message || 'Failed to remove member', $('#group-card-' + groupCN), 'danger'); app.messages.action(e.message || 'Failed to remove member', $('#group-card-' + groupCN), 'danger');
} }
} }
async function removeOwner(groupCN, uid, btn) { async function removeOwner(groupCN, uid, btn) {
const $item = $(btn).closest('li'); const $item = $(btn).closest('li');
$item.addClass('list-group-item-warning'); $item.addClass('list-group-item-warning');
const confirmed = await app.util.actionConfirm(`Remove "${uid}" as owner of "${groupCN}"?`, $item, 'warning'); const confirmed = await app.messages.confirm(`Remove "${uid}" as owner of "${groupCN}"?`, $item, 'warning');
if (!confirmed) { $item.removeClass('list-group-item-warning'); return; } if (!confirmed) { $item.removeClass('list-group-item-warning'); return; }
try { try {
const data = await app.api.delete(`group/owner/${groupCN}/${uid}`); const data = await app.api.delete(`group/owner/${groupCN}/${uid}`);
const groupData = await app.group.get(groupCN); const groupData = await app.group.get(groupCN);
$.scope.groupCard.update('cn', groupCN, processGroup(groupData.results)); $.scope.groupCard.update('cn', groupCN, processGroup(groupData.results));
app.util.actionMessage(data.message, $('#group-card-' + groupCN), 'success'); app.messages.action(data.message, $('#group-card-' + groupCN), 'success');
} catch(e) { } catch(e) {
$item.removeClass('list-group-item-warning'); $item.removeClass('list-group-item-warning');
app.util.actionMessage(e.message || 'Failed to remove owner', $('#group-card-' + groupCN), 'danger'); app.messages.action(e.message || 'Failed to remove owner', $('#group-card-' + groupCN), 'danger');
} }
} }
async function deleteGroup(cn, btn) { async function deleteGroup(cn, btn) {
const $card = $(btn).closest('.card'); const $card = $(btn).closest('.card');
const confirmed = await app.util.actionConfirm(`Delete group "${cn}"?`, $card, 'danger'); const confirmed = await app.messages.confirm(`Delete group "${cn}"?`, $card, 'danger');
if (!confirmed) return; if (!confirmed) return;
try { try {
await app.api.delete(`group/${cn}`); await app.api.delete(`group/${cn}`);
$.scope.groupCard.remove('cn', cn); $.scope.groupCard.remove('cn', cn);
} catch(e) { } catch(e) {
app.util.actionMessage(e.message || 'Failed to delete group', $card, 'danger'); app.messages.action(e.message || 'Failed to delete group', $card, 'danger');
} }
} }
+2 -2
View File
@@ -68,7 +68,7 @@
function startImpersonate(uid){ function startImpersonate(uid){
app.impersonate.create(uid, function(error, data){ app.impersonate.create(uid, function(error, data){
if(error){ if(error){
alert('Could not start impersonation: ' + (data && data.message ? data.message : 'Unknown error')); app.messages.toast('Could not start impersonation: ' + (data && data.message ? data.message : 'Unknown error'), 'danger');
return; return;
} }
$('#impersonateModalTitle').text(data.uid); $('#impersonateModalTitle').text(data.uid);
@@ -79,7 +79,7 @@ function startImpersonate(uid){
$('#impersonateStopBtn').off('click').on('click', function(){ $('#impersonateStopBtn').off('click').on('click', function(){
app.impersonate.revoke(data.uid, function(err){ app.impersonate.revoke(data.uid, function(err){
$('#impersonateModal').modal('hide'); $('#impersonateModal').modal('hide');
if(!err) app.util.actionMessage('Impersonation ended for ' + data.uid, $('body'), 'success'); if(!err) app.messages.action('Impersonation ended for ' + data.uid, $('body'), 'success');
}); });
}); });
+1 -1
View File
@@ -1,7 +1,7 @@
<%- include('top') %> <%- include('top') %>
<script type="text/javascript"> <script type="text/javascript">
function tableAJAX(message){ function tableAJAX(message){
app.util.actionMessage(message); app.messages.action(message);
} }
$(document).ready(function(){ $(document).ready(function(){
+1 -1
View File
@@ -131,6 +131,6 @@
}); });
function requestAccess(id) { function requestAccess(id) {
app.util.alert('Access Request', 'This feature is coming soon!', 'info'); app.modal.open({title: 'Access Request', bodyHtml: 'This feature is coming soon!'});
} }
</script> </script>
+1 -1
View File
@@ -39,7 +39,7 @@
app.api.post('oauth/authorize', oauthParams, function(error, data){ app.api.post('oauth/authorize', oauthParams, function(error, data){
if(error){ if(error){
$btn.prop('disabled', false).html('<i class="fa-solid fa-check"></i> Allow'); $btn.prop('disabled', false).html('<i class="fa-solid fa-check"></i> Allow');
app.util.actionMessage(data.message || 'Authorization failed.', $('#authorize-card'), 'danger'); app.messages.action(data.message || 'Authorization failed.', $('#authorize-card'), 'danger');
return; return;
} }
window.location.href = data.redirect_url; window.location.href = data.redirect_url;
+7 -7
View File
@@ -38,7 +38,7 @@
async function acceptTos() { async function acceptTos() {
var checkbox = document.getElementById('tosCheckbox'); var checkbox = document.getElementById('tosCheckbox');
if (!checkbox.checked) { if (!checkbox.checked) {
alert('Please read and check the box to accept the Terms of Service.'); app.messages.toast('Please read and check the box to accept the Terms of Service.', 'danger');
return; return;
} }
try { try {
@@ -50,14 +50,14 @@
document.getElementById('section-tos').style.display = 'none'; document.getElementById('section-tos').style.display = 'none';
checkAllDone(); checkAllDone();
} catch(e) { } catch(e) {
alert('Could not save TOS acceptance. Please try again.'); app.messages.toast('Could not save TOS acceptance. Please try again.', 'danger');
} }
} }
async function saveDob() { async function saveDob() {
var dob = document.getElementById('dobInput').value; var dob = document.getElementById('dobInput').value;
if (!dob) { if (!dob) {
alert('Please enter your date of birth.'); app.messages.toast('Please enter your date of birth.', 'danger');
return; return;
} }
try { try {
@@ -73,7 +73,7 @@
document.getElementById('section-dob').style.display = 'none'; document.getElementById('section-dob').style.display = 'none';
checkAllDone(); checkAllDone();
} catch(e) { } catch(e) {
alert('Could not save date of birth. Please try again.'); app.messages.toast('Could not save date of birth. Please try again.', 'danger');
} }
} }
@@ -81,11 +81,11 @@
var pw = document.getElementById('pwInput').value; var pw = document.getElementById('pwInput').value;
var pw2 = document.getElementById('pwInput2').value; var pw2 = document.getElementById('pwInput2').value;
if (!pw || pw.length < 5) { if (!pw || pw.length < 5) {
alert('Password must be at least 5 characters.'); app.messages.toast('Password must be at least 5 characters.', 'danger');
return; return;
} }
if (pw !== pw2) { if (pw !== pw2) {
alert('Passwords do not match.'); app.messages.toast('Passwords do not match.', 'danger');
return; return;
} }
try { try {
@@ -101,7 +101,7 @@
document.getElementById('section-password').style.display = 'none'; document.getElementById('section-password').style.display = 'none';
checkAllDone(); checkAllDone();
} catch(e) { } catch(e) {
alert('Could not change password. Please try again.'); app.messages.toast('Could not change password. Please try again.', 'danger');
} }
} }
+14 -14
View File
@@ -27,10 +27,10 @@
async function removeFromGroup(cn, btn){ async function removeFromGroup(cn, btn){
const $row = $(btn).closest('tr'); const $row = $(btn).closest('tr');
const confirmed = await app.util.actionConfirm(`Remove ${currentUser.uid} from "${cn}"?`, $row, 'warning'); const confirmed = await app.messages.confirm(`Remove ${currentUser.uid} from "${cn}"?`, $row, 'warning');
if (!confirmed) return; if (!confirmed) return;
app.api.delete('group/' + encodeURIComponent(cn) + '/' + encodeURIComponent(currentUser.uid), function(error, data){ app.api.delete('group/' + encodeURIComponent(cn) + '/' + encodeURIComponent(currentUser.uid), function(error, data){
if(error){ app.util.actionMessage((data && data.message) || 'Failed to remove from group', $row, 'danger'); return; } if(error){ app.messages.action((data && data.message) || 'Failed to remove from group', $row, 'danger'); return; }
$.scope.mygroups.remove('cn', cn); $.scope.mygroups.remove('cn', cn);
}); });
} }
@@ -43,7 +43,7 @@
for(const cn of cns){ for(const cn of cns){
await new Promise(function(resolve){ await new Promise(function(resolve){
app.api.put('group/' + encodeURIComponent(cn) + '/' + encodeURIComponent(currentUser.uid), {}, function(error, data){ app.api.put('group/' + encodeURIComponent(cn) + '/' + encodeURIComponent(currentUser.uid), {}, function(error, data){
if(error) app.util.actionMessage((data && data.message) || `Failed to add to "${cn}"`, $card, 'danger'); if(error) app.messages.action((data && data.message) || `Failed to add to "${cn}"`, $card, 'danger');
resolve(); resolve();
}); });
}); });
@@ -64,10 +64,10 @@
async function removePersonalGroupMember(memberUid, btn){ async function removePersonalGroupMember(memberUid, btn){
const $row = $(btn).closest('tr'); const $row = $(btn).closest('tr');
const confirmed = await app.util.actionConfirm(`Remove ${memberUid} from ${currentUser.uid}'s group?`, $row, 'warning'); const confirmed = await app.messages.confirm(`Remove ${memberUid} from ${currentUser.uid}'s group?`, $row, 'warning');
if (!confirmed) return; if (!confirmed) return;
app.api.delete('user/' + encodeURIComponent(currentUser.uid) + '/group-member/' + encodeURIComponent(memberUid), function(error, data){ app.api.delete('user/' + encodeURIComponent(currentUser.uid) + '/group-member/' + encodeURIComponent(memberUid), function(error, data){
if(error){ app.util.actionMessage((data && data.message) || 'Failed to remove from group', $row, 'danger'); return; } if(error){ app.messages.action((data && data.message) || 'Failed to remove from group', $row, 'danger'); return; }
$.scope.personalGroupMembers.remove('uid', memberUid); $.scope.personalGroupMembers.remove('uid', memberUid);
}); });
} }
@@ -80,7 +80,7 @@
for(const uid of uids){ for(const uid of uids){
await new Promise(function(resolve){ await new Promise(function(resolve){
app.api.put('user/' + encodeURIComponent(currentUser.uid) + '/group-member/' + encodeURIComponent(uid), {}, function(error, data){ app.api.put('user/' + encodeURIComponent(currentUser.uid) + '/group-member/' + encodeURIComponent(uid), {}, function(error, data){
if(error) app.util.actionMessage((data && data.message) || `Failed to add "${uid}"`, $card, 'danger'); if(error) app.messages.action((data && data.message) || `Failed to add "${uid}"`, $card, 'danger');
resolve(); resolve();
}); });
}); });
@@ -176,7 +176,7 @@
async function toggleActive(uid, active){ async function toggleActive(uid, active){
app.user.setActive(uid, active, async function(error, data){ app.user.setActive(uid, active, async function(error, data){
if(error) return alert('Failed to update user status'); if(error) return app.messages.toast('Failed to update user status', 'danger');
currentUser = await determinUser(); currentUser = await determinUser();
renderProfile(currentUser); renderProfile(currentUser);
}); });
@@ -184,11 +184,11 @@
async function deleteUser(uid, btn){ async function deleteUser(uid, btn){
const $card = $(btn).closest('.card'); const $card = $(btn).closest('.card');
const confirmed = await app.util.actionConfirm(`Delete user "${uid}"?`, $card, 'warning'); const confirmed = await app.messages.confirm(`Delete user "${uid}"?`, $card, 'warning');
if (!confirmed) return; if (!confirmed) return;
app.api.delete('user/' + uid, function(error, data){ app.api.delete('user/' + uid, function(error, data){
if (error) { if (error) {
app.util.actionMessage(data.message || 'Failed to delete user', $card, 'danger'); app.messages.action(data.message || 'Failed to delete user', $card, 'danger');
return; return;
} }
window.location.href = '/users'; window.location.href = '/users';
@@ -662,21 +662,21 @@
async function revokeToken(id, name, btn){ async function revokeToken(id, name, btn){
var $card = $(btn).closest('.card'); var $card = $(btn).closest('.card');
$card.addClass('table-warning'); $card.addClass('table-warning');
var confirmed = await app.util.actionConfirm('Revoke API token "' + name + '"? It stops working immediately.', $card, 'warning'); var confirmed = await app.messages.confirm('Revoke API token "' + name + '"? It stops working immediately.', $card, 'warning');
$card.removeClass('table-warning'); $card.removeClass('table-warning');
if(!confirmed) return; if(!confirmed) return;
app.apiToken.remove({id: id}, function(error, data){ app.apiToken.remove({id: id}, function(error, data){
if(error){ app.util.actionMessage('Error: ' + data.message, $card, 'danger'); return; } if(error){ app.messages.action('Error: ' + data.message, $card, 'danger'); return; }
$.scope.apiTokenCard.remove('id', id); $.scope.apiTokenCard.remove('id', id);
}); });
} }
async function rotateToken(id, name, btn){ async function rotateToken(id, name, btn){
var $card = $(btn).closest('.card'); var $card = $(btn).closest('.card');
var confirmed = await app.util.actionConfirm('Rotate API token "' + name + '"? The old token stops working immediately.', $card, 'warning'); var confirmed = await app.messages.confirm('Rotate API token "' + name + '"? The old token stops working immediately.', $card, 'warning');
if(!confirmed) return; if(!confirmed) return;
app.apiToken.rotate({id: id}, function(error, data){ app.apiToken.rotate({id: id}, function(error, data){
if(error){ app.util.actionMessage('Error: ' + data.message, $card, 'danger'); return; } if(error){ app.messages.action('Error: ' + data.message, $card, 'danger'); return; }
showSecret(data.token); showSecret(data.token);
tableAJAX(); tableAJAX();
}); });
@@ -700,7 +700,7 @@
expires_in_days: $('#edit-expires_in_days').val(), expires_in_days: $('#edit-expires_in_days').val(),
}; };
app.apiToken.update(payload, function(error, data){ app.apiToken.update(payload, function(error, data){
if(error){ app.util.actionMessage((data && data.message) || 'Update failed.', $msg.parent(), 'danger'); return; } if(error){ app.messages.action((data && data.message) || 'Update failed.', $msg.parent(), 'danger'); return; }
editModal.hide(); editModal.hide();
tableAJAX(); tableAJAX();
}); });
+3 -1
View File
@@ -21,9 +21,11 @@
<script type="text/javascript" src="/static-modules/@fortawesome/fontawesome-free/js/all.min.js"></script> <script type="text/javascript" src="/static-modules/@fortawesome/fontawesome-free/js/all.min.js"></script>
<script type="text/javascript" src='/static-modules/mustache/mustache.min.js'></script> <script type="text/javascript" src='/static-modules/mustache/mustache.min.js'></script>
<script type="text/javascript" src='/static-modules/jq-repeat/dist/js/jq-repeat.js'></script> <script type="text/javascript" src='/static-modules/jq-repeat/dist/js/jq-repeat.js'></script>
<script type="text/javascript" src='/static/lib/js/val.js'></script>
<script type="text/javascript" src="/static-modules/moment/moment.js"></script> <script type="text/javascript" src="/static-modules/moment/moment.js"></script>
<script type="text/javascript" src="/static/lib/js/app-base.js"></script> <script type="text/javascript" src="/static/lib/js/app-base.js"></script>
<script type="text/javascript" src="/static-modules/@simpleworkjs/frontend/lib/app.messages.js"></script>
<script type="text/javascript" src="/static-modules/@simpleworkjs/frontend/lib/app.modal.js"></script>
<script type="text/javascript" src="/static-modules/@simpleworkjs/frontend/lib/app.validate.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script> <script type="text/javascript" src="/static/js/app.js"></script>
</head> </head>
<body> <body>
+6 -6
View File
@@ -6,7 +6,7 @@
function renderUsers(){ function renderUsers(){
app.user.list(function(error, data){ app.user.list(function(error, data){
if(error){ if(error){
app.util.actionMessage(data.message, $('#tab-people'), 'danger'); app.messages.action(data.message, $('#tab-people'), 'danger');
return; return;
} }
$.scope.userRow.empty(); $.scope.userRow.empty();
@@ -19,7 +19,7 @@
function toggleActive(uid, active){ function toggleActive(uid, active){
app.user.setActive(uid, active, function(error, data){ app.user.setActive(uid, active, function(error, data){
if(error) return alert('Failed to update user status'); if(error) return app.messages.toast('Failed to update user status', 'danger');
renderUsers(); renderUsers();
}); });
} }
@@ -128,7 +128,7 @@
async function revokeInvite(tokenId, btn) { async function revokeInvite(tokenId, btn) {
$thisRow = $(btn).closest('tr'); $thisRow = $(btn).closest('tr');
$thisRow.addClass('table-warning'); $thisRow.addClass('table-warning');
let confirmation = await app.util.actionConfirm('Revoke selected invite token?', $thisRow, 'warning'); let confirmation = await app.messages.confirm('Revoke selected invite token?', $thisRow, 'warning');
if(!confirmation){ if(!confirmation){
$thisRow.removeClass('table-warning'); $thisRow.removeClass('table-warning');
return; return;
@@ -137,7 +137,7 @@
await app.api.delete(`user/invite/${tokenId}`); await app.api.delete(`user/invite/${tokenId}`);
loadInvites(); loadInvites();
} catch(e) { } catch(e) {
alert('Failed to revoke invite.'); app.messages.action('Failed to revoke invite.', $thisRow, 'danger');
} }
} }
@@ -153,12 +153,12 @@
async function deleteUser(uid, btn){ async function deleteUser(uid, btn){
const $row = $(btn).closest('tr'); const $row = $(btn).closest('tr');
$row.addClass('table-warning'); $row.addClass('table-warning');
const confirmed = await app.util.actionConfirm(`Delete user "${uid}"?`, $row, 'warning'); const confirmed = await app.messages.confirm(`Delete user "${uid}"?`, $row, 'warning');
$row.removeClass('table-warning'); $row.removeClass('table-warning');
if (!confirmed) return; if (!confirmed) return;
app.api.delete('user/' + uid, function(error, data){ app.api.delete('user/' + uid, function(error, data){
if (error) { if (error) {
app.util.actionMessage(data.message || 'Failed to delete user', $row, 'danger'); app.messages.action(data.message || 'Failed to delete user', $row, 'danger');
return; return;
} }
renderUsers(); renderUsers();