started with metaData for addresses

This commit is contained in:
Andris Reinman 2020-09-25 16:49:38 +03:00
parent 89dad94ae3
commit f2f14ca8fc
4 changed files with 231 additions and 33 deletions

View file

@ -243,13 +243,13 @@
"user": {
"addresslisting": {
"read:own": ["*"]
"read:own": ["*", "!internalData"]
},
"addresses": {
"create:own": ["*"],
"read:own": ["*"],
"update:own": ["*"],
"create:own": ["*", "!internalData"],
"read:own": ["*", "!internalData"],
"update:own": ["*", "!internalData"],
"delete:own": ["*"]
},

View file

@ -25,6 +25,8 @@ module.exports = (db, server, userHandler) => {
* @apiParam {String} [query] Partial match of an address
* @apiParam {String} [tags] Comma separated list of tags. The Address must have at least one to be set
* @apiParam {String} [requiredTags] Comma separated list of tags. The Address must have all listed tags to be set
* @apiParam {Boolean} [metaData] If true, then includes <code>metaData</code> in the response
* @apiParam {Boolean} [internalData] If true, then includes <code>internalData</code> in the response. Not shown for user-role tokens.
* @apiParam {Number} [limit=20] How many records to return
* @apiParam {Number} [page=1] Current page number. Informational only, page numbers start from 1
* @apiParam {Number} [next] Cursor value for next page, retrieved from <code>nextCursor</code> response value
@ -86,6 +88,8 @@ module.exports = (db, server, userHandler) => {
query: Joi.string().trim().empty('').max(255),
tags: Joi.string().trim().empty('').max(1024),
requiredTags: Joi.string().trim().empty('').max(1024),
metaData: booleanSchema,
internalData: booleanSchema,
limit: Joi.number().default(20).min(1).max(250),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
@ -202,6 +206,14 @@ module.exports = (db, server, userHandler) => {
sortAscending: true
};
if (result.value.metaData) {
opts.fields.projection.metaData = true;
}
if (result.value.internalData) {
opts.fields.projection.internalData = true;
}
if (pageNext) {
opts.next = pageNext;
} else if ((!page || page > 1) && pagePrevious) {
@ -230,16 +242,28 @@ module.exports = (db, server, userHandler) => {
page,
previousCursor: listing.hasPrevious ? listing.previous : false,
nextCursor: listing.hasNext ? listing.next : false,
results: (listing.results || []).map(addressData => ({
id: addressData._id.toString(),
name: addressData.name || false,
address: addressData.address,
user: addressData.user,
forwarded: !!addressData.targets,
forwardedDisabled: !!(addressData.targets && addressData.forwardedDisabled),
targets: addressData.targets && addressData.targets.map(t => t.value),
tags: addressData.tags || []
}))
results: (listing.results || []).map(addressData => {
let values = {
id: addressData._id.toString(),
name: addressData.name || false,
address: addressData.address,
user: addressData.user,
forwarded: !!addressData.targets,
forwardedDisabled: !!(addressData.targets && addressData.forwardedDisabled),
targets: addressData.targets && addressData.targets.map(t => t.value),
tags: addressData.tags || []
};
if (addressData.metaData) {
values.metaData = formatMetaData(addressData.metaData);
}
if (addressData.internalData) {
values.internalData = formatMetaData(addressData.internalData);
}
return permission.filter(values);
})
};
res.json(response);
@ -268,6 +292,8 @@ module.exports = (db, server, userHandler) => {
* @apiParam {String[]} [tags] A list of tags associated with this address
* @apiParam {Boolean} [main=false] Indicates if this is the default address for the User
* @apiParam {Boolean} [allowWildcard=false] If <code>true</code> then address value can be in the form of <code>\*@example.com</code>, <code>\*suffix@example.com</code> and <code>username@\*</code>, otherwise using \* is not allowed. Static suffix can be up to 32 characters long.
* @apiParam {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object
* @apiParam {Object|String} [internalData] Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
@ -306,6 +332,23 @@ module.exports = (db, server, userHandler) => {
main: booleanSchema,
allowWildcard: booleanSchema,
tags: Joi.array().items(Joi.string().trim().max(128)),
metaData: Joi.alternatives().try(
Joi.string()
.empty('')
.trim()
.max(1024 * 1024),
Joi.object()
),
internalData: Joi.alternatives().try(
Joi.string()
.empty('')
.trim()
.max(1024 * 1024),
Joi.object()
),
sess: sessSchema,
ip: sessIPSchema
});
@ -442,6 +485,35 @@ module.exports = (db, server, userHandler) => {
result.value.tagsview = tags.map(tag => tag.toLowerCase());
}
for (let key of ['metaData', 'internalData']) {
if (result.value[key]) {
if (typeof result.value[key] === 'object') {
try {
result.value[key] = JSON.stringify(result.value[key]);
} catch (err) {
res.json({
error: `${key} value must be serializable to JSON`,
code: 'InputValidationError'
});
return next();
}
} else {
try {
let value = JSON.parse(result.value[key]);
if (!value || typeof value !== 'object') {
throw new Error('Not an object');
}
} catch (err) {
res.json({
error: `${key} value must be valid JSON object string`,
code: 'InputValidationError'
});
return next();
}
}
}
}
let userData;
try {
userData = await db.users.collection('users').findOne(
@ -504,6 +576,14 @@ module.exports = (db, server, userHandler) => {
addressData.tagsview = result.value.tags;
}
if (result.value.metaData) {
addressData.metaData = result.value.metaData;
}
if (result.value.internalData) {
addressData.internalData = result.value.internalData;
}
let r;
// insert alias address to email address registry
try {
@ -555,6 +635,8 @@ module.exports = (db, server, userHandler) => {
* }
*
* @apiParam {String} user ID of the User
* @apiParam {Boolean} [metaData] If true, then includes <code>metaData</code> in the response
* @apiParam {Boolean} [internalData] If true, then includes <code>internalData</code> in the response. Not shown for user-role tokens.
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {Object[]} results Address listing
@ -601,6 +683,8 @@ module.exports = (db, server, userHandler) => {
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
metaData: booleanSchema,
internalData: booleanSchema,
sess: sessSchema,
ip: sessIPSchema
});
@ -623,12 +707,16 @@ module.exports = (db, server, userHandler) => {
let user = new ObjectID(result.value.user);
// permissions check
let permission;
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('addresses'));
permission = roles.can(req.role).readOwn('addresses');
} else {
req.validate(roles.can(req.role).readAny('addresses'));
permission = roles.can(req.role).readAny('addresses');
}
// permissions check
req.validate(permission);
let userData;
try {
userData = await db.users.collection('users').findOne(
@ -685,14 +773,26 @@ module.exports = (db, server, userHandler) => {
res.json({
success: true,
results: addresses.map(address => ({
id: address._id,
name: address.name || false,
address: address.address,
main: address.address === userData.address,
tags: address.tags || [],
created: address.created
}))
results: addresses.map(addressData => {
let values = {
id: addressData._id.toString(),
name: addressData.name || false,
address: addressData.address,
main: addressData.address === userData.address,
tags: addressData.tags || [],
created: addressData.created
};
if (result.value.metaData && addressData.metaData) {
values.metaData = formatMetaData(addressData.metaData);
}
if (result.value.internalData && addressData.internalData) {
values.internalData = formatMetaData(addressData.internalData);
}
return permission.filter(values);
})
});
return next();
@ -770,11 +870,13 @@ module.exports = (db, server, userHandler) => {
let user = new ObjectID(result.value.user);
// permissions check
let permission;
if (req.user && req.user === result.value.user) {
req.validate(roles.can(req.role).readOwn('addresses'));
permission = roles.can(req.role).readOwn('addresses');
} else {
req.validate(roles.can(req.role).readAny('addresses'));
permission = roles.can(req.role).readAny('addresses');
}
req.validate(permission);
let address = new ObjectID(result.value.address);
@ -829,14 +931,24 @@ module.exports = (db, server, userHandler) => {
return next();
}
res.json({
let value = {
success: true,
id: addressData._id,
id: addressData._id.toString(),
name: addressData.name || false,
address: addressData.address,
main: addressData.address === userData.address,
created: addressData.created
});
};
if (addressData.metaData) {
value.metaData = formatMetaData(addressData.metaData);
}
if (addressData.internalData) {
value.internalData = formatMetaData(addressData.internalData);
}
res.json(permission.filter(value));
return next();
})
@ -857,6 +969,8 @@ module.exports = (db, server, userHandler) => {
* @apiParam {String} [name] Identity name
* @apiParam {String} [address] New address if you want to rename existing address. Only affects normal addresses, special addresses that include \* can not be changed
* @apiParam {Boolean} main Indicates if this is the default address for the User
* @apiParam {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object
* @apiParam {Object|String} [internalData] Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens
* @apiParam {String[]} [tags] A list of tags associated with this address
*
@ -895,6 +1009,23 @@ module.exports = (db, server, userHandler) => {
address: Joi.string().email({ tlds: false }),
main: booleanSchema,
tags: Joi.array().items(Joi.string().trim().max(128)),
metaData: Joi.alternatives().try(
Joi.string()
.empty('')
.trim()
.max(1024 * 1024),
Joi.object()
),
internalData: Joi.alternatives().try(
Joi.string()
.empty('')
.trim()
.max(1024 * 1024),
Joi.object()
),
sess: sessSchema,
ip: sessIPSchema
});
@ -1044,6 +1175,36 @@ module.exports = (db, server, userHandler) => {
addressData.address = result.value.address;
}
for (let key of ['metaData', 'internalData']) {
if (result.value[key]) {
if (typeof result.value[key] === 'object') {
try {
updates[key] = JSON.stringify(result.value[key]);
} catch (err) {
res.json({
error: `${key} value must be serializable to JSON`,
code: 'InputValidationError'
});
return next();
}
} else {
try {
let value = JSON.parse(result.value[key]);
if (!value || typeof value !== 'object') {
throw new Error('Not an object');
}
updates[key] = result.value[key];
} catch (err) {
res.json({
error: `${key} value must be valid JSON object string`,
code: 'InputValidationError'
});
return next();
}
}
}
}
if (Object.keys(updates).length) {
try {
await db.users.collection('addresses').updateOne(
@ -2300,7 +2461,7 @@ module.exports = (db, server, userHandler) => {
if (addressData.user) {
res.json({
success: true,
id: addressData._id,
id: addressData._id.toString(),
address: addressData.address,
user: addressData.user,
tags: addressData.tags || [],
@ -2328,7 +2489,7 @@ module.exports = (db, server, userHandler) => {
res.json({
success: true,
id: addressData._id,
id: addressData._id.toString(),
name: addressData.name || '',
address: addressData.address,
targets: addressData.targets && addressData.targets.map(t => t.value),
@ -2545,3 +2706,14 @@ module.exports = (db, server, userHandler) => {
})
);
};
function formatMetaData(metaData) {
if (typeof metaData === 'string') {
try {
metaData = JSON.parse(metaData);
} catch (err) {
// ignore
}
}
return metaData || {};
}

View file

@ -32,7 +32,7 @@
"mailparser": "3.0.0",
"mocha": "8.1.3",
"request": "2.88.2",
"supertest": "4.0.2"
"supertest": "5.0.0"
},
"dependencies": {
"@phc/pbkdf2": "1.1.14",

View file

@ -191,7 +191,10 @@ describe('API tests', function () {
.post(`/users/${userId}/addresses`)
.send({
address: 'alias1@example.com',
main: true
main: true,
metaData: {
tere: 123
}
})
.expect(200);
expect(response1.body.success).to.be.true;
@ -214,8 +217,10 @@ describe('API tests', function () {
it('should GET /users/:user/addresses (updated listing)', async () => {
const response = await server.get(`/users/${userId}/addresses`).expect(200);
expect(response.body.success).to.be.true;
expect(response.body.results.length).to.equal(3);
response.body.results.sort((a, b) => a.id.localeCompare(b.id));
expect(response.body.results[0].address).to.equal('testuser@example.com');
@ -223,7 +228,9 @@ describe('API tests', function () {
expect(response.body.results[1].address).to.equal('alias1@example.com');
expect(response.body.results[1].main).to.be.true;
expect(response.body.results[1].metaData).to.not.exist;
// no metaData present
expect(response.body.results[2].address).to.equal('alias2@example.com');
expect(response.body.results[2].main).to.be.false;
@ -235,6 +242,25 @@ describe('API tests', function () {
expect(response.body.success).to.be.true;
});
it('should GET /users/:user/addresses (with metaData)', async () => {
const response = await server.get(`/users/${userId}/addresses?metaData=true`).expect(200);
expect(response.body.success).to.be.true;
expect(response.body.results.length).to.equal(2);
response.body.results.sort((a, b) => a.id.localeCompare(b.id));
expect(response.body.results[1].address).to.equal('alias1@example.com');
expect(response.body.results[1].main).to.be.true;
expect(response.body.results[1].metaData.tere).to.equal(123);
address = response.body.results[1];
});
it('should GET /users/:user/address/:address', async () => {
const response = await server.get(`/users/${userId}/addresses/${address.id}`).expect(200);
expect(response.body.success).to.be.true;
expect(response.body.metaData.tere).to.equal(123);
});
it('should GET /users/:user/addresses (after DELETE)', async () => {
const response = await server.get(`/users/${userId}/addresses`).expect(200);
expect(response.body.success).to.be.true;