Account Deletion (#716)
Adds ability to remove user account, wipes personal information and soft-deletes record.
This commit is contained in:
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Collection from './Collection';
|
|
||||||
export default Collection;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import CollectionDelete from './CollectionDelete';
|
|
||||||
export default CollectionDelete;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import CollectionEdit from './CollectionEdit';
|
|
||||||
export default CollectionEdit;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import CollectionExport from './CollectionExport';
|
|
||||||
export default CollectionExport;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import CollectionNew from './CollectionNew';
|
|
||||||
export default CollectionNew;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Dashboard from './Dashboard';
|
|
||||||
export default Dashboard;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import DocumentDelete from './DocumentDelete';
|
|
||||||
export default DocumentDelete;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import DocumentShare from './DocumentShare';
|
|
||||||
export default DocumentShare;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Drafts from './Drafts';
|
|
||||||
export default Drafts;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import ErrorSuspended from './ErrorSuspended';
|
|
||||||
export default ErrorSuspended;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Home from './Home';
|
|
||||||
export default Home;
|
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import KeyboardShortcuts from './KeyboardShortcuts';
|
|
||||||
export default KeyboardShortcuts;
|
|
@ -11,6 +11,7 @@ import Input, { LabelText } from 'components/Input';
|
|||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
|
import UserDelete from 'scenes/UserDelete';
|
||||||
import Flex from 'shared/components/Flex';
|
import Flex from 'shared/components/Flex';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -25,6 +26,7 @@ class Profile extends React.Component<Props> {
|
|||||||
|
|
||||||
@observable name: string;
|
@observable name: string;
|
||||||
@observable avatarUrl: ?string;
|
@observable avatarUrl: ?string;
|
||||||
|
@observable showDeleteModal: boolean = false;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.auth.user) {
|
if (this.props.auth.user) {
|
||||||
@ -58,6 +60,10 @@ class Profile extends React.Component<Props> {
|
|||||||
this.props.ui.showToast(error || 'Unable to upload new avatar');
|
this.props.ui.showToast(error || 'Unable to upload new avatar');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toggleDeleteAccount = () => {
|
||||||
|
this.showDeleteModal = !this.showDeleteModal;
|
||||||
|
};
|
||||||
|
|
||||||
get isValid() {
|
get isValid() {
|
||||||
return this.form && this.form.checkValidity();
|
return this.form && this.form.checkValidity();
|
||||||
}
|
}
|
||||||
@ -97,11 +103,28 @@ class Profile extends React.Component<Props> {
|
|||||||
{isSaving ? 'Saving…' : 'Save'}
|
{isSaving ? 'Saving…' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<DangerZone>
|
||||||
|
<LabelText>Delete Account</LabelText>
|
||||||
|
<p>
|
||||||
|
You may delete your account at any time, note that this is
|
||||||
|
unrecoverable.{' '}
|
||||||
|
<a onClick={this.toggleDeleteAccount}>Delete account</a>.
|
||||||
|
</p>
|
||||||
|
</DangerZone>
|
||||||
|
{this.showDeleteModal && (
|
||||||
|
<UserDelete onRequestClose={this.toggleDeleteAccount} />
|
||||||
|
)}
|
||||||
</CenteredContent>
|
</CenteredContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DangerZone = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
const ProfilePicture = styled(Flex)`
|
const ProfilePicture = styled(Flex)`
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
`;
|
`;
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
// @flow
|
|
||||||
import Starred from './Starred';
|
|
||||||
export default Starred;
|
|
62
app/scenes/UserDelete.js
Normal file
62
app/scenes/UserDelete.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { inject, observer } from 'mobx-react';
|
||||||
|
import Button from 'components/Button';
|
||||||
|
import Flex from 'shared/components/Flex';
|
||||||
|
import HelpText from 'components/HelpText';
|
||||||
|
import Modal from 'components/Modal';
|
||||||
|
import AuthStore from 'stores/AuthStore';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
auth: AuthStore,
|
||||||
|
onRequestClose: () => *,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class UserDelete extends React.Component<Props> {
|
||||||
|
@observable isDeleting: boolean;
|
||||||
|
|
||||||
|
handleSubmit = async (ev: SyntheticEvent<*>) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.isDeleting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await this.props.auth.deleteUser();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
this.props.auth.logout();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isDeleting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { auth, ...rest } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen title="Delete Account" {...rest}>
|
||||||
|
<Flex column>
|
||||||
|
<form onSubmit={this.handleSubmit}>
|
||||||
|
<HelpText>
|
||||||
|
Are you sure? Deleting your account will destory identifying data
|
||||||
|
associated with your user and cannot be undone. You will be
|
||||||
|
immediately logged out of Outline and all your API tokens will be
|
||||||
|
revoked.
|
||||||
|
</HelpText>
|
||||||
|
<HelpText>
|
||||||
|
<strong>Note:</strong> Signing back in will cause a new account to
|
||||||
|
be automatically reprovisioned.
|
||||||
|
</HelpText>
|
||||||
|
<Button type="submit" danger>
|
||||||
|
{this.isDeleting ? 'Deleting…' : 'Delete My Account'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default inject('auth')(UserDelete);
|
@ -50,6 +50,17 @@ class AuthStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
|
deleteUser = async () => {
|
||||||
|
await client.post(`/user.delete`, { confirmation: true });
|
||||||
|
|
||||||
|
runInAction('AuthStore#updateUser', () => {
|
||||||
|
this.user = null;
|
||||||
|
this.team = null;
|
||||||
|
this.token = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@action
|
@action
|
||||||
updateUser = async (params: { name: string, avatarUrl: ?string }) => {
|
updateUser = async (params: { name: string, avatarUrl: ?string }) => {
|
||||||
this.isSaving = true;
|
this.isSaving = true;
|
||||||
|
@ -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 {
|
||||||
@ -61,29 +70,6 @@ Object {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`#user.info should require authentication 1`] = `
|
|
||||||
Object {
|
|
||||||
"error": "authentication_required",
|
|
||||||
"message": "Authentication required",
|
|
||||||
"ok": false,
|
|
||||||
"status": 401,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`#user.info should return known user 1`] = `
|
|
||||||
Object {
|
|
||||||
"data": Object {
|
|
||||||
"avatarUrl": "http://example.com/avatar.png",
|
|
||||||
"createdAt": "2018-01-01T00:00:00.000Z",
|
|
||||||
"id": "46fde1d4-0050-428f-9f0b-0bf77f4bdf61",
|
|
||||||
"name": "User 1",
|
|
||||||
"username": "user1",
|
|
||||||
},
|
|
||||||
"ok": true,
|
|
||||||
"status": 200,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`#user.promote should promote a new admin 1`] = `
|
exports[`#user.promote should promote a new 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,19 +12,60 @@ 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() },
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body).toMatchSnapshot();
|
expect(body.data.id).toEqual(user.id);
|
||||||
|
expect(body.data.name).toEqual(user.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
||||||
|
expect(res.status).toEqual(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
const body = await res.json();
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
@ -44,7 +86,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 +108,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 +137,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 +152,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 +180,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 +194,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 +227,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');
|
||||||
|
}
|
||||||
|
}
|
14
server/migrations/20180708231200-serviceid-null.js
Normal file
14
server/migrations/20180708231200-serviceid-null.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.changeColumn('users', 'serviceId', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.changeColumn('users', 'serviceId', {
|
||||||
|
type: Sequelize.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -31,4 +31,11 @@ const ApiKey = sequelize.define(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ApiKey.associate = models => {
|
||||||
|
ApiKey.belongsTo(models.User, {
|
||||||
|
as: 'user',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default ApiKey;
|
export default ApiKey;
|
||||||
|
@ -141,8 +141,8 @@ Document.associate = models => {
|
|||||||
{
|
{
|
||||||
include: [
|
include: [
|
||||||
{ model: models.Collection, as: 'collection' },
|
{ model: models.Collection, as: 'collection' },
|
||||||
{ model: models.User, as: 'createdBy' },
|
{ model: models.User, as: 'createdBy', paranoid: false },
|
||||||
{ model: models.User, as: 'updatedBy' },
|
{ model: models.User, as: 'updatedBy', paranoid: false },
|
||||||
],
|
],
|
||||||
where: {
|
where: {
|
||||||
publishedAt: {
|
publishedAt: {
|
||||||
@ -156,8 +156,8 @@ Document.associate = models => {
|
|||||||
Document.addScope('withUnpublished', {
|
Document.addScope('withUnpublished', {
|
||||||
include: [
|
include: [
|
||||||
{ model: models.Collection, as: 'collection' },
|
{ model: models.Collection, as: 'collection' },
|
||||||
{ model: models.User, as: 'createdBy' },
|
{ model: models.User, as: 'createdBy', paranoid: false },
|
||||||
{ model: models.User, as: 'updatedBy' },
|
{ model: models.User, as: 'updatedBy', paranoid: false },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
Document.addScope('withViews', userId => ({
|
Document.addScope('withViews', userId => ({
|
||||||
|
@ -6,6 +6,7 @@ import subMinutes from 'date-fns/sub_minutes';
|
|||||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||||
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
|
||||||
import { sendEmail } from '../mailer';
|
import { sendEmail } from '../mailer';
|
||||||
|
import { Star, ApiKey } from '.';
|
||||||
|
|
||||||
const User = sequelize.define(
|
const User = sequelize.define(
|
||||||
'user',
|
'user',
|
||||||
@ -25,13 +26,14 @@ const User = sequelize.define(
|
|||||||
slackData: DataTypes.JSONB,
|
slackData: DataTypes.JSONB,
|
||||||
jwtSecret: encryptedFields.vault('jwtSecret'),
|
jwtSecret: encryptedFields.vault('jwtSecret'),
|
||||||
lastActiveAt: DataTypes.DATE,
|
lastActiveAt: DataTypes.DATE,
|
||||||
lastActiveIp: DataTypes.STRING,
|
lastActiveIp: { type: DataTypes.STRING, allowNull: true },
|
||||||
lastSignedInAt: DataTypes.DATE,
|
lastSignedInAt: DataTypes.DATE,
|
||||||
lastSignedInIp: DataTypes.STRING,
|
lastSignedInIp: { type: DataTypes.STRING, allowNull: true },
|
||||||
suspendedAt: DataTypes.DATE,
|
suspendedAt: DataTypes.DATE,
|
||||||
suspendedById: DataTypes.UUID,
|
suspendedById: DataTypes.UUID,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
paranoid: true,
|
||||||
getterMethods: {
|
getterMethods: {
|
||||||
isSuspended() {
|
isSuspended() {
|
||||||
return !!this.suspendedAt;
|
return !!this.suspendedAt;
|
||||||
@ -91,6 +93,41 @@ const setRandomJwtSecret = model => {
|
|||||||
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
model.jwtSecret = crypto.randomBytes(64).toString('hex');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeIdentifyingInfo = async model => {
|
||||||
|
await ApiKey.destroy({ where: { userId: model.id } });
|
||||||
|
await Star.destroy({ where: { userId: model.id } });
|
||||||
|
|
||||||
|
model.email = '';
|
||||||
|
model.name = 'Unknown';
|
||||||
|
model.avatarUrl = '';
|
||||||
|
model.serviceId = null;
|
||||||
|
model.username = null;
|
||||||
|
model.slackData = null;
|
||||||
|
model.lastActiveIp = null;
|
||||||
|
model.lastSignedInIp = null;
|
||||||
|
|
||||||
|
// this shouldn't be needed once this issue is resolved:
|
||||||
|
// https://github.com/sequelize/sequelize/issues/9318
|
||||||
|
await model.save({ hooks: 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.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