Compare commits

..

3 Commits

Author SHA1 Message Date
jc21
b4f49969d6 Merge pull request #4261 from NginxProxyManager/develop
v2.12.2
2024-12-29 14:40:05 +10:00
jc21
5084cb7296 Merge pull request #4077 from NginxProxyManager/develop
v2.12.1
2024-10-17 09:49:07 +10:00
jc21
e677bfa2e8 Merge pull request #4073 from NginxProxyManager/develop
v2.12.0
2024-10-16 15:41:55 +10:00
30 changed files with 1494 additions and 1300 deletions

6
Jenkinsfile vendored
View File

@@ -128,7 +128,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
dir(path: 'testing/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
@@ -161,7 +161,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
dir(path: 'testing/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}
@@ -199,7 +199,7 @@ pipeline {
sh 'docker-compose down --remove-orphans --volumes -t 30 || true'
}
unstable {
dir(path: 'test/results') {
dir(path: 'testing/results') {
archiveArtifacts(allowEmptyArchive: true, artifacts: '**/*', excludes: '**/*.xml')
}
}

View File

@@ -258,7 +258,6 @@ const internalAccessList = {
})
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.groupBy('access_list.id')
.allowGraph('[owner,items,clients,proxy_hosts.[certificate,access_list.[clients,items]]]')
.first();

View File

@@ -313,9 +313,6 @@ const internalCertificate = {
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.first();
if (access_data.permission_visibility !== 'all') {
@@ -467,9 +464,6 @@ const internalCertificate = {
.where('is_deleted', 0)
.groupBy('id')
.allowGraph('[owner]')
.allowGraph('[proxy_hosts]')
.allowGraph('[redirection_hosts]')
.allowGraph('[dead_hosts]')
.orderBy('nice_name', 'ASC');
if (access_data.permission_visibility !== 'all') {

View File

@@ -1,76 +0,0 @@
const authModel = require('../models/auth');
const error = require('../lib/error');
const speakeasy = require('speakeasy');
module.exports = {
validateMfaTokenForUser: (userId, token) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth || !auth.mfa_enabled) {
throw new error.AuthError('MFA is not enabled for this user.');
}
const verified = speakeasy.totp.verify({
secret: auth.mfa_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
throw new error.AuthError('Invalid MFA token.');
}
return true;
});
},
isMfaEnabledForUser: (userId) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
console.log(auth);
if (!auth) {
throw new error.AuthError('User not found.');
}
return auth.mfa_enabled === true;
});
},
createMfaSecretForUser: (userId) => {
const secret = speakeasy.generateSecret({ length: 20 });
console.log(secret);
return authModel
.query()
.where('user_id', userId)
.update({
mfa_secret: secret.base32
})
.then(() => secret);
},
enableMfaForUser: (userId, token) => {
return authModel
.query()
.where('user_id', userId)
.first()
.then((auth) => {
if (!auth || !auth.mfa_secret) {
throw new error.AuthError('MFA is not set up for this user.');
}
const verified = speakeasy.totp.verify({
secret: auth.mfa_secret,
encoding: 'base32',
token: token,
window: 2
});
if (!verified) {
throw new error.AuthError('Invalid MFA token.');
}
return authModel
.query()
.where('user_id', userId)
.update({ mfa_enabled: true })
.then(() => true);
});
},
};

View File

