208762f0d1
views/top.ejs, views/bottom.ejs and public/lib/js/app-base.js are now byte-identical across sso-manager-node, proxy and jump-host. Everything per-app moved into utils/ui.js, exposed to every render as `ui` via app.locals (nav items + their group gates, footer repo/docs/ToS links, favicon, profile/logout targets, update-banner on/off + label). Client framework changes: - One gating model everywhere: app-base.js reveals .group-required-<cn> for each of the current user user/me groups. sso-manager-node sends LDAP DNs in memberOf, the OIDC clients send CNs in groups; both normalise to CNs, and the clients isAdmin flag becomes a synthetic `admin` group, so proxy nav-admin items are now group-required-admin. - user/me is fetched once per page load and cached (app.auth.loadUser); nav, forceLogin and group-required elements all read that one promise. - isLoggedIn is dual-mode (Promise + node-style callback), so the async and callback call styles both work from one shared top.ejs. - forceLogin no longer uses $.holdReady (removed in jQuery 4): it redirects to /login?redirect=<path>, and still enforces required groups. - logOut only clears the session; the caller decides where to go next. - post/put/delete are dual-mode Promise/callback, which also removes the undefined `callback2` reference that threw on a non-function callback. Dependencies: jquery ^4.0.0 and ejs ^3.1.10 in all three apps. sso-manager-node specifics: - val.js adopts the shared superset (adds the target/hostname rules and the password policy, and fixes the let-shadowed `message` that stopped custom rule messages from reaching validateMessage). - GET /api/user/me now also reports isAdmin (membership in app_sso_admin). - public/js/app.js: $.isFunction -> typeof (removed in jQuery 4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
5.4 KiB
JavaScript
201 lines
5.4 KiB
JavaScript
( 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)";
|
|
}
|
|
}
|
|
}
|
|
});
|
|
})(); |