mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-10-06 12:50:10 +00:00
Merge da22e0777e
into c4df89df1f
This commit is contained in:
@@ -202,7 +202,49 @@ module.exports = {
|
||||
return fetch('get', '');
|
||||
},
|
||||
|
||||
Mfa: {
|
||||
create: function () {
|
||||
return fetch('post', 'mfa/create');
|
||||
},
|
||||
enable: function (token) {
|
||||
return fetch('post', 'mfa/enable', {token: token});
|
||||
},
|
||||
check: function () {
|
||||
return fetch('get', 'mfa/check');
|
||||
},
|
||||
delete: function (secret) {
|
||||
return fetch('delete', 'mfa/delete', {secret: secret});
|
||||
}
|
||||
},
|
||||
|
||||
Tokens: {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} identity
|
||||
* @param {String} secret
|
||||
* @param {String} token
|
||||
* @param {Boolean} wipe
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
loginWithMFA: function (identity, secret, mfaToken, wipe) {
|
||||
return fetch('post', 'tokens', {identity: identity, secret: secret, mfa_token: mfaToken})
|
||||
.then(response => {
|
||||
if (response.token) {
|
||||
if (wipe) {
|
||||
Tokens.clearTokens();
|
||||
}
|
||||
|
||||
// Set storage token
|
||||
Tokens.addToken(response.token);
|
||||
return response.token;
|
||||
} else {
|
||||
Tokens.clearTokens();
|
||||
throw(new Error('No token returned'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} identity
|
||||
|
@@ -25,6 +25,27 @@
|
||||
<div class="invalid-feedback secret-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<label class="form-label mfa-label"><%- i18n('mfa', 'mfa') %></label>
|
||||
<button type="button" class="btn btn-info mfa-add"><%- i18n('mfa', 'mfa-add') %></button>
|
||||
<button type="button" class="btn btn-danger mfa-remove" style="display: none;"><%- i18n('mfa', 'mfa-remove') %></button>
|
||||
<div class="mfa-remove-confirm-container" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><%- i18n('mfa', 'confirm-password') %></label>
|
||||
<input name="mfa_password" type="password" class="form-control mfa-remove-password-field" placeholder="<%- i18n('mfa', 'enter-password') %>">
|
||||
<div class="invalid-feedback mfa-error"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger mfa-remove-confirm"><%- i18n('mfa', 'confirm-remove-mfa') %></button>
|
||||
</div>
|
||||
<p class="qr-instructions" style="display: none;"><%- i18n('mfa', 'mfa-setup-instruction') %></p>
|
||||
<div class="mfa-validation-container" style="display: none;">
|
||||
<label class="form-label"><%- i18n('mfa', 'mfa-token') %> <span class="form-required">*</span></label>
|
||||
<input name="mfa_validation" type="text" class="form-control" placeholder="000000" value="">
|
||||
<div class="invalid-feedback mfa-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (isAdmin() && !isSelf()) { %>
|
||||
<div class="col-sm-12 col-md-12">
|
||||
<div class="form-label"><%- i18n('roles', 'title') %></div>
|
||||
|
@@ -14,7 +14,15 @@ module.exports = Mn.View.extend({
|
||||
buttons: '.modal-footer button',
|
||||
cancel: 'button.cancel',
|
||||
save: 'button.save',
|
||||
error: '.secret-error'
|
||||
error: '.secret-error',
|
||||
mfaError: '.mfa-error',
|
||||
addMfa: '.mfa-add',
|
||||
mfaValidation: '.mfa-validation-container',
|
||||
qrInstructions: '.qr-instructions',
|
||||
removeMfa: '.mfa-remove',
|
||||
removeMfaConfirmContainer: '.mfa-remove-confirm-container',
|
||||
removeMfaConfirm: '.mfa-remove-confirm',
|
||||
removeMfaPassword: '.mfa-remove-password-field'
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -25,6 +33,10 @@ module.exports = Mn.View.extend({
|
||||
let view = this;
|
||||
let data = this.ui.form.serializeJSON();
|
||||
|
||||
let mfaToken = data.mfa_validation;
|
||||
delete data.mfa_validation;
|
||||
delete data.mfa_password;
|
||||
|
||||
let show_password = this.model.get('email') === 'admin@example.com';
|
||||
|
||||
// admin@example.com is not allowed
|
||||
@@ -62,6 +74,19 @@ module.exports = Mn.View.extend({
|
||||
}
|
||||
|
||||
view.model.set(result);
|
||||
|
||||
if (mfaToken) {
|
||||
return App.Api.Mfa.enable(mfaToken)
|
||||
.then(() => result)
|
||||
.catch(err => {
|
||||
view.ui.mfaError.text(err.message).show();
|
||||
err.mfaHandled = true;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.then(result => {
|
||||
App.UI.closeModal(function () {
|
||||
if (method === App.Api.Users.create) {
|
||||
// Show permissions dialog immediately
|
||||
@@ -72,9 +97,50 @@ module.exports = Mn.View.extend({
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.ui.error.text(err.message).show();
|
||||
if (!err.mfaHandled) {
|
||||
this.ui.error.text(err.message).show();
|
||||
}
|
||||
this.ui.buttons.prop('disabled', false).removeClass('btn-disabled');
|
||||
});
|
||||
},
|
||||
'click @ui.addMfa': function (e) {
|
||||
let view = this;
|
||||
App.Api.Mfa.create()
|
||||
.then(response => {
|
||||
view.ui.addMfa.replaceWith(`<img class="qr-code" src="${response.qrCode}" alt="QR Code">`);
|
||||
view.ui.qrInstructions.show();
|
||||
view.ui.mfaValidation.show();
|
||||
// Add required attribute once MFA is activated
|
||||
view.ui.mfaValidation.find('input[name="mfa_validation"]').attr('required', true);
|
||||
})
|
||||
.catch(err => {
|
||||
view.ui.error.text(err.message).show();
|
||||
});
|
||||
},
|
||||
'click @ui.removeMfa': function (e) {
|
||||
// Show confirmation section with a password field and confirm button
|
||||
this.ui.removeMfa.hide();
|
||||
this.ui.removeMfaConfirmContainer.show();
|
||||
},
|
||||
'click @ui.removeMfaConfirm': function (e) {
|
||||
let view = this;
|
||||
let password = view.ui.removeMfaPassword.val();
|
||||
if (!password) {
|
||||
view.ui.error.text('Password required to remove MFA').show();
|
||||
return;
|
||||
}
|
||||
App.Api.Mfa.delete(password)
|
||||
.then(() => {
|
||||
view.ui.addMfa.show();
|
||||
view.ui.qrInstructions.hide();
|
||||
view.ui.mfaValidation.hide();
|
||||
view.ui.removeMfaConfirmContainer.hide();
|
||||
view.ui.removeMfa.hide();
|
||||
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
||||
})
|
||||
.catch(err => {
|
||||
view.ui.mfaError.text(err.message).show();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -104,5 +170,30 @@ module.exports = Mn.View.extend({
|
||||
if (typeof options.model === 'undefined' || !options.model) {
|
||||
this.model = new UserModel.Model();
|
||||
}
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
let view = this;
|
||||
App.Api.Mfa.check()
|
||||
.then(response => {
|
||||
if (response.active) {
|
||||
view.ui.addMfa.hide();
|
||||
view.ui.qrInstructions.hide();
|
||||
view.ui.mfaValidation.hide();
|
||||
view.ui.removeMfa.show();
|
||||
view.ui.removeMfaConfirmContainer.hide();
|
||||
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
||||
} else {
|
||||
view.ui.addMfa.show();
|
||||
view.ui.qrInstructions.hide();
|
||||
view.ui.mfaValidation.hide();
|
||||
view.ui.removeMfa.hide();
|
||||
view.ui.removeMfaConfirmContainer.hide();
|
||||
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
view.ui.error.text(err.message).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -37,8 +37,19 @@
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
},
|
||||
"mfa": {
|
||||
"mfa": "Multi Factor Authentication",
|
||||
"mfa-add": "Add Multi Factor Authentication",
|
||||
"mfa-remove": "Remove Multi Factor Authentication",
|
||||
"mfa-setup-instruction": "Scan this QR code in your authenticator app to set up MFA and then enter the current MFA code in the input field.",
|
||||
"mfa-token": "Multi factor authentication token",
|
||||
"confirm-password": "Please enter your password to confirm",
|
||||
"enter-password": "Enter Password",
|
||||
"confirm-remove-mfa": "Confirm Multi Factor Authentication removal"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login to your account"
|
||||
"title": "Login to your account",
|
||||
"mfa-required-text": "Please enter your MFA token to continue"
|
||||
},
|
||||
"main": {
|
||||
"app": "Nginx Proxy Manager",
|
||||
|
@@ -1,3 +1,4 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
@@ -24,6 +25,12 @@
|
||||
<input name="secret" type="password" class="form-control" placeholder="<%- i18n('str', 'password') %>" required>
|
||||
<div class="invalid-feedback secret-error"></div>
|
||||
</div>
|
||||
<div class="form-group mfa-group" style="display: none;">
|
||||
<p class="mfa-info"><%- i18n('login', 'mfa-required-text') %>:</p>
|
||||
<label class="form-label"><%- i18n('mfa', 'mfa-token') %></label>
|
||||
<input name="mfa_token" type="text" class="form-control" placeholder="<%- i18n('mfa', 'mfa-token') %>">
|
||||
<div class="invalid-feedback mfa-error"></div>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-teal btn-block"><%- i18n('str', 'sign-in') %></button>
|
||||
</div>
|
||||
@@ -34,4 +41,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -13,7 +13,11 @@ module.exports = Mn.View.extend({
|
||||
identity: 'input[name="identity"]',
|
||||
secret: 'input[name="secret"]',
|
||||
error: '.secret-error',
|
||||
button: 'button'
|
||||
error_mfa:'.mfa-error',
|
||||
button: 'button',
|
||||
mfaGroup: '.mfa-group', // added MFA group selector
|
||||
mfaToken: 'input[name="mfa_token"]', // added MFA token input
|
||||
mfaInfo: '.mfa-info' // added MFA info element
|
||||
},
|
||||
|
||||
events: {
|
||||
@@ -22,14 +26,36 @@ module.exports = Mn.View.extend({
|
||||
this.ui.button.addClass('btn-loading').prop('disabled', true);
|
||||
this.ui.error.hide();
|
||||
|
||||
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
|
||||
if(this.ui.mfaToken.val()) {
|
||||
Api.Tokens.loginWithMFA(this.ui.identity.val(), this.ui.secret.val(), this.ui.mfaToken.val(), true)
|
||||
.then(() => {
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(err => {
|
||||
this.ui.error.text(err.message).show();
|
||||
if (err.message === 'Invalid MFA token.') {
|
||||
this.ui.error_mfa.text(err.message).show();
|
||||
} else {
|
||||
this.ui.error.text(err.message).show();
|
||||
}
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
} else {
|
||||
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
|
||||
.then(() => {
|
||||
window.location = '/';
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message === 'MFA token required') {
|
||||
this.ui.mfaGroup.show();
|
||||
this.ui.mfaInfo.show();
|
||||
} else {
|
||||
this.ui.error.text(err.message).show();
|
||||
}
|
||||
this.ui.button.removeClass('btn-loading').prop('disabled', false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,3 +66,5 @@ module.exports = Mn.View.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user