@@ -4,7 +4,6 @@ const userModel = require('../models/user');
const authModel = require('../models/auth');
const helpers = require('../lib/helpers');
const TokenModel = require('../models/token');
const mfa = require('../internal/mfa'); // <-- added MFA import
const ERROR_MESSAGE_INVALID_AUTH = 'Invalid email or password';
@@ -22,8 +21,6 @@ module.exports = {
getTokenFromEmail: (data, issuer) => {
let Token = new TokenModel();
console.log(data);
data.scope = data.scope || 'user';
data.expiry = data.expiry || '1d';
@@ -44,66 +41,34 @@ module.exports = {
.then((auth) => {
if (auth) {
return auth.verifyPassword(data.secret)
.then(async (valid) => {
.then((valid) => {
if (valid) {
if (data.scope !== 'user' && _.indexOf(user.roles, data.scope) === -1) {
// The scope requested doesn't exist as a role against the user,
// you shall not pass.
throw new error.AuthError('Invalid scope: ' + data.scope);
}
return await mfa.isMfaEnabledForUser(user.id)
.then((mfaEnabled) => {
if (mfaEnabled) {
if (!data.mfa_token) {
throw new error.AuthError('MFA token required');
}
console.log(data.mfa_token);
return mfa.validateMfaTokenForUser(user.id, data.mfa_token)
.then((mfaValid) => {
if (!mfaValid) {
throw new error.AuthError('Invalid MFA token');
}
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
});
} else {
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
// Create a moment of the expiry expression
let expiry = helpers.parseDatePeriod(data.expiry);
if (expiry === null) {
throw new error.AuthError('Invalid expiry time: ' + data.expiry);
}
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
}
return Token.create({
iss: issuer || 'api',
attrs: {
id: user.id
},
scope: [data.scope],
expiresIn: data.expiry
})
.then((signed) => {
return {
token: signed.token,
expires: expiry.toISOString()
};
});
} else {
throw new error.AuthError(ERROR_MESSAGE_INVALID_AUTH);

View File

@@ -507,8 +507,7 @@ const internalUser = {
.then((user) => {
return internalToken.getTokenFromUser(user);
});
},
}
};
module.exports = internalUser;

View File

@@ -1,45 +0,0 @@
const migrate_name = 'identifier_for_migrate';
const logger = require('../logger').migrate;
/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Up...');
return knex.schema.alterTable('auth', (table) => {
table.string('mfa_secret');
table.boolean('mfa_enabled').defaultTo(false);
})
.then(() => {
logger.info('[' + migrate_name + '] User Table altered');
logger.info('[' + migrate_name + '] Migrating Up Complete');
});
};
/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex/*, Promise*/) {
logger.info('[' + migrate_name + '] Migrating Down...');
return knex.schema.alterTable('auth', (table) => {
table.dropColumn('mfa_key');
table.dropColumn('mfa_enabled');
})
.then(() => {
logger.info('[' + migrate_name + '] User Table altered');
logger.info('[' + migrate_name + '] Migrating Down Complete');
});
};

View File

@@ -4,6 +4,7 @@
const db = require('../db');
const helpers = require('../lib/helpers');
const Model = require('objection').Model;
const User = require('./user');
const now = require('./now_helper');
Model.knex(db);
@@ -67,11 +68,6 @@ class Certificate extends Model {
}
static get relationMappings () {
const ProxyHost = require('./proxy_host');
const DeadHost = require('./dead_host');
const User = require('./user');
const RedirectionHost = require('./redirection_host');
return {
owner: {
relation: Model.HasOneRelation,
@@ -83,39 +79,6 @@ class Certificate extends Model {
modify: function (qb) {
qb.where('user.is_deleted', 0);
}
},
proxy_hosts: {
relation: Model.HasManyRelation,
modelClass: ProxyHost,
join: {
from: 'certificate.id',
to: 'proxy_host.certificate_id'
},
modify: function (qb) {
qb.where('proxy_host.is_deleted', 0);
}
},
dead_hosts: {
relation: Model.HasManyRelation,
modelClass: DeadHost,
join: {
from: 'certificate.id',
to: 'dead_host.certificate_id'
},
modify: function (qb) {
qb.where('dead_host.is_deleted', 0);
}
},
redirection_hosts: {
relation: Model.HasManyRelation,
modelClass: RedirectionHost,
join: {
from: 'certificate.id',
to: 'redirection_host.certificate_id'
},
modify: function (qb) {
qb.where('redirection_host.is_deleted', 0);
}
}
};
}

View File

@@ -23,10 +23,8 @@
"node-rsa": "^1.0.8",
"objection": "3.0.1",
"path": "^0.12.7",
"qrcode": "^1.5.4",
"pg": "^8.13.1",
"signale": "1.4.0",
"speakeasy": "^2.0.0",
"sqlite3": "5.1.6",
"temp-write": "^4.0.0"
},

View File

@@ -27,7 +27,6 @@ router.get('/', (req, res/*, next*/) => {
router.use('/schema', require('./schema'));
router.use('/tokens', require('./tokens'));
router.use('/mfa', require('./mfa'));
router.use('/users', require('./users'));
router.use('/audit-log', require('./audit-log'));
router.use('/reports', require('./reports'));

View File

@@ -1,89 +0,0 @@
const express = require('express');
const jwtdecode = require('../lib/express/jwt-decode');
const apiValidator = require('../lib/validator/api');
const internalToken = require('../internal/token');
const schema = require('../schema');
const internalMfa = require('../internal/mfa');
const qrcode = require('qrcode');
const speakeasy = require('speakeasy');
const userModel = require('../models/user');
let router = express.Router({
caseSensitive: true,
strict: true,
mergeParams: true
});
router
.route('/')
.options((_, res) => {
res.sendStatus(204);
})
.get(async (req, res, next) => {
internalToken.getFreshToken(res.locals.access, {
expiry: (typeof req.query.expiry !== 'undefined' ? req.query.expiry : null),
scope: (typeof req.query.scope !== 'undefined' ? req.query.scope : null)
})
.then((data) => {
res.status(200)
.send(data);
})
.catch(next);
});
router
.route('/create')
.post(jwtdecode(), (req, res, next) => {
if (!res.locals.access) {
return next(new Error('Invalid token'));
}
const userId = res.locals.access.token.getUserId();
internalMfa.createMfaSecretForUser(userId)
.then((secret) => {
return userModel.query()
.where('id', '=', userId)
.first()
.then((user) => {
if (!user) {
return next(new Error('User not found'));
}
return { secret, user };
});
})
.then(({ secret, user }) => {
const otpAuthUrl = speakeasy.otpauthURL({
secret: secret.ascii,
label: user.email,
issuer: 'Nginx Proxy Manager'
});
qrcode.toDataURL(otpAuthUrl, (err, dataUrl) => {
if (err) {
console.error('Error generating QR code:', err);
return next(err);
}
res.status(200).send({ qrCode: dataUrl });
});
})
.catch(next);
});
router
.route('/enable')
.post(jwtdecode(), (req, res, next) => {
apiValidator(schema.getValidationSchema('/mfa', 'post'), req.body).then((params) => {
internalMfa.enableMfaForUser(res.locals.access.token.getUserId(), params.token)
.then(() => res.status(200).send({ success: true }))
.catch(next);
}
);});
router
.route('/check')
.get(jwtdecode(), (req, res, next) => {
internalMfa.isMfaEnabledForUser(res.locals.access.token.getUserId())
.then((active) => res.status(200).send({ active }))
.catch(next);
});
module.exports = router;

View File

@@ -1,41 +0,0 @@
{
"operationId": "enableMfa",
"summary": "Enable multi-factor authentication for a user",
"tags": ["MFA"],
"requestBody": {
"description": "MFA Token Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"token": {
"type": "string",
"minLength": 1
}
},
"required": ["token"]
}
}
}
},
"responses": {
"200": {
"description": "MFA enabled successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
}
}
}
}
}
}
}

View File

@@ -22,10 +22,6 @@
"secret": {
"minLength": 1,
"type": "string"
},
"mfa_token": {
"minLength": 1,
"type": "string"
}
},
"required": ["identity", "secret"],

View File

@@ -14,7 +14,7 @@
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"additionalProperties": false,
"required": ["name", "nickname", "email"],
"properties": {
"name": {

View File

@@ -15,11 +15,6 @@
"$ref": "./paths/get.json"
}
},
"/mfa": {
"post": {
"$ref": "./paths/mfa/post.json"
}
},
"/audit-log": {
"get": {
"$ref": "./paths/audit-log/get.json"

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@ services:
- ca.internal
pdns:
image: pschiffe/pdns-mysql:4.8
image: pschiffe/pdns-mysql
volumes:
- '/etc/localtime:/etc/localtime:ro'
environment:

View File

@@ -132,7 +132,7 @@ services:
- 8128:3128
pdns:
image: pschiffe/pdns-mysql:4.8
image: pschiffe/pdns-mysql
container_name: npm2dev.pdns
volumes:
- '/etc/localtime:/etc/localtime:ro'

View File

@@ -1065,9 +1065,9 @@ vfile@^6.0.0:
vfile-message "^4.0.0"
vite@^5.4.8:
version "5.4.14"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
version "5.4.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8"
integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"

View File

@@ -202,47 +202,8 @@ 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
* @param {String} secret

View File

@@ -33,13 +33,6 @@
<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">
@@ -55,13 +48,6 @@
<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,24 +44,14 @@ module.exports = Mn.View.extend({
},
},
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;
}, []);
}
};
templateContext: {
canManage: App.Cache.User.canManage('certificates'),
isExpired: function () {
return moment(this.expires_on).isBefore(moment());
},
dns_providers: dns_providers
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}

