wildduck/lib/api/asps.js

445 lines
15 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 ObjectID = require('mongodb').ObjectID;
const mobileconfig = require('mobileconfig');
2017-11-06 23:32:45 +08:00
const consts = require('../consts');
2017-07-26 16:52:55 +08:00
const certs = require('../certs').get('api.mobileconfig');
module.exports = (db, server, userHandler) => {
2017-11-28 22:14:58 +08:00
/**
* @api {get} /users/:user/asps List Application Passwords
* @apiName GetASPs
* @apiGroup ApplicationPasswords
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user ID of the User
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {Object[]} results Event listing
* @apiSuccess {String} results.id ID of the Application Password
* @apiSuccess {String} results.description Description
* @apiSuccess {String[]} results.scopes Allowed scopes for the Application Password
* @apiSuccess {String} results.created Datestring
* @apiSuccess {String} results.expires Datestring if this is a temporary Application Password or <code>false</code> if not
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i "http://localhost:8080/users/59fc66a03e54454869460e45/asps"
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true,
* "results": [
* {
* "id": "5a1d6dd776e56b6d97e5dd48",
* "description": "Thunderbird",
* "scopes": [
* "imap",
* "smtp"
* ],
* "created": "2017-11-28T14:08:23.520Z",
* "expires": false
* }
* ]
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Database error"
* }
*/
2017-07-26 16:52:55 +08:00
server.get('/users/:user/asps', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
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-28 22:14:58 +08:00
db.users.collection('users').findOne(
{
_id: user
},
{
fields: {
address: true
}
},
(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-28 22:14:58 +08:00
db.users
.collection('asps')
.find({
user,
active: true,
$or: [
{
expires: false
},
{
expires: { $gt: new Date() }
}
]
})
.sort({ _id: 1 })
.toArray((err, asps) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message
});
return next();
2017-11-17 19:37:53 +08:00
}
2017-07-26 16:52:55 +08:00
2017-11-28 22:14:58 +08:00
if (!asps) {
asps = [];
}
2017-07-26 16:52:55 +08:00
2017-11-28 22:14:58 +08:00
res.json({
success: true,
2017-07-26 16:52:55 +08:00
2017-11-28 22:14:58 +08:00
results: asps.map(asp => ({
id: asp._id,
description: asp.description,
scopes: asp.scopes,
created: asp.created,
expires: asp.expires
}))
});
2017-07-26 16:52:55 +08:00
2017-11-28 22:14:58 +08:00
return next();
});
}
);
2017-07-26 16:52:55 +08:00
});
2017-11-28 22:14:58 +08:00
/**
* @api {post} /users/:user/asps Create new Application Password
* @apiName PostASP
* @apiGroup ApplicationPasswords
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user ID of the User
* @apiParam {String} description Description
* @apiParam {String[]} scopes List of scopes this Password applies to. Special scope "*" indicates that this password can be used for any scope except "master"
* @apiParam {String} [expires] Datestring if this is a temporary Application Password
* @apiParam {Boolean} [generateMobileconfig] If true then result contains a mobileconfig formatted file with account config
* @apiParam {String} [sess] Session identifier for the logs
* @apiParam {String} [ip] IP address for the logs
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Application Password
* @apiSuccess {String} password Application Specific Password. Generated password is whitespace agnostic, so it could be displayed to the client as "abcd efgh ijkl mnop" instead of "abcdefghijklmnop"
* @apiSuccess {String} mobileconfig Base64 encoded mobileconfig file. Generated profile file should be sent to the client with <code>Content-Type</code> value of <code>application/x-apple-aspen-config</code>.
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XPOST http://localhost:8080/users/59fc66a03e54454869460e45/asps \
* -H 'Content-type: application/json' \
* -d '{
* "description": "Thunderbird",
* "scopes": ["imap", "smtp"],
* "generateMobileconfig": true
* }'
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true,
* "id": "5a1d6dd776e56b6d97e5dd48",
* "password": "rflhmllyegblyybd",
* "mobileconfig": "MIIQBgYJKoZIhvcNAQcCoIIP9..."
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Database error"
* }
*/
2017-07-26 16:52:55 +08:00
server.post('/users/:user/asps', (req, res, next) => {
res.charSet('utf-8');
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()
2017-11-06 23:32:45 +08:00
.valid(...consts.SCOPES, '*')
.required()
)
.unique(),
generateMobileconfig: Joi.boolean()
.truthy(['Y', 'true', 'yes', 1])
.default(false),
2017-11-17 19:44:03 +08:00
expires: Joi.date(),
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'
})
});
if (typeof req.params.scopes === 'string') {
req.params.scopes = req.params.scopes
.split(',')
.map(scope => scope.trim())
.filter(scope => scope);
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);
let generateMobileconfig = result.value.generateMobileconfig;
let scopes = result.value.scopes || ['*'];
2017-08-01 03:28:06 +08:00
let description = result.value.description;
2017-07-26 16:52:55 +08:00
2017-07-30 03:08:43 +08:00
if (scopes.includes('*')) {
scopes = ['*'];
}
2017-07-26 16:52:55 +08:00
if (generateMobileconfig && !scopes.includes('*') && (!scopes.includes('imap') || !scopes.includes('smtp'))) {
res.json({
error: 'Profile file requires imap and smtp scopes'
});
return next();
}
2017-11-28 22:14:58 +08:00
db.users.collection('users').findOne(
{
_id: user
},
{
fields: {
username: true,
name: true,
address: true
}
},
(err, userData) => {
2017-07-26 16:52:55 +08:00
if (err) {
res.json({
2017-11-28 22:14:58 +08:00
error: 'MongoDB Error: ' + err.message
2017-07-26 16:52:55 +08:00
});
return next();
}
2017-11-28 22:14:58 +08:00
if (!userData) {
2017-07-26 16:52:55 +08:00
res.json({
2017-11-28 22:14:58 +08:00
error: 'This user does not exist'
2017-07-26 16:52:55 +08:00
});
return next();
}
2017-11-28 22:14:58 +08:00
userHandler.generateASP(user, result.value, (err, result) => {
2017-07-26 16:52:55 +08:00
if (err) {
res.json({
error: err.message
});
return next();
}
2017-11-28 22:14:58 +08:00
if (!generateMobileconfig) {
res.json({
success: true,
id: result.id,
password: result.password
});
return next();
}
let profileOpts = {};
Object.keys(config.api.mobileconfig || {}).forEach(key => {
profileOpts[key] = (config.api.mobileconfig[key] || '')
.toString()
.replace(/\{email\}/g, userData.address)
.trim();
});
let options = {
displayName: description || profileOpts.displayName,
displayDescription: profileOpts.displayDescription,
accountDescription: profileOpts.accountDescription,
emailAddress: userData.address,
emailAccountName: userData.name,
identifier: profileOpts.identifier + '.' + userData.username,
imap: {
hostname: config.imap.setup.hostname,
port: config.imap.setup.port || config.imap.port,
secure: config.imap.setup.secure,
username: userData.username,
password: result.password
},
smtp: {
hostname: config.smtp.setup.hostname,
port: config.smtp.setup.port || config.smtp.port,
secure: true, //config.setup.smtp.secure,
username: userData.username,
password: false // use the same password as for IMAP
},
keys: certs
};
mobileconfig.getSignedEmailConfig(options, (err, data) => {
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success: true,
id: result.id,
password: result.password,
mobileconfig: data.toString('base64')
});
return next();
2017-07-26 16:52:55 +08:00
});
});
2017-11-28 22:14:58 +08:00
}
);
2017-07-26 16:52:55 +08:00
});
2017-11-28 22:14:58 +08:00
/**
* @api {delete} /users/:user/asps/:asp Delete an Application Password
* @apiName DeleteASP
* @apiGroup ApplicationPasswords
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
* @apiHeaderExample {json} Header-Example:
* {
* "X-Access-Token": "59fc66a03e54454869460e45"
* }
*
* @apiParam {String} user ID of the User
* @apiParam {String} asp ID of the Application Password
*
* @apiSuccess {Boolean} success Indicates successful response
*
* @apiError error Description of the error
*
* @apiExample {curl} Example usage:
* curl -i -XDELETE "http://localhost:8080/users/59fc66a03e54454869460e45/asps/5a1d6dd776e56b6d97e5dd48"
*
* @apiSuccessExample {json} Success-Response:
* HTTP/1.1 200 OK
* {
* "success": true
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Database error"
* }
*/
2017-07-26 16:52:55 +08:00
server.del('/users/:user/asps/:asp', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
asp: Joi.string()
.hex()
.lowercase()
.length(24)
.required(),
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);
let asp = new ObjectID(result.value.asp);
userHandler.deleteASP(user, asp, result.value, err => {
if (err) {
res.json({
error: err.message
});
return next();
}
res.json({
success: true
});
return next();
});
});
};