feat: Guest email authentication (#1088)

* feat: API endpoints for email signin

* fix: After testing

* Initial signin flow working

* move shared middleware

* feat: Add guest signin toggle, obey on endpoints

* feat: Basic email signin when enabled

* Improve guest signin email
Disable double signin with JWT

* fix: Simple rate limiting

* create placeholder users in db

* fix: Give invited users default avatar
add invited users to people settings

* test

* add transaction

* tmp: test CI

* derp

* md5

* urgh

* again

* test: pass

* test

* fix: Remove usage of data values

* guest signin page

* Visually separator 'Invited' from other people tabs

* fix: Edge case attempting SSO signin for guest email account

* fix: Correctly set email auth method to cookie

* Improve rate limit error display

* lint: cleanup / comments

* Improve invalid token error display

* style tweaks

* pass guest value to subdomain

* Restore copy link option

* feat: Allow invite revoke from people management

* fix: Incorrect users email schema does not allow for user deletion

* lint

* fix: avatarUrl for deleted user failure

* change default to off for guest invites

* fix: Changing security settings wipes subdomain

* fix: user delete permissioning

* test: Add user.invite specs
This commit is contained in:
Tom Moor
2019-12-15 18:46:08 -08:00
committed by GitHub
parent 5731ff34a4
commit 6d8216c54e
45 changed files with 846 additions and 206 deletions

View File

@ -8,6 +8,8 @@ import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3';
import { sendEmail } from '../mailer';
import { Star, Team, Collection, NotificationSetting, ApiKey } from '.';
const DEFAULT_AVATAR_HOST = 'https://tiley.herokuapp.com';
const User = sequelize.define(
'user',
{
@ -29,6 +31,7 @@ const User = sequelize.define(
lastActiveIp: { type: DataTypes.STRING, allowNull: true },
lastSignedInAt: DataTypes.DATE,
lastSignedInIp: { type: DataTypes.STRING, allowNull: true },
lastSigninEmailSentAt: DataTypes.DATE,
suspendedAt: DataTypes.DATE,
suspendedById: DataTypes.UUID,
},
@ -38,6 +41,18 @@ const User = sequelize.define(
isSuspended() {
return !!this.suspendedAt;
},
avatarUrl() {
const original = this.getDataValue('avatarUrl');
if (original) {
return original;
}
const hash = crypto
.createHash('md5')
.update(this.email || '')
.digest('hex');
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${this.name[0]}.png`;
},
},
}
);
@ -96,12 +111,28 @@ User.prototype.getJwtToken = function() {
return JWT.sign({ id: this.id }, this.jwtSecret);
};
User.prototype.getEmailSigninToken = function() {
if (this.service && this.service !== 'email') {
throw new Error('Cannot generate email signin token for OAuth user');
}
return JWT.sign(
{ id: this.id, createdAt: new Date().toISOString() },
this.jwtSecret
);
};
const uploadAvatar = async model => {
const endpoint = publicS3Endpoint();
const { avatarUrl } = model;
if (model.avatarUrl && !model.avatarUrl.startsWith(endpoint)) {
if (
avatarUrl &&
!avatarUrl.startsWith(endpoint) &&
!avatarUrl.startsWith(DEFAULT_AVATAR_HOST)
) {
const newUrl = await uploadToS3FromUrl(
model.avatarUrl,
avatarUrl,
`avatars/${model.id}/${uuid.v4()}`
);
if (newUrl) model.avatarUrl = newUrl;
@ -126,7 +157,7 @@ const removeIdentifyingInfo = async (model, options) => {
transaction: options.transaction,
});
model.email = '';
model.email = null;
model.name = 'Unknown';
model.avatarUrl = '';
model.serviceId = null;
@ -165,7 +196,7 @@ User.afterCreate(async user => {
// From Slack support:
// If you wish to contact users at an email address obtained through Slack,
// you need them to opt-in through a clear and separate process.
if (!team.slackId) {
if (user.service && user.service !== 'slack') {
sendEmail('welcome', user.email, { teamUrl: team.url });
}
});