This commit is contained in:
Julian Gassner
2025-02-05 16:49:04 +00:00
committed by GitHub
24 changed files with 1278 additions and 1481 deletions

View File

@@ -202,7 +202,46 @@ 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');
}
},
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

View File

@@ -33,6 +33,13 @@
<td class="<%- isExpired() ? 'text-danger' : '' %>">
<%- formatDbDate(expires_on, 'Do MMMM YYYY, h:mm a') %>
</td>
<td>
<% if (active_domain_names().length > 0) { %>
<span class="status-icon bg-success"></span> <%- i18n('certificates', 'in-use') %>
<% } else { %>
<span class="status-icon bg-danger"></span> <%- i18n('certificates', 'inactive') %>
<% } %>
</td>
<% if (canManage) { %>
<td class="text-right">
<div class="item-action dropdown">
@@ -48,7 +55,14 @@
<div class="dropdown-divider"></div>
<% } %>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
<% if (active_domain_names().length > 0) { %>
<div class="dropdown-divider"></div>
<span class="dropdown-header"><%- i18n('certificates', 'active-domain_names') %></span>
<% active_domain_names().forEach(function(host) { %>
<a href="https://<%- host %>" class="dropdown-item" target="_blank"><%- host %></a>
<% }); %>
<% } %>
</div>
</div>
</td>
<% } %>
<% } %>

View File

@@ -44,14 +44,24 @@ module.exports = Mn.View.extend({
},
},
templateContext: {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
},
dns_providers: dns_providers
templateContext: function () {
return {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
},
dns_providers: dns_providers,
active_domain_names: function () {
const { proxy_hosts = [], redirect_hosts = [], dead_hosts = [] } = this;
return [...proxy_hosts, ...redirect_hosts, ...dead_hosts].reduce((acc, host) => {
acc.push(...(host.domain_names || []));
return acc;
}, []);
}
};
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}

View File

@@ -3,6 +3,7 @@
<th><%- i18n('str', 'name') %></th>
<th><%- i18n('all-hosts', 'cert-provider') %></th>
<th><%- i18n('str', 'expires') %></th>
<th><%- i18n('str', 'status') %></th>
<% if (canManage) { %>
<th>&nbsp;</th>
<% } %>

View File

@@ -74,7 +74,7 @@ module.exports = Mn.View.extend({
e.preventDefault();
let query = this.ui.query.val();
this.fetch(['owner'], query)
this.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'], query)
.then(response => this.showData(response))
.catch(err => {
this.showError(err);
@@ -89,7 +89,7 @@ module.exports = Mn.View.extend({
onRender: function () {
let view = this;
view.fetch(['owner'])
view.fetch(['owner','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@@ -25,6 +25,17 @@
<div class="invalid-feedback secret-error"></div>
</div>
</div>
<div class="col-sm-12 col-md-12">
<label class="form-label mfa-label" style="display: none;"><%- i18n('mfa', 'mfa') %></label>
<button type="button" class="btn btn-info add-mfa"><%- i18n('mfa', 'add-mfa') %></button>
<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>
</div>
<% if (isAdmin() && !isSelf()) { %>
<div class="col-sm-12 col-md-12">
<div class="form-label"><%- i18n('roles', 'title') %></div>

View File

@@ -14,7 +14,11 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
error: '.secret-error'
error: '.secret-error',
addMfa: '.add-mfa',
mfaLabel: '.mfa-label', // added binding
mfaValidation: '.mfa-validation-container', // added binding
qrInstructions: '.qr-instructions' // added binding for instructions
},
events: {
@@ -25,6 +29,10 @@ module.exports = Mn.View.extend({
let view = this;
let data = this.ui.form.serializeJSON();
// Save "mfa_validation" value and remove it from data
let mfaToken = data.mfa_validation;
delete data.mfa_validation;
let show_password = this.model.get('email') === 'admin@example.com';
// admin@example.com is not allowed
@@ -62,6 +70,15 @@ module.exports = Mn.View.extend({
}
view.model.set(result);
if (mfaToken) {
return App.Api.Mfa.enable(mfaToken)
.then(() => result);
}
console.log(result);
return result;
})
.then(result => {
App.UI.closeModal(function () {
if (method === App.Api.Users.create) {
// Show permissions dialog immediately
@@ -75,6 +92,20 @@ module.exports = Mn.View.extend({
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();
});
}
},
@@ -104,5 +135,29 @@ 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.mfaLabel.hide();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
// Remove required attribute if MFA is active & field is hidden
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
} else {
view.ui.addMfa.show();
view.ui.mfaLabel.show();
view.ui.qrInstructions.hide();
view.ui.mfaValidation.hide();
view.ui.mfaValidation.find('input[name="mfa_validation"]').removeAttr('required');
}
})
.catch(err => {
view.ui.error.text(err.message).show();
});
}
});