'use strict';
const config = require('wild-config');
const Joi = require('joi');
const ObjectID = require('mongodb').ObjectID;
const mobileconfig = require('mobileconfig');
const consts = require('../consts');
const certs = require('../certs').get('api.mobileconfig');
module.exports = (db, server, userHandler) => {
/**
* @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.used Datestring of last use or false if password has not been used
* @apiSuccess {String} results.created Datestring
*
* @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"
* ],
* "used": false,
* "created": "2017-11-28T14:08:23.520Z"
* }
* ]
* }
*
* @apiErrorExample {json} Error-Response:
* HTTP/1.1 200 OK
* {
* "error": "Database error"
* }
*/
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(),
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,
code: 'InputValidationError'
});
return next();
}
let user = new ObjectID(result.value.user);
db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
address: true
}
},
(err, userData) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!userData) {
res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
return next();
}
db.users
.collection('asps')
.find({
user
})
.sort({ _id: 1 })
.toArray((err, asps) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!asps) {
asps = [];
}
res.json({
success: true,
results: asps.map(asp => ({
id: asp._id,
description: asp.description,
scopes: asp.scopes,
lastUse: {
time: asp.used || false,
event: asp.authEvent || false
},
created: asp.created
}))
});
return next();
});
}
);
});
/**
* @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 {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 Content-Type
value of application/x-apple-aspen-config
.
*
* @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"
* }
*/
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()
.valid(...consts.SCOPES, '*')
.required()
)
.unique(),
generateMobileconfig: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])
.falsy(['N', 'false', 'no', 'off', 0, ''])
.default(false),
sess: Joi.string().max(255),
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);
}
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message,
code: 'InputValidationError'
});
return next();
}
let user = new ObjectID(result.value.user);
let generateMobileconfig = result.value.generateMobileconfig;
let scopes = result.value.scopes || ['*'];
let description = result.value.description;
if (scopes.includes('*')) {
scopes = ['*'];
}
if (generateMobileconfig && !scopes.includes('*') && (!scopes.includes('imap') || !scopes.includes('smtp'))) {
res.json({
error: 'Profile file requires imap and smtp scopes'
});
return next();
}
db.users.collection('users').findOne(
{
_id: user
},
{
projection: {
username: true,
name: true,
address: true
}
},
(err, userData) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (!userData) {
res.json({
error: 'This user does not exist',
code: 'UserNotFound'
});
return next();
}
userHandler.generateASP(user, result.value, (err, result) => {
if (err) {
res.json({
error: err.message
});
return next();
}
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();
});
});
}
);
});
/**
* @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"
* }
*/
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(),
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,
code: 'InputValidationError'
});
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();
});
});
};