chore: Remove old auth cruft
parent
081284c2d3
commit
0c23844793
|
@ -11,24 +11,24 @@ import moment from 'moment';
|
|||
import dedent from 'dedent';
|
||||
import debugFactory from 'debug';
|
||||
import { isEmail } from 'validator';
|
||||
import path from 'path';
|
||||
import loopback from 'loopback';
|
||||
import _ from 'lodash';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import generate from 'nanoid/generate';
|
||||
|
||||
import { homeLocation, apiLocation } from '../../../config/env';
|
||||
import { apiLocation } from '../../../config/env';
|
||||
|
||||
import { fixCompletedChallengeItem } from '../utils';
|
||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||
import { blacklistedUsernames } from '../../server/utils/constants.js';
|
||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||
import { getEmailSender } from '../../server/utils/url-utils.js';
|
||||
import {
|
||||
normaliseUserFields,
|
||||
getProgress,
|
||||
publicUserProps
|
||||
} from '../../server/utils/publicUserProps';
|
||||
import {
|
||||
setAccessTokenToResponse,
|
||||
removeCookies
|
||||
} from '../../server/utils/getSetAccessToken';
|
||||
|
||||
const log = debugFactory('fcc:models:user');
|
||||
const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
|
||||
|
@ -93,42 +93,6 @@ function isTheSame(val1, val2) {
|
|||
return val1 === val2;
|
||||
}
|
||||
|
||||
const renderSignUpEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-sign-up.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
const renderSignInEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-sign-in.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
const renderEmailChangeEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-update-email.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
function getAboutProfile({
|
||||
username,
|
||||
githubProfile: github,
|
||||
|
@ -147,37 +111,34 @@ function nextTick(fn) {
|
|||
return process.nextTick(fn);
|
||||
}
|
||||
|
||||
function getWaitPeriod(ttl) {
|
||||
const fiveMinutesAgo = moment().subtract(5, 'minutes');
|
||||
const lastEmailSentAt = moment(new Date(ttl || null));
|
||||
const isWaitPeriodOver = ttl
|
||||
? lastEmailSentAt.isBefore(fiveMinutesAgo)
|
||||
: true;
|
||||
|
||||
if (!isWaitPeriodOver) {
|
||||
const minutesLeft = 5 - (moment().minutes() - lastEmailSentAt.minutes());
|
||||
return minutesLeft;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getWaitMessage(ttl) {
|
||||
const minutesLeft = getWaitPeriod(ttl);
|
||||
if (minutesLeft <= 0) {
|
||||
return null;
|
||||
}
|
||||
const timeToWait = minutesLeft
|
||||
? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}`
|
||||
: 'a few seconds';
|
||||
|
||||
return dedent`
|
||||
Please wait ${timeToWait} to resend an authentication link.
|
||||
`;
|
||||
}
|
||||
const getRandomNumber = () => Math.random();
|
||||
|
||||
module.exports = function(User) {
|
||||
function populateRequiredFields(user) {
|
||||
user.username = user.username.trim().toLowerCase();
|
||||
user.email =
|
||||
typeof user.email === 'string'
|
||||
? user.email.trim().toLowerCase()
|
||||
: user.email;
|
||||
|
||||
if (!user.progressTimestamps) {
|
||||
user.progressTimestamps = [];
|
||||
}
|
||||
|
||||
if (user.progressTimestamps.length === 0) {
|
||||
user.progressTimestamps.push(Date.now());
|
||||
}
|
||||
|
||||
if (!user.externalId) {
|
||||
user.externalId = uuid();
|
||||
}
|
||||
|
||||
if (!user.unsubscribeId) {
|
||||
user.unsubscribeId = generate(nanoidCharSet, 20);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export default function(User) {
|
||||
// set salt factor for passwords
|
||||
User.settings.saltWorkFactor = 5;
|
||||
// set user.rand to random number
|
||||
|
@ -217,29 +178,13 @@ module.exports = function(User) {
|
|||
throw createEmailError();
|
||||
}
|
||||
// assign random username to new users
|
||||
// actual usernames will come from github
|
||||
// use full uuid to ensure uniqueness
|
||||
user.username = 'fcc' + uuid();
|
||||
|
||||
if (!user.externalId) {
|
||||
user.externalId = uuid();
|
||||
}
|
||||
if (!user.unsubscribeId) {
|
||||
user.unsubscribeId = generate(nanoidCharSet, 20);
|
||||
}
|
||||
|
||||
if (!user.progressTimestamps) {
|
||||
user.progressTimestamps = [];
|
||||
}
|
||||
|
||||
if (user.progressTimestamps.length === 0) {
|
||||
user.progressTimestamps.push(Date.now());
|
||||
}
|
||||
populateRequiredFields(user);
|
||||
return Observable.fromPromise(User.doesExist(null, user.email)).do(
|
||||
exists => {
|
||||
if (exists) {
|
||||
throw wrapHandledError(new Error('user already exists'), {
|
||||
redirectTo: `${homeLocation}/signin`,
|
||||
redirectTo: `${apiLocation}/signin`,
|
||||
message: dedent`
|
||||
The ${user.email} email address is already associated with an account.
|
||||
Try signing in with it here instead.
|
||||
|
@ -263,28 +208,7 @@ module.exports = function(User) {
|
|||
if (user.email && !isEmail(user.email)) {
|
||||
throw createEmailError();
|
||||
}
|
||||
|
||||
user.username = user.username.trim().toLowerCase();
|
||||
user.email =
|
||||
typeof user.email === 'string'
|
||||
? user.email.trim().toLowerCase()
|
||||
: user.email;
|
||||
|
||||
if (!user.progressTimestamps) {
|
||||
user.progressTimestamps = [];
|
||||
}
|
||||
|
||||
if (user.progressTimestamps.length === 0) {
|
||||
user.progressTimestamps.push(Date.now());
|
||||
}
|
||||
|
||||
if (!user.externalId) {
|
||||
user.externalId = uuid();
|
||||
}
|
||||
|
||||
if (!user.unsubscribeId) {
|
||||
user.unsubscribeId = generate(nanoidCharSet, 20);
|
||||
}
|
||||
populateRequiredFields(user);
|
||||
})
|
||||
.ignoreElements();
|
||||
return Observable.merge(beforeCreate, updateOrSave).toPromise();
|
||||
|
@ -364,32 +288,13 @@ module.exports = function(User) {
|
|||
});
|
||||
};
|
||||
|
||||
function manualReload() {
|
||||
this.reload((err, instance) => {
|
||||
if (err) {
|
||||
throw Error('failed to reload user instance');
|
||||
}
|
||||
Object.assign(this, instance);
|
||||
log('user reloaded from db');
|
||||
});
|
||||
}
|
||||
User.prototype.manualReload = manualReload;
|
||||
|
||||
User.prototype.loginByRequest = function loginByRequest(req, res) {
|
||||
const {
|
||||
query: { emailChange }
|
||||
} = req;
|
||||
const createToken = this.createAccessToken$().do(accessToken => {
|
||||
const config = {
|
||||
signed: !!req.signedCookies,
|
||||
maxAge: accessToken.ttl,
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
};
|
||||
if (accessToken && accessToken.id) {
|
||||
const jwtAccess = jwt.sign({ accessToken }, process.env.JWT_SECRET);
|
||||
res.cookie('jwt_access_token', jwtAccess, config);
|
||||
res.cookie('access_token', accessToken.id, config);
|
||||
res.cookie('userId', accessToken.userId, config);
|
||||
setAccessTokenToResponse({ accessToken }, req, res);
|
||||
}
|
||||
});
|
||||
let data = {
|
||||
|
@ -421,14 +326,7 @@ module.exports = function(User) {
|
|||
};
|
||||
|
||||
User.afterRemote('logout', function({ req, res }, result, next) {
|
||||
const config = {
|
||||
signed: !!req.signedCookies,
|
||||
domain: process.env.COOKIE_DOMAIN || 'localhost'
|
||||
};
|
||||
res.clearCookie('jwt_access_token', config);
|
||||
res.clearCookie('access_token', config);
|
||||
res.clearCookie('userId', config);
|
||||
res.clearCookie('_csrf', config);
|
||||
removeCookies(req, res);
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -533,154 +431,6 @@ module.exports = function(User) {
|
|||
);
|
||||
};
|
||||
|
||||
User.prototype.getEncodedEmail = function getEncodedEmail(email) {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
return Buffer(email).toString('base64');
|
||||
};
|
||||
|
||||
User.decodeEmail = email => Buffer(email, 'base64').toString();
|
||||
|
||||
function requestAuthEmail(isSignUp, newEmail) {
|
||||
return Observable.defer(() => {
|
||||
const messageOrNull = getWaitMessage(this.emailAuthLinkTTL);
|
||||
if (messageOrNull) {
|
||||
throw wrapHandledError(new Error('request is throttled'), {
|
||||
type: 'info',
|
||||
message: messageOrNull
|
||||
});
|
||||
}
|
||||
|
||||
// create a temporary access token with ttl for 15 minutes
|
||||
return this.createAuthToken({ ttl: 15 * 60 * 1000 });
|
||||
})
|
||||
.flatMap(token => {
|
||||
let renderAuthEmail = renderSignInEmail;
|
||||
let subject = 'Your sign in link for freeCodeCamp.org';
|
||||
if (isSignUp) {
|
||||
renderAuthEmail = renderSignUpEmail;
|
||||
subject = 'Your sign in link for your new freeCodeCamp.org account';
|
||||
}
|
||||
if (newEmail) {
|
||||
renderAuthEmail = renderEmailChangeEmail;
|
||||
subject = dedent`
|
||||
Please confirm your updated email address for freeCodeCamp.org
|
||||
`;
|
||||
}
|
||||
const { id: loginToken, created: emailAuthLinkTTL } = token;
|
||||
const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null);
|
||||
const host = apiLocation;
|
||||
const mailOptions = {
|
||||
type: 'email',
|
||||
to: newEmail ? newEmail : this.email,
|
||||
from: getEmailSender(),
|
||||
subject,
|
||||
text: renderAuthEmail({
|
||||
host,
|
||||
loginEmail,
|
||||
loginToken,
|
||||
emailChange: !!newEmail
|
||||
})
|
||||
};
|
||||
const userUpdate = new Promise((resolve, reject) =>
|
||||
this.updateAttributes({ emailAuthLinkTTL }, err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
);
|
||||
return Observable.forkJoin(
|
||||
User.email.send$(mailOptions),
|
||||
Observable.fromPromise(userUpdate)
|
||||
);
|
||||
})
|
||||
.map(
|
||||
() =>
|
||||
'Check your email and click the link we sent you to confirm' +
|
||||
' your new email address.'
|
||||
);
|
||||
}
|
||||
|
||||
User.prototype.requestAuthEmail = requestAuthEmail;
|
||||
|
||||
User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) {
|
||||
const currentEmail = this.email;
|
||||
const isOwnEmail = isTheSame(newEmail, currentEmail);
|
||||
const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail);
|
||||
const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL);
|
||||
const isVerifiedEmail = this.emailVerified;
|
||||
|
||||
if (isOwnEmail && isVerifiedEmail) {
|
||||
// email is already associated and verified with this account
|
||||
throw wrapHandledError(new Error('email is already verified'), {
|
||||
type: 'info',
|
||||
message: `
|
||||
${newEmail} is already associated with this account.
|
||||
You can update a new email address instead.`
|
||||
});
|
||||
}
|
||||
if (isResendUpdateToSameEmail && isLinkSentWithinLimit) {
|
||||
// trying to update with the same newEmail and
|
||||
// confirmation email is still valid
|
||||
throw wrapHandledError(new Error(), {
|
||||
type: 'info',
|
||||
message: dedent`
|
||||
We have already sent an email confirmation request to ${newEmail}.
|
||||
${isLinkSentWithinLimit}`
|
||||
});
|
||||
}
|
||||
if (!isEmail('' + newEmail)) {
|
||||
throw createEmailError();
|
||||
}
|
||||
|
||||
// newEmail is not associated with this user, and
|
||||
// this attempt to change email is the first or
|
||||
// previous attempts have expired
|
||||
if (
|
||||
!isOwnEmail ||
|
||||
(isOwnEmail && !isVerifiedEmail) ||
|
||||
(isResendUpdateToSameEmail && !isLinkSentWithinLimit)
|
||||
) {
|
||||
const updateConfig = {
|
||||
newEmail,
|
||||
emailVerified: false,
|
||||
emailVerifyTTL: new Date()
|
||||
};
|
||||
|
||||
// defer prevents the promise from firing prematurely (before subscribe)
|
||||
return Observable.defer(() => User.doesExist(null, newEmail))
|
||||
.do(exists => {
|
||||
if (exists && !isOwnEmail) {
|
||||
// newEmail is not associated with this account,
|
||||
// but is associated with different account
|
||||
throw wrapHandledError(new Error('email already in use'), {
|
||||
type: 'info',
|
||||
message: `${newEmail} is already associated with another account.`
|
||||
});
|
||||
}
|
||||
})
|
||||
.flatMap(() => {
|
||||
const updatePromise = new Promise((resolve, reject) =>
|
||||
this.updateAttributes(updateConfig, err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
})
|
||||
);
|
||||
return Observable.forkJoin(
|
||||
Observable.fromPromise(updatePromise),
|
||||
this.requestAuthEmail(false, newEmail),
|
||||
(_, message) => message
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return 'Something unexpected happened while updating your email.';
|
||||
}
|
||||
};
|
||||
|
||||
function requestCompletedChallenges() {
|
||||
return this.getCompletedChallenges$();
|
||||
}
|
||||
|
@ -1117,4 +867,4 @@ module.exports = function(User) {
|
|||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import _ from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
import dedent from 'dedent';
|
||||
import passport from 'passport';
|
||||
import { isEmail } from 'validator';
|
||||
import { check } from 'express-validator/check';
|
||||
|
||||
import { homeLocation } from '../../../config/env';
|
||||
import {
|
||||
|
@ -11,11 +6,7 @@ import {
|
|||
saveResponseAuthCookies,
|
||||
loginRedirect
|
||||
} from '../component-passport';
|
||||
import {
|
||||
ifUserRedirectTo,
|
||||
ifNoUserRedirectTo,
|
||||
createValidatorErrorHandler
|
||||
} from '../utils/middleware';
|
||||
import { ifUserRedirectTo } from '../utils/middleware';
|
||||
import { wrapHandledError } from '../utils/create-handled-error.js';
|
||||
import { removeCookies } from '../utils/getSetAccessToken';
|
||||
|
||||
|
@ -31,9 +22,7 @@ module.exports = function enableAuthentication(app) {
|
|||
const ifUserRedirect = ifUserRedirectTo();
|
||||
const saveAuthCookies = saveResponseAuthCookies();
|
||||
const loginSuccessRedirect = loginRedirect();
|
||||
const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation);
|
||||
const api = app.loopback.Router();
|
||||
const { AuthToken, User } = app.models;
|
||||
|
||||
// Use a local mock strategy for signing in if we are in dev mode.
|
||||
// Otherwise we use auth0 login. We use a string for 'true' because values
|
||||
|
@ -73,167 +62,5 @@ module.exports = function enableAuthentication(app) {
|
|||
});
|
||||
});
|
||||
|
||||
const defaultErrorMsg = dedent`
|
||||
Oops, something is not right,
|
||||
please request a fresh link to sign in / sign up.
|
||||
`;
|
||||
|
||||
const passwordlessGetValidators = [
|
||||
check('email')
|
||||
.isBase64()
|
||||
.withMessage('Email should be a base64 encoded string.'),
|
||||
check('token')
|
||||
.exists()
|
||||
.withMessage('Token should exist.')
|
||||
// based on strongloop/loopback/common/models/access-token.js#L15
|
||||
.isLength({ min: 64, max: 64 })
|
||||
.withMessage('Token is not the right length.')
|
||||
];
|
||||
|
||||
function getPasswordlessAuth(req, res, next) {
|
||||
const {
|
||||
query: { email: encodedEmail, token: authTokenId, emailChange } = {}
|
||||
} = req;
|
||||
|
||||
const email = User.decodeEmail(encodedEmail);
|
||||
if (!isEmail(email)) {
|
||||
return next(
|
||||
wrapHandledError(new TypeError('decoded email is invalid'), {
|
||||
type: 'info',
|
||||
message: 'The email encoded in the link is incorrectly formatted',
|
||||
redirectTo: `${homeLocation}/signin`
|
||||
})
|
||||
);
|
||||
}
|
||||
// first find
|
||||
return (
|
||||
AuthToken.findOne$({ where: { id: authTokenId } })
|
||||
.flatMap(authToken => {
|
||||
if (!authToken) {
|
||||
throw wrapHandledError(
|
||||
new Error(`no token found for id: ${authTokenId}`),
|
||||
{
|
||||
type: 'info',
|
||||
message: defaultErrorMsg,
|
||||
redirectTo: `${homeLocation}/signin`
|
||||
}
|
||||
);
|
||||
}
|
||||
// find user then validate and destroy email validation token
|
||||
// finally retun user instance
|
||||
return User.findOne$({ where: { id: authToken.userId } }).flatMap(
|
||||
user => {
|
||||
if (!user) {
|
||||
throw wrapHandledError(
|
||||
new Error(`no user found for token: ${authTokenId}`),
|
||||
{
|
||||
type: 'info',
|
||||
message: defaultErrorMsg,
|
||||
redirectTo: `${homeLocation}/signin`
|
||||
}
|
||||
);
|
||||
}
|
||||
if (user.email !== email) {
|
||||
if (!emailChange || (emailChange && user.newEmail !== email)) {
|
||||
throw wrapHandledError(
|
||||
new Error('user email does not match'),
|
||||
{
|
||||
type: 'info',
|
||||
message: defaultErrorMsg,
|
||||
redirectTo: `${homeLocation}/signin`
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return authToken
|
||||
.validate$()
|
||||
.map(isValid => {
|
||||
if (!isValid) {
|
||||
throw wrapHandledError(new Error('token is invalid'), {
|
||||
type: 'info',
|
||||
message: `
|
||||
Looks like the link you clicked has expired,
|
||||
please request a fresh link, to sign in.
|
||||
`,
|
||||
redirectTo: `${homeLocation}/signin`
|
||||
});
|
||||
}
|
||||
return authToken.destroy$();
|
||||
})
|
||||
.map(() => user);
|
||||
}
|
||||
);
|
||||
})
|
||||
// at this point token has been validated and destroyed
|
||||
// update user and log them in
|
||||
.map(user => user.loginByRequest(req, res))
|
||||
.do(() => {
|
||||
req.flash(
|
||||
'success',
|
||||
'Success! You have signed in to your account. Happy Coding!'
|
||||
);
|
||||
return res.redirectWithFlash(`${homeLocation}/welcome`);
|
||||
})
|
||||
.subscribe(() => {}, next)
|
||||
);
|
||||
}
|
||||
|
||||
api.get(
|
||||
'/passwordless-auth',
|
||||
ifUserRedirect,
|
||||
passwordlessGetValidators,
|
||||
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
||||
getPasswordlessAuth
|
||||
);
|
||||
|
||||
api.get('/passwordless-change', (req, res) =>
|
||||
res.redirect(301, '/confirm-email')
|
||||
);
|
||||
|
||||
api.get(
|
||||
'/confirm-email',
|
||||
ifNoUserRedirectHome,
|
||||
passwordlessGetValidators,
|
||||
getPasswordlessAuth
|
||||
);
|
||||
|
||||
const passwordlessPostValidators = [
|
||||
check('email')
|
||||
.isEmail()
|
||||
.withMessage('Email is not a valid email address.')
|
||||
];
|
||||
function postPasswordlessAuth(req, res, next) {
|
||||
const { body: { email } = {} } = req;
|
||||
|
||||
return User.findOne$({ where: { email } })
|
||||
.flatMap(_user =>
|
||||
Observable.if(
|
||||
// if no user found create new user and save to db
|
||||
_.constant(_user),
|
||||
Observable.of(_user),
|
||||
User.create$({ email })
|
||||
).flatMap(user => user.requestAuthEmail(!_user))
|
||||
)
|
||||
.do(msg => {
|
||||
let redirectTo = homeLocation;
|
||||
|
||||
if (req.session && req.session.returnTo) {
|
||||
redirectTo = req.session.returnTo;
|
||||
}
|
||||
|
||||
req.flash('info', msg);
|
||||
return res.redirect(redirectTo);
|
||||
})
|
||||
.subscribe(_.noop, next);
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/passwordless-auth',
|
||||
ifUserRedirect,
|
||||
passwordlessPostValidators,
|
||||
createValidatorErrorHandler('errors', `${homeLocation}/signin`),
|
||||
postPasswordlessAuth
|
||||
);
|
||||
|
||||
app.use(api);
|
||||
};
|
||||
|
|
|
@ -25,8 +25,6 @@ export function setAccessTokenToResponse(
|
|||
};
|
||||
const jwtAccess = jwt.sign({ accessToken }, jwtSecret);
|
||||
res.cookie(jwtCookieNS, jwtAccess, cookieConfig);
|
||||
res.cookie('access_token', accessToken.id, cookieConfig);
|
||||
res.cookie('userId', accessToken.userId, cookieConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -121,8 +121,7 @@ describe('getSetAccessToken', () => {
|
|||
});
|
||||
|
||||
describe('setAccessTokenToResponse', () => {
|
||||
it('sets three cookies in the response', () => {
|
||||
expect.assertions(3);
|
||||
it('sets a jwt access token cookie in the response', () => {
|
||||
const req = mockReq();
|
||||
const res = mockRes();
|
||||
|
||||
|
@ -139,24 +138,6 @@ describe('getSetAccessToken', () => {
|
|||
maxAge: accessToken.ttl
|
||||
}
|
||||
]);
|
||||
expect(res.cookie.getCall(1).args).toEqual([
|
||||
'access_token',
|
||||
accessToken.id,
|
||||
{
|
||||
signed: false,
|
||||
domain: 'localhost',
|
||||
maxAge: accessToken.ttl
|
||||
}
|
||||
]);
|
||||
expect(res.cookie.getCall(2).args).toEqual([
|
||||
'userId',
|
||||
accessToken.userId,
|
||||
{
|
||||
signed: false,
|
||||
domain: 'localhost',
|
||||
maxAge: accessToken.ttl
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary:
|
||||
|
||||
<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %>
|
||||
|
||||
Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin
|
||||
|
||||
See you soon!
|
||||
|
||||
- The freeCodeCamp.org Team
|
|
@ -1,13 +0,0 @@
|
|||
Welcome to the freeCodeCamp community!
|
||||
|
||||
We have created a new account for you.
|
||||
|
||||
Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary:
|
||||
|
||||
<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %>
|
||||
|
||||
Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin
|
||||
|
||||
See you soon!
|
||||
|
||||
- The freeCodeCamp.org Team
|
|
@ -1,7 +0,0 @@
|
|||
Please confirm this address for freeCodeCamp:
|
||||
|
||||
<%= host %>/internal/confirm-email?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %>
|
||||
|
||||
Happy coding!
|
||||
|
||||
- The freeCodeCamp.org Team
|
Loading…
Reference in New Issue