Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18119d54aa | |||
| 487e38f1a4 | |||
| 2e011dd383 | |||
| 3c12ebba16 | |||
| ffb2e99199 | |||
| 9d5f106863 | |||
| 7f00d4c845 | |||
| 1d1d29d287 | |||
| 5665504bc1 | |||
| 2ac1c30112 | |||
| 04c18eaf30 | |||
| 6835074b8b |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 221 KiB |
@@ -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)*
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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){
|
||||||
|
|||||||
@@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
type = type || 'info';
|
.replace(/>/g, '>')
|
||||||
callback = callback || function(){};
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
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)
|
||||||
|
|||||||
@@ -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)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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(){
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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();
|
||||||
|
|||||||