mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-09 14:46:55 +08:00
updates
This commit is contained in:
parent
9c6e378edb
commit
534e917b0f
3 changed files with 137 additions and 33 deletions
106
api.js
106
api.js
|
@ -300,6 +300,10 @@ server.post('/users', (req, res, next) => {
|
|||
language: Joi.string().min(2).max(20).lowercase(),
|
||||
retention: Joi.number().min(0).default(0),
|
||||
|
||||
name: Joi.string().max(256),
|
||||
forward: Joi.string().email(),
|
||||
targetUrl: Joi.string().max(256),
|
||||
|
||||
quota: Joi.number().min(0).default(0),
|
||||
recipients: Joi.number().min(0).default(0),
|
||||
forwards: Joi.number().min(0).default(0),
|
||||
|
@ -310,6 +314,13 @@ server.post('/users', (req, res, next) => {
|
|||
})
|
||||
});
|
||||
|
||||
let forward = req.params.forward ? tools.normalizeAddress(req.params.forward) : false;
|
||||
|
||||
if (forward && /[\u0080-\uFFFF]/.test(forward)) {
|
||||
// replace unicode characters in email addresses before validation
|
||||
req.params.forward = forward.replace(/[\u0080-\uFFFF]/g, 'x');
|
||||
}
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
|
@ -322,6 +333,10 @@ server.post('/users', (req, res, next) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
result.value.forward = forward;
|
||||
}
|
||||
|
||||
userHandler.create(result.value, (err, id) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
|
@ -390,7 +405,7 @@ server.post('/authenticate', (req, res, next) => {
|
|||
|
||||
res.json({
|
||||
success: true,
|
||||
id: authData._id,
|
||||
id: authData.user,
|
||||
username: authData.username,
|
||||
scope: authData.scope,
|
||||
require2fa: authData.require2fa
|
||||
|
@ -414,7 +429,6 @@ server.put('/users/:user', (req, res, next) => {
|
|||
name: Joi.string().max(256),
|
||||
forward: Joi.string().email(),
|
||||
targetUrl: Joi.string().max(256),
|
||||
autoreply: Joi.string().max(256),
|
||||
|
||||
retention: Joi.number().min(0),
|
||||
quota: Joi.number().min(0),
|
||||
|
@ -955,6 +969,8 @@ server.get('/users/:user', (req, res, next) => {
|
|||
language: userData.language,
|
||||
retention: userData.retention || false,
|
||||
|
||||
enabled2fa: userData.enabled2fa,
|
||||
|
||||
limits: {
|
||||
quota: {
|
||||
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
|
||||
|
@ -3162,7 +3178,11 @@ server.post('/users/:user/asps', (req, res, next) => {
|
|||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
description: Joi.string().trim().max(255).required(),
|
||||
scopes: Joi.array().items(Joi.string().valid('imap', 'pop3', 'smtp', '*').required()).unique()
|
||||
scopes: Joi.array().items(Joi.string().valid('imap', 'pop3', 'smtp', '*').required()).unique(),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
})
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
@ -3178,10 +3198,8 @@ server.post('/users/:user/asps', (req, res, next) => {
|
|||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
let description = result.value.description;
|
||||
let scopes = result.value.scopes;
|
||||
|
||||
userHandler.generateASP(user, description, scopes, (err, result) => {
|
||||
userHandler.generateASP(user, result.value, (err, result) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
|
@ -3202,7 +3220,11 @@ server.del('/users/:user/asps/:asp', (req, res, next) => {
|
|||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
asp: Joi.string().hex().lowercase().length(24).required()
|
||||
asp: Joi.string().hex().lowercase().length(24).required(),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
})
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
|
@ -3220,24 +3242,13 @@ server.del('/users/:user/asps/:asp', (req, res, next) => {
|
|||
let user = new ObjectID(result.value.user);
|
||||
let asp = new ObjectID(result.value.asp);
|
||||
|
||||
db.users.collection('asps').deleteOne({
|
||||
_id: asp,
|
||||
user
|
||||
}, (err, r) => {
|
||||
userHandler.deleteASP(user, asp, result.value, err => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message
|
||||
error: err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!r.deletedCount) {
|
||||
res.json({
|
||||
error: 'Application Specific Password was not found'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
|
@ -3288,6 +3299,57 @@ server.post('/users/:user/2fa', (req, res, next) => {
|
|||
});
|
||||
});
|
||||
|
||||
server.get('/users/:user/2fa', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
token: Joi.string().length(6).required(),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
})
|
||||
});
|
||||
|
||||
req.query.user = req.params.user;
|
||||
|
||||
const result = Joi.validate(req.query, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
|
||||
userHandler.check2fa(user, result.value, (err, result) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: err.message
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
res.json({
|
||||
error: 'Invalid authentication token'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
server.put('/users/:user/2fa', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
|
@ -3348,7 +3410,9 @@ server.del('/users/:user/2fa', (req, res, next) => {
|
|||
})
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
req.query.user = req.params.user;
|
||||
|
||||
const result = Joi.validate(req.query, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ indexes:
|
|||
name: user
|
||||
key:
|
||||
user: 1
|
||||
created: -1
|
||||
_id: -1
|
||||
- collection: authlog
|
||||
type: users # index applies to users database
|
||||
index:
|
||||
|
|
|
@ -215,7 +215,7 @@ class UserHandler {
|
|||
});
|
||||
}
|
||||
|
||||
generateASP(user, description, scopes, callback) {
|
||||
generateASP(user, data, callback) {
|
||||
let password = generatePassword.generate({
|
||||
length: 16,
|
||||
uppercase: false,
|
||||
|
@ -226,8 +226,9 @@ class UserHandler {
|
|||
let allowedScopes = ['imap', 'pop3', 'smtp'];
|
||||
let hasAllScopes = false;
|
||||
let scopeSet = new Set();
|
||||
let scopes = [].concat(data.scopes || []);
|
||||
|
||||
(scopes || []).forEach(scope => {
|
||||
scopes.forEach(scope => {
|
||||
scope = scope.toLowerCase().trim();
|
||||
if (scope === '*') {
|
||||
hasAllScopes = true;
|
||||
|
@ -244,7 +245,7 @@ class UserHandler {
|
|||
let passwordData = {
|
||||
id: new ObjectID(),
|
||||
user,
|
||||
description,
|
||||
description: data.description,
|
||||
scopes,
|
||||
password: bcrypt.hashSync(password, 11),
|
||||
created: new Date()
|
||||
|
@ -270,14 +271,50 @@ class UserHandler {
|
|||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, {
|
||||
id: passwordData._id,
|
||||
password
|
||||
});
|
||||
return this.logAuthEvent(
|
||||
user,
|
||||
{
|
||||
action: 'create asp',
|
||||
asp: passwordData._id,
|
||||
result: 'success',
|
||||
ip: data.ip
|
||||
},
|
||||
() =>
|
||||
callback(null, {
|
||||
id: passwordData._id,
|
||||
password
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteASP(user, asp, data, callback) {
|
||||
this.users.collection('asps').deleteOne({
|
||||
_id: asp,
|
||||
user
|
||||
}, (err, r) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!r.deletedCount) {
|
||||
return callback(new Error('Application Specific Password was not found'));
|
||||
}
|
||||
|
||||
return this.logAuthEvent(
|
||||
user,
|
||||
{
|
||||
action: 'delete asp',
|
||||
asp,
|
||||
result: 'success',
|
||||
ip: data.ip
|
||||
},
|
||||
() => callback(null, true)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
create(data, callback) {
|
||||
this.users.collection('users').findOne({
|
||||
username: data.username
|
||||
|
@ -601,7 +638,8 @@ class UserHandler {
|
|||
|
||||
disable2fa(user, data, callback) {
|
||||
return this.users.collection('users').findOneAndUpdate({
|
||||
_id: user
|
||||
_id: user,
|
||||
enabled2fa: true
|
||||
}, {
|
||||
$set: {
|
||||
enabled2fa: false,
|
||||
|
@ -634,6 +672,7 @@ class UserHandler {
|
|||
}, {
|
||||
fields: {
|
||||
username: true,
|
||||
enabled2fa: true,
|
||||
seed: true
|
||||
}
|
||||
}, (err, userData) => {
|
||||
|
@ -642,13 +681,14 @@ class UserHandler {
|
|||
return callback(new Error('Database Error, failed to find user'));
|
||||
}
|
||||
if (!userData) {
|
||||
let err = new Error('This username does not exist');
|
||||
let err = new Error('This user does not exist');
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!userData.seed) {
|
||||
if (!userData.seed || !userData.enabled2fa) {
|
||||
// 2fa not set up
|
||||
return callback(null, true);
|
||||
let err = new Error('2FA is not enabled for this user');
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let verified = speakeasy.totp.verify({
|
||||
|
|
Loading…
Add table
Reference in a new issue