Account deletion endpoint
This commit is contained in:
@ -26,6 +26,15 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`#user.delete should require authentication 1`] = `
|
||||||
|
Object {
|
||||||
|
"error": "authentication_required",
|
||||||
|
"message": "Authentication required",
|
||||||
|
"ok": false,
|
||||||
|
"status": 401,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`#user.demote should demote an admin 1`] = `
|
exports[`#user.demote should demote an admin 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"data": Object {
|
"data": Object {
|
||||||
|
@ -164,4 +164,22 @@ router.post('user.activate', auth(), async ctx => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('user.delete', auth(), async ctx => {
|
||||||
|
const { confirmation } = ctx.body;
|
||||||
|
ctx.assertPresent(confirmation, 'confirmation is required');
|
||||||
|
|
||||||
|
const user = ctx.state.user;
|
||||||
|
authorize(user, 'delete', user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await user.destroy();
|
||||||
|
} catch (err) {
|
||||||
|
throw new ValidationError(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@ -3,6 +3,7 @@ import TestServer from 'fetch-test-server';
|
|||||||
import app from '..';
|
import app from '..';
|
||||||
|
|
||||||
import { flushdb, seed } from '../test/support';
|
import { flushdb, seed } from '../test/support';
|
||||||
|
import { buildUser } from '../test/factories';
|
||||||
|
|
||||||
const server = new TestServer(app.callback());
|
const server = new TestServer(app.callback());
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ afterAll(server.close);
|
|||||||
|
|
||||||
describe('#user.info', async () => {
|
describe('#user.info', async () => {
|
||||||
it('should return known user', async () => {
|
it('should return known user', async () => {
|
||||||
const { user } = await seed();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/user.info', {
|
const res = await server.post('/api/user.info', {
|
||||||
body: { token: user.getJwtToken() },
|
body: { token: user.getJwtToken() },
|
||||||
});
|
});
|
||||||
@ -22,7 +23,6 @@ describe('#user.info', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
await seed();
|
|
||||||
const res = await server.post('/api/user.info');
|
const res = await server.post('/api/user.info');
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
@ -31,6 +31,50 @@ describe('#user.info', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#user.delete', async () => {
|
||||||
|
it('should not allow deleting without confirmation', async () => {
|
||||||
|
const user = await buildUser();
|
||||||
|
const res = await server.post('/api/user.delete', {
|
||||||
|
body: { token: user.getJwtToken() },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow deleting last admin if only user', async () => {
|
||||||
|
const user = await buildUser({ isAdmin: true });
|
||||||
|
const res = await server.post('/api/user.delete', {
|
||||||
|
body: { token: user.getJwtToken(), confirmation: true },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow deleting last admin if many users', async () => {
|
||||||
|
const user = await buildUser({ isAdmin: true });
|
||||||
|
await buildUser({ teamId: user.teamId, isAdmin: false });
|
||||||
|
|
||||||
|
const res = await server.post('/api/user.delete', {
|
||||||
|
body: { token: user.getJwtToken(), confirmation: true },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow deleting user account with confirmation', async () => {
|
||||||
|
const user = await buildUser();
|
||||||
|
const res = await server.post('/api/user.delete', {
|
||||||
|
body: { token: user.getJwtToken(), confirmation: true },
|
||||||
|
});
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const res = await server.post('/api/user.delete');
|
||||||
|
const body = await res.json();
|
||||||
|
|
||||||
|
expect(res.status).toEqual(401);
|
||||||
|
expect(body).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#user.update', async () => {
|
describe('#user.update', async () => {
|
||||||
it('should update user profile information', async () => {
|
it('should update user profile information', async () => {
|
||||||
const { user } = await seed();
|
const { user } = await seed();
|
||||||
@ -44,7 +88,6 @@ describe('#user.update', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
await seed();
|
|
||||||
const res = await server.post('/api/user.update');
|
const res = await server.post('/api/user.update');
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
@ -67,7 +110,7 @@ describe('#user.promote', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require admin', async () => {
|
it('should require admin', async () => {
|
||||||
const { user } = await seed();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/user.promote', {
|
const res = await server.post('/api/user.promote', {
|
||||||
body: { token: user.getJwtToken(), id: user.id },
|
body: { token: user.getJwtToken(), id: user.id },
|
||||||
});
|
});
|
||||||
@ -96,7 +139,7 @@ describe('#user.demote', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't demote admins if only one available ", async () => {
|
it("shouldn't demote admins if only one available ", async () => {
|
||||||
const { admin } = await seed();
|
const admin = await buildUser({ isAdmin: true });
|
||||||
|
|
||||||
const res = await server.post('/api/user.demote', {
|
const res = await server.post('/api/user.demote', {
|
||||||
body: {
|
body: {
|
||||||
@ -111,7 +154,7 @@ describe('#user.demote', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require admin', async () => {
|
it('should require admin', async () => {
|
||||||
const { user } = await seed();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/user.promote', {
|
const res = await server.post('/api/user.promote', {
|
||||||
body: { token: user.getJwtToken(), id: user.id },
|
body: { token: user.getJwtToken(), id: user.id },
|
||||||
});
|
});
|
||||||
@ -139,8 +182,7 @@ describe('#user.suspend', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't allow suspending the user themselves", async () => {
|
it("shouldn't allow suspending the user themselves", async () => {
|
||||||
const { admin } = await seed();
|
const admin = await buildUser({ isAdmin: true });
|
||||||
|
|
||||||
const res = await server.post('/api/user.suspend', {
|
const res = await server.post('/api/user.suspend', {
|
||||||
body: {
|
body: {
|
||||||
token: admin.getJwtToken(),
|
token: admin.getJwtToken(),
|
||||||
@ -154,7 +196,7 @@ describe('#user.suspend', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require admin', async () => {
|
it('should require admin', async () => {
|
||||||
const { user } = await seed();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/user.suspend', {
|
const res = await server.post('/api/user.suspend', {
|
||||||
body: { token: user.getJwtToken(), id: user.id },
|
body: { token: user.getJwtToken(), id: user.id },
|
||||||
});
|
});
|
||||||
@ -187,7 +229,7 @@ describe('#user.activate', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require admin', async () => {
|
it('should require admin', async () => {
|
||||||
const { user } = await seed();
|
const user = await buildUser();
|
||||||
const res = await server.post('/api/user.activate', {
|
const res = await server.post('/api/user.activate', {
|
||||||
body: { token: user.getJwtToken(), id: user.id },
|
body: { token: user.getJwtToken(), id: user.id },
|
||||||
});
|
});
|
||||||
|
16
server/migrations/20180707220121-more-soft-delete.js
Normal file
16
server/migrations/20180707220121-more-soft-delete.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('users', 'deletedAt', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
await queryInterface.addColumn('teams', 'deletedAt', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('users', 'deletedAt');
|
||||||
|
await queryInterface.removeColumn('teams', 'deletedAt');
|
||||||
|
}
|
||||||
|
}
|
@ -37,6 +37,7 @@ const User = sequelize.define(
|
|||||||
suspendedById: DataTypes.UUID,
|
suspendedById: DataTypes.UUID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
paranoid: true,
|
||||||
getterMethods: {
|
getterMethods: {
|
||||||
isSuspended() {
|
isSuspended() {
|
||||||
return !!this.suspendedAt;
|
return !!this.suspendedAt;
|
||||||
@ -132,8 +133,33 @@ const hashPassword = model => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeIdentifyingInfo = model => {
|
||||||
|
model.email = '';
|
||||||
|
model.username = '';
|
||||||
|
model.slackData = null;
|
||||||
|
model.serviceId = null;
|
||||||
|
model.isAdmin = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkLastAdmin = async model => {
|
||||||
|
const teamId = model.teamId;
|
||||||
|
|
||||||
|
if (model.isAdmin) {
|
||||||
|
const userCount = await User.count({ where: { teamId } });
|
||||||
|
const adminCount = await User.count({ where: { isAdmin: true, teamId } });
|
||||||
|
|
||||||
|
if (userCount > 1 && adminCount <= 1) {
|
||||||
|
throw new Error(
|
||||||
|
'Cannot delete account as only admin. Please transfer admin permissions to another user and try again.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
User.beforeCreate(hashPassword);
|
User.beforeCreate(hashPassword);
|
||||||
User.beforeUpdate(hashPassword);
|
User.beforeUpdate(hashPassword);
|
||||||
|
User.beforeDestroy(checkLastAdmin);
|
||||||
|
User.beforeDestroy(removeIdentifyingInfo);
|
||||||
User.beforeSave(uploadAvatar);
|
User.beforeSave(uploadAvatar);
|
||||||
User.beforeCreate(setRandomJwtSecret);
|
User.beforeCreate(setRandomJwtSecret);
|
||||||
User.afterCreate(user => sendEmail('welcome', user.email));
|
User.afterCreate(user => sendEmail('welcome', user.email));
|
||||||
|
Reference in New Issue
Block a user