View File

@@ -3,7 +3,6 @@
<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','proxy_hosts', 'dead_hosts', 'redirection_hosts'], query)
this.fetch(['owner'], 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','proxy_hosts', 'dead_hosts', 'redirection_hosts'])
view.fetch(['owner'])
.then(response => {
if (!view.isDestroyed()) {
if (response && response.length) {

View File

@@ -25,17 +25,6 @@
<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,11 +14,7 @@ module.exports = Mn.View.extend({
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
error: '.secret-error',
addMfa: '.add-mfa',
mfaLabel: '.mfa-label', // added binding
mfaValidation: '.mfa-validation-container', // added binding
qrInstructions: '.qr-instructions' // added binding for instructions
error: '.secret-error'
},
events: {
@@ -29,10 +25,6 @@ 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
@@ -70,15 +62,6 @@ 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
@@ -92,20 +75,6 @@ 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();
});
}
},
@@ -135,29 +104,5 @@ 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();
});
}
});

View File

@@ -37,15 +37,8 @@
"all": "All",
"any": "Any"
},
"mfa": {
"mfa": "Multi Factor Authentication",
"add-mfa": "Generate secret",
"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"
},
"login": {
"title": "Login to your account",
"mfa-required-text": "Please enter your MFA token to continue"
"title": "Login to your account"
},
"main": {
"app": "Nginx Proxy Manager",
@@ -213,10 +206,7 @@
"reachability-other": "There is a server found at this domain but it returned an unexpected status code {code}. Is it the NPM server? Please make sure your domain points to the IP where your NPM instance is running.",
"download": "Download",
"renew-title": "Renew Let's Encrypt Certificate",
"search": "Search Certificate…",
"in-use" : "In use",
"inactive": "Inactive",
"active-domain_names": "Active domain names"
"search": "Search Certificate…"
},
"access-lists": {
"title": "Access Lists",

View File

@@ -1,4 +1,3 @@
<div class="container">
<div class="row">
<div class="col col-login mx-auto">
@@ -25,12 +24,6 @@
<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>

View File

@@ -13,11 +13,7 @@ module.exports = Mn.View.extend({
identity: 'input[name="identity"]',
secret: 'input[name="secret"]',
error: '.secret-error',
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
button: 'button'
},
events: {
@@ -26,36 +22,14 @@ module.exports = Mn.View.extend({
this.ui.button.addClass('btn-loading').prop('disabled', true);
this.ui.error.hide();
if(this.ui.mfaToken.val()) {
Api.Tokens.loginWithMFA(this.ui.identity.val(), this.ui.secret.val(), this.ui.mfaToken.val(), true)
Api.Tokens.login(this.ui.identity.val(), this.ui.secret.val(), true)
.then(() => {
window.location = '/';
})
.catch(err => {
if (err.message === 'Invalid MFA token.') {
this.ui.error_mfa.text(err.message).show();
} else {
this.ui.error.text(err.message).show();
}
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);
});
}
}
},
@@ -66,5 +40,3 @@ module.exports = Mn.View.extend({
}
}
});

View File

@@ -215,14 +215,6 @@
"credentials": "# Gandi personal access token\ndns_gandi_token=PERSONAL_ACCESS_TOKEN",
"full_plugin_name": "dns-gandi"
},
"gcore": {
"name": "Gcore DNS",
"package_name": "certbot-dns-gcore",
"version": "~=0.1.8",
"dependencies": "",
"credentials": "dns_gcore_apitoken = 0123456789abcdef0123456789abcdef01234567",
"full_plugin_name": "dns-gcore"
},
"godaddy": {
"name": "GoDaddy",
"package_name": "certbot-dns-godaddy",