wildduck/lib/api/users.js

940 lines
28 KiB
JavaScript
Raw Normal View History

2017-07-26 16:52:55 +08:00
'use strict';
const config = require('wild-config');
const Joi = require('joi');
const MongoPaging = require('mongo-cursor-pagination');
const ObjectID = require('mongodb').ObjectID;
const tools = require('../tools');
2017-10-03 18:09:16 +08:00
const errors = require('../errors');
const openpgp = require('openpgp');
2017-08-09 21:45:52 +08:00
const addressparser = require('addressparser');
const libmime = require('libmime');
2017-07-26 16:52:55 +08:00
module.exports = (db, server, userHandler) => {
server.get({ name: 'users', path: '/users' }, (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
query: Joi.string()
2017-09-04 19:49:04 +08:00
.empty('')
2017-09-01 19:50:53 +08:00
.lowercase()
2017-09-04 19:49:04 +08:00
.max(128),
2017-11-03 20:11:59 +08:00
tags: Joi.string()
.trim()
.empty('')
.max(1024),
requiredTags: Joi.string()
.trim()
.empty('')
.max(1024),
2017-09-01 19:50:53 +08:00
limit: Joi.number()
.default(20)
.min(1)
.max(250),
next: Joi.string()
2017-09-04 19:49:04 +08:00
.empty('')
2017-09-01 19:50:53 +08:00
.alphanum()
2017-09-04 19:49:04 +08:00
.max(1024),
2017-09-01 19:50:53 +08:00
previous: Joi.string()
2017-09-04 19:49:04 +08:00
.empty('')
2017-09-01 19:50:53 +08:00
.alphanum()
2017-09-04 19:49:04 +08:00
.max(1024),
2017-07-26 16:52:55 +08:00
page: Joi.number().default(1)
});
const result = Joi.validate(req.query, schema, {
abortEarly: false,
convert: true,
allowUnknown: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let query = result.value.query;
let limit = result.value.limit;
let page = result.value.page;
let pageNext = result.value.next;
2017-09-04 20:04:43 +08:00
let pagePrevious = result.value.previous;
2017-07-26 16:52:55 +08:00
let filter = query
? {
2017-11-03 20:11:59 +08:00
$or: [
{
address: {
$regex: query.replace(/\./g, ''),
$options: ''
}
},
{
unameview: {
$regex: query.replace(/\./g, ''),
$options: ''
}
}
]
2017-07-26 16:52:55 +08:00
}
: {};
2017-11-03 20:11:59 +08:00
let tagSeen = new Set();
let requiredTags = (result.value.requiredTags || '')
.split(',')
.map(tag => tag.toLowerCase().trim())
.filter(tag => {
if (tag && !tagSeen.has(tag)) {
tagSeen.add(tag);
return true;
}
return false;
});
let tags = (result.value.tags || '')
.split(',')
.map(tag => tag.toLowerCase().trim())
.filter(tag => {
if (tag && !tagSeen.has(tag)) {
tagSeen.add(tag);
return true;
}
return false;
});
let tagsview = {};
if (requiredTags.length) {
tagsview.$all = requiredTags;
}
if (tags.length) {
tagsview.$in = tags;
}
if (requiredTags.length || tags.length) {
filter.tagsview = tagsview;
2017-10-24 20:25:26 +08:00
}
2017-07-26 16:52:55 +08:00
db.users.collection('users').count(filter, (err, total) => {
if (err) {
res.json({
error: err.message
});
return next();
}
let opts = {
limit,
query: filter,
fields: {
_id: true,
username: true,
name: true,
address: true,
2017-11-03 20:11:59 +08:00
tags: true,
2017-07-26 16:52:55 +08:00
storageUsed: true,
2017-10-24 19:30:33 +08:00
forward: true,
targetUrl: true,
2017-07-26 16:52:55 +08:00
quota: true,
2017-11-03 20:11:59 +08:00
activated: true,
2017-10-27 20:45:51 +08:00
disabled: true,
2017-11-03 20:11:59 +08:00
password: true,
2017-10-30 19:41:53 +08:00
encryptMessages: true,
encryptForwarded: true
2017-07-26 16:52:55 +08:00
},
sortAscending: true
};
if (pageNext) {
opts.next = pageNext;
2017-09-04 20:04:43 +08:00
} else if (pagePrevious) {
opts.previous = pagePrevious;
2017-07-26 16:52:55 +08:00
}
MongoPaging.find(db.users.collection('users'), opts, (err, result) => {
if (err) {
res.json({
error: err.message
});
return next();
}
if (!result.hasPrevious) {
page = 1;
}
let response = {
success: true,
query,
total,
page,
2017-09-01 19:50:53 +08:00
previousCursor: result.hasPrevious ? result.previous : false,
nextCursor: result.hasNext ? result.next : false,
2017-07-26 16:52:55 +08:00
results: (result.results || []).map(userData => ({
id: userData._id.toString(),
username: userData.username,
name: userData.name,
address: userData.address,
2017-11-03 20:11:59 +08:00
tags: userData.tags || [],
2017-11-13 21:27:37 +08:00
forward: [].concat(userData.forward || []),
2017-10-24 19:30:33 +08:00
targetUrl: userData.targetUrl,
2017-10-27 20:45:51 +08:00
encryptMessages: !!userData.encryptMessages,
2017-10-30 19:41:53 +08:00
encryptForwarded: !!userData.encryptForwarded,
2017-07-26 16:52:55 +08:00
quota: {
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
used: Math.max(Number(userData.storageUsed) || 0, 0)
},
2017-11-03 20:11:59 +08:00
hasPasswordSet: !!userData.password,
activated: userData.activated,
2017-07-26 16:52:55 +08:00
disabled: userData.disabled
}))
};
res.json(response);
return next();
});
});
});
server.post('/users', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
username: Joi.string()
.lowercase()
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
2017-10-24 19:30:33 +08:00
.min(1)
.max(32)
2017-09-01 19:50:53 +08:00
.required(),
password: Joi.string()
2017-10-24 19:30:33 +08:00
.allow(false)
2017-09-01 19:50:53 +08:00
.max(256)
.required(),
2017-07-26 16:52:55 +08:00
address: Joi.string().email(),
emptyAddress: Joi.boolean()
.truthy(['Y', 'true', 'yes', 1])
.default(false),
2017-07-26 16:52:55 +08:00
2017-09-01 19:50:53 +08:00
language: Joi.string()
.min(2)
.max(20)
.lowercase(),
retention: Joi.number()
.min(0)
.default(0),
2017-07-26 16:52:55 +08:00
name: Joi.string().max(256),
2017-11-13 21:27:37 +08:00
forward: Joi.array().items(Joi.string().email()),
2017-07-26 16:52:55 +08:00
targetUrl: Joi.string().max(256),
2017-09-01 19:50:53 +08:00
quota: Joi.number()
.min(0)
.default(0),
recipients: Joi.number()
.min(0)
.default(0),
forwards: Joi.number()
.min(0)
.default(0),
2017-11-03 20:11:59 +08:00
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
),
2017-09-01 19:50:53 +08:00
pubKey: Joi.string()
.empty('')
.trim()
.regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
encryptMessages: Joi.boolean()
.truthy(['Y', 'true', 'yes', 1])
.default(false),
2017-10-30 19:41:53 +08:00
encryptForwarded: Joi.boolean()
.truthy(['Y', 'true', 'yes', 1])
.default(false),
sess: Joi.string().max(255),
2017-07-26 16:52:55 +08:00
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
2017-11-13 21:27:37 +08:00
if (result.value.forward) {
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
2017-07-26 16:52:55 +08:00
}
if ('pubKey' in req.params && !result.value.pubKey) {
result.value.pubKey = '';
}
2017-11-03 20:11:59 +08:00
if (result.value.tags) {
let tagSeen = new Set();
let tags = result.value.tags
.map(tag => tag.trim())
.filter(tag => {
if (tag && !tagSeen.has(tag.toLowerCase())) {
tagSeen.add(tag.toLowerCase());
return true;
}
return false;
})
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
result.value.tags = tags;
result.value.tagsview = tags.map(tag => tag.toLowerCase());
}
checkPubKey(result.value.pubKey, err => {
2017-07-26 16:52:55 +08:00
if (err) {
res.json({
error: 'PGP key validation failed. ' + err.message
2017-07-26 16:52:55 +08:00
});
return next();
}
2017-08-09 21:45:52 +08:00
userHandler.create(result.value, (err, id) => {
if (err) {
res.json({
error: err.message,
username: result.value.username
});
return next();
}
2017-07-26 16:52:55 +08:00
res.json({
success: !!id,
id
});
2017-07-26 16:52:55 +08:00
return next();
});
2017-07-26 16:52:55 +08:00
});
});
server.get('/users/:user', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required()
2017-07-26 16:52:55 +08:00
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
2017-11-23 17:51:37 +08:00
db.users.collection('users').findOne(
{
_id: user
},
(err, userData) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message
});
return next();
}
if (!userData) {
res.json({
error: 'This user does not exist'
});
return next();
}
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
db.redis
.multi()
// sending counters are stored in Redis
.get('wdr:' + userData._id.toString())
.ttl('wdr:' + userData._id.toString())
.get('wdf:' + userData._id.toString())
.ttl('wdf:' + userData._id.toString())
.exec((err, result) => {
if (err) {
// ignore
errors.notify(err, { userId: user });
}
2017-10-03 18:09:16 +08:00
2017-11-23 17:51:37 +08:00
let recipients = Number(userData.recipients) || config.maxRecipients;
let forwards = Number(userData.forwards) || config.maxForwards;
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
let recipientsSent = Number(result && result[0] && result[0][1]) || 0;
let recipientsTtl = Number(result && result[1] && result[1][1]) || 0;
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
let forwardsSent = Number(result && result[2] && result[2][1]) || 0;
let forwardsTtl = Number(result && result[3] && result[3][1]) || 0;
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
res.json({
success: true,
id: user,
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
username: userData.username,
name: userData.name,
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
address: userData.address,
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
language: userData.language,
retention: userData.retention || false,
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
enabled2fa: Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []),
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
encryptMessages: userData.encryptMessages,
encryptForwarded: userData.encryptForwarded,
pubKey: userData.pubKey,
keyInfo: getKeyInfo(userData.pubKey),
2017-11-23 17:51:37 +08:00
forward: [].concat(userData.forward || []),
targetUrl: userData.targetUrl,
2017-07-30 03:08:43 +08:00
2017-11-23 17:51:37 +08:00
limits: {
quota: {
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
used: Math.max(Number(userData.storageUsed) || 0, 0)
},
recipients: {
allowed: recipients,
used: recipientsSent,
ttl: recipientsTtl >= 0 ? recipientsTtl : false
},
forwards: {
allowed: forwards,
used: forwardsSent,
ttl: forwardsTtl >= 0 ? forwardsTtl : false
}
2017-07-26 16:52:55 +08:00
},
2017-11-23 17:51:37 +08:00
tags: userData.tags || [],
hasPasswordSet: !!userData.password,
activated: userData.activated,
disabled: userData.disabled
});
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
return next();
});
}
);
2017-07-26 16:52:55 +08:00
});
server.put('/users/:user', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
existingPassword: Joi.string()
.empty('')
.min(1)
.max(256),
password: Joi.string()
.min(8)
.max(256),
language: Joi.string()
.min(2)
.max(20)
.lowercase(),
name: Joi.string()
.empty('')
.max(256),
2017-11-13 21:27:37 +08:00
forward: Joi.array().items(Joi.string().email()),
2017-09-01 19:50:53 +08:00
targetUrl: Joi.string()
.empty('')
.max(256),
pubKey: Joi.string()
.empty('')
.trim()
.regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
encryptMessages: Joi.boolean()
.empty('')
.truthy(['Y', 'true', 'yes', 1]),
2017-10-30 19:41:53 +08:00
encryptForwarded: Joi.boolean()
.empty('')
.truthy(['Y', 'true', 'yes', 1]),
2017-07-26 16:52:55 +08:00
retention: Joi.number().min(0),
quota: Joi.number().min(0),
recipients: Joi.number().min(0),
forwards: Joi.number().min(0),
2017-11-03 20:11:59 +08:00
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
),
2017-10-24 20:17:36 +08:00
disabled: Joi.boolean()
.empty('')
.truthy(['Y', 'true', 'yes', 1]),
2017-10-30 19:41:53 +08:00
sess: Joi.string().max(255),
2017-07-26 16:52:55 +08:00
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
2017-11-13 21:27:37 +08:00
if (result.value.forward) {
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
2017-07-30 03:08:43 +08:00
}
if (!result.value.targetUrl && 'targetUrl' in req.params) {
result.value.targetUrl = '';
}
if (!result.value.name && 'name' in req.params) {
result.value.name = '';
2017-07-26 16:52:55 +08:00
}
if (!result.value.pubKey && 'pubKey' in req.params) {
result.value.pubKey = '';
}
2017-11-03 20:11:59 +08:00
if (result.value.tags) {
let tagSeen = new Set();
let tags = result.value.tags
.map(tag => tag.trim())
.filter(tag => {
if (tag && !tagSeen.has(tag.toLowerCase())) {
tagSeen.add(tag.toLowerCase());
return true;
}
return false;
})
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
result.value.tags = tags;
result.value.tagsview = tags.map(tag => tag.toLowerCase());
}
checkPubKey(result.value.pubKey, err => {
2017-07-26 16:52:55 +08:00
if (err) {
res.json({
error: 'PGP key validation failed. ' + err.message
2017-07-26 16:52:55 +08:00
});
return next();
}
userHandler.update(user, result.value, (err, success) => {
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success
});
return next();
2017-07-26 16:52:55 +08:00
});
});
});
server.put('/users/:user/logout', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
reason: Joi.string()
.empty('')
.max(128),
2017-10-30 19:41:53 +08:00
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
userHandler.logout(result.value.user, result.value.reason || 'Logout requested from API', (err, success) => {
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success
});
return next();
});
});
2017-07-26 16:52:55 +08:00
server.post('/users/:user/quota/reset', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
user: Joi.string()
.hex()
.lowercase()
.length(24)
2017-11-03 20:11:59 +08:00
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
2017-07-26 16:52:55 +08:00
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
db.users.collection('users').findOne(
{
_id: user
},
{
fields: {
storageUsed: true
}
},
(err, userData) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.messageusername
});
return next();
}
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
if (!userData) {
res.json({
error: 'This user does not exist'
});
return next();
}
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
// calculate mailbox size by aggregating the size's of all messages
// NB! Scattered query
db.database
.collection('messages')
.aggregate(
[
{
$match: {
user
}
},
{
$group: {
_id: {
user: '$user'
},
storageUsed: {
$sum: '$size'
}
}
2017-07-26 16:52:55 +08:00
}
2017-11-23 17:51:37 +08:00
],
2017-07-26 16:52:55 +08:00
{
2017-11-23 17:51:37 +08:00
cursor: {
batchSize: 1
2017-07-26 16:52:55 +08:00
}
}
2017-11-23 17:51:37 +08:00
)
.toArray((err, result) => {
2017-07-26 16:52:55 +08:00
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message
});
return next();
}
2017-11-23 17:51:37 +08:00
let storageUsed = (result && result[0] && result[0].storageUsed) || 0;
2017-07-26 16:52:55 +08:00
2017-11-23 17:51:37 +08:00
// update quota counter
db.users.collection('users').findOneAndUpdate(
{
_id: userData._id
},
{
$set: {
storageUsed: Number(storageUsed) || 0
}
},
{
returnOriginal: false
},
(err, result) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message
});
return next();
}
if (!result || !result.value) {
res.json({
error: 'This user does not exist'
});
return next();
}
res.json({
success: true,
storageUsed: Number(result.value.storageUsed) || 0
});
return next();
}
);
2017-07-26 16:52:55 +08:00
});
2017-11-23 17:51:37 +08:00
}
);
2017-07-26 16:52:55 +08:00
});
server.post('/users/:user/password/reset', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
2017-09-01 19:50:53 +08:00
user: Joi.string()
.hex()
.lowercase()
.length(24)
2017-11-03 20:11:59 +08:00
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
2017-11-03 20:11:59 +08:00
userHandler.reset(user, result.value, (err, password) => {
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success: true,
password
});
return next();
});
});
2017-11-17 19:37:53 +08:00
server.del('/users/:user', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
2017-11-23 17:51:37 +08:00
.required(),
sess: Joi.string().max(255),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
2017-11-17 19:37:53 +08:00
});
2017-11-23 17:51:37 +08:00
if (req.query.sess) {
req.params.sess = req.query.sess;
}
if (req.query.ip) {
req.params.ip = req.query.ip;
}
2017-11-17 19:37:53 +08:00
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
2017-11-23 17:51:37 +08:00
userHandler.delete(user, {}, (err, status) => {
2017-11-17 19:37:53 +08:00
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success: status
});
return next();
});
});
2017-07-26 16:52:55 +08:00
};
2017-08-09 21:45:52 +08:00
function getKeyInfo(pubKey) {
if (!pubKey) {
return false;
}
// try to encrypt something with that key
let armored;
try {
armored = openpgp.key.readArmored(pubKey).keys;
} catch (E) {
return false;
}
if (!armored || !armored[0]) {
return false;
}
let fingerprint = armored[0].primaryKey.fingerprint;
let name, address;
if (armored && armored[0] && armored[0].users && armored[0].users[0] && armored[0].users[0].userId) {
let user = addressparser(armored[0].users[0].userId.userid);
if (user && user[0] && user[0].address) {
address = tools.normalizeAddress(user[0].address);
try {
name = libmime.decodeWords(user[0].name || '').trim();
} catch (E) {
// failed to parse value
name = user[0].name || '';
}
}
}
return {
name,
address,
fingerprint
};
}
function checkPubKey(pubKey, done) {
if (!pubKey) {
return done();
}
// try to encrypt something with that key
let armored;
try {
armored = openpgp.key.readArmored(pubKey).keys;
} catch (E) {
return done(E);
}
2017-08-09 21:45:52 +08:00
if (!armored || !armored[0]) {
return done(new Error('Did not find key information'));
}
let fingerprint = armored[0].primaryKey.fingerprint;
let name, address;
if (armored && armored[0] && armored[0].users && armored[0].users[0] && armored[0].users[0].userId) {
let user = addressparser(armored[0].users[0].userId.userid);
if (user && user[0] && user[0].address) {
address = tools.normalizeAddress(user[0].address);
try {
name = libmime.decodeWords(user[0].name || '').trim();
} catch (E) {
// failed to parse value
name = user[0].name || '';
}
}
}
openpgp
.encrypt({
data: 'Hello, World!',
publicKeys: armored
})
.then(ciphertext => {
if (/^-----BEGIN PGP MESSAGE/.test(ciphertext.data)) {
// everything checks out
2017-08-09 21:45:52 +08:00
return done(null, {
address,
name,
fingerprint
});
}
return done(new Error('Unexpected message'));
})
.catch(err => done(err));
}