added signup v3 endpoints and developed initial new signup flow

This commit is contained in:
Sheen Capadngan
2023-05-04 01:32:40 +08:00
parent f703ee29e5
commit 1172726e74
15 changed files with 243 additions and 34 deletions
+2
View File
@@ -1,9 +1,11 @@
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController'; import * as workspacesController from './workspacesController';
import * as authController from './authController'; import * as authController from './authController';
import * as signupController from './signupController';
export { export {
authController, authController,
secretsController, secretsController,
signupController,
workspacesController, workspacesController,
} }
@@ -0,0 +1,177 @@
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
initializeDefaultOrg
} from '../../helpers/signup';
import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config';
import { BadRequestError } from '../../utils/errors';
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
providerAuthToken?: string;
} = req.body;
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken,
user,
});
} else {
const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if(AUTH_TOKEN_TYPE === null) {
throw BadRequestError({message: `Missing Authorization Header in the request header.`});
}
if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') {
throw BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})
}
if(AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: 'Missing Authorization Body in the request header',
})
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
);
if (decodedToken.userId !== user.id) {
throw BadRequestError();
}
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id.toString()
});
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await request.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,
token
});
};
+3 -1
View File
@@ -65,7 +65,8 @@ import {
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
secrets as v3SecretsRouter, secrets as v3SecretsRouter,
workspaces as v3WorkspacesRouter signup as v3SignupRouter,
workspaces as v3WorkspacesRouter,
} from './routes/v3'; } from './routes/v3';
import { healthCheck } from './routes/status'; import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger'; import { getLogger } from './utils/logger';
@@ -170,6 +171,7 @@ const main = async () => {
app.use('/api/v3/auth', v3AuthRouter); app.use('/api/v3/auth', v3AuthRouter);
app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter);
app.use('/api/v3/signup', v3SignupRouter);
// api docs // api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
+1
View File
@@ -42,6 +42,7 @@ const userSchema = new Schema<IUser>(
email: { email: {
type: String, type: String,
required: true, required: true,
unique: true,
}, },
firstName: { firstName: {
type: String type: String
+3 -1
View File
@@ -1,9 +1,11 @@
import auth from './auth'; import auth from './auth';
import secrets from './secrets'; import secrets from './secrets';
import workspaces from './workspaces'; import workspaces from './workspaces';
import signup from './signup';
export { export {
auth, auth,
secrets, secrets,
workspaces signup,
workspaces,
} }
+28
View File
@@ -0,0 +1,28 @@
import express from 'express';
const router = express.Router();
import { body } from 'express-validator';
import { signupController } from '../../controllers/v3';
import { authLimiter } from '../../helpers/rateLimiter';
import { validateRequest } from '../../middleware';
router.post(
'/complete-account/signup',
authLimiter,
body('email').exists().isString().trim().notEmpty().isEmail(),
body('firstName').exists().isString().trim().notEmpty(),
body('lastName').exists().isString().trim().notEmpty(),
body('protectedKey').exists().isString().trim().notEmpty(),
body('protectedKeyIV').exists().isString().trim().notEmpty(),
body('protectedKeyTag').exists().isString().trim().notEmpty(),
body('publicKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKey').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(),
body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(),
body('salt').exists().isString().trim().notEmpty(),
body('verifier').exists().isString().trim().notEmpty(),
body('organizationName').exists().isString().trim().notEmpty(),
validateRequest,
signupController.completeAccountSignup,
);
export default router;
@@ -12,7 +12,6 @@ import { getTranslatedStaticProps } from '@app/components/utilities/withTranslat
import SecurityClient from '../utilities/SecurityClient'; import SecurityClient from '../utilities/SecurityClient';
export default function PasswordInputStep({ export default function PasswordInputStep({
userId,
email, email,
password, password,
setPassword, setPassword,
@@ -20,7 +19,6 @@ export default function PasswordInputStep({
setStep setStep
}: { }: {
email: string; email: string;
userId: string;
password: string; password: string;
setPassword: (password: string) => void; setPassword: (password: string) => void;
setProviderAuthToken: (value: string) => void; setProviderAuthToken: (value: string) => void;
@@ -34,13 +32,8 @@ export default function PasswordInputStep({
const handleLogin = async () => { const handleLogin = async () => {
try { try {
if (!userId || !password) {
return;
}
setIsLoading(true); setIsLoading(true);
const isLoginSuccessful = await attemptLogin({ const isLoginSuccessful = await attemptLogin({
userId,
email, email,
password password
}); });
@@ -32,6 +32,7 @@ interface UserInfoStepProps {
setFirstName: (value: string) => void; setFirstName: (value: string) => void;
lastName: string; lastName: string;
setLastName: (value: string) => void; setLastName: (value: string) => void;
providerAuthToken?: string;
} }
/** /**
@@ -55,7 +56,8 @@ export default function UserInfoStep({
firstName, firstName,
setFirstName, setFirstName,
lastName, lastName,
setLastName setLastName,
providerAuthToken,
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const [firstNameError, setFirstNameError] = useState(false); const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
@@ -156,9 +158,10 @@ export default function UserInfoStep({
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
organizationName: `${firstName}'s organization` organizationName: `${firstName}'s organization`,
}); });
// unset signup JWT token and set JWT token // unset signup JWT token and set JWT token
@@ -29,20 +29,17 @@ const attemptLogin = async (
{ {
email, email,
password, password,
userId,
}: { }: {
email: string; email: string;
userId?: string;
password: string; password: string;
} }
): Promise<IsLoginSuccessful> => { ): Promise<IsLoginSuccessful> => {
const username = userId ?? email;
const telemetry = new Telemetry().getInstance(); const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
client.init( client.init(
{ {
username, username: email,
password password
}, },
async () => { async () => {
@@ -51,7 +48,6 @@ const attemptLogin = async (
const { serverPublicKey, salt } = await login1({ const { serverPublicKey, salt } = await login1({
email, email,
clientPublicKey, clientPublicKey,
userId,
}); });
client.setSalt(salt); client.setSalt(salt);
@@ -72,7 +68,6 @@ const attemptLogin = async (
} = await login2( } = await login2(
{ {
email, email,
userId,
clientProof, clientProof,
} }
); );
+7 -1
View File
@@ -31,10 +31,16 @@ export const useProviderAuth = () => {
window.addEventListener('storage', handleStorageChange); window.addEventListener('storage', handleStorageChange);
if (providerAuthToken) {
const { userId: resultUserId, email: resultEmail } = jwt_decode(providerAuthToken) as any;
setEmail(resultEmail);
setUserId(resultUserId);
}
return () => { return () => {
window.removeEventListener('storage', handleStorageChange); window.removeEventListener('storage', handleStorageChange);
}; };
}); }, []);
return { return {
email, email,
@@ -8,6 +8,7 @@ interface Props {
protectedKey: string; protectedKey: string;
protectedKeyIV: string; protectedKeyIV: string;
protectedKeyTag: string; protectedKeyTag: string;
providerAuthToken?: string;
publicKey: string; publicKey: string;
encryptedPrivateKey: string; encryptedPrivateKey: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyIV: string;
@@ -49,9 +50,10 @@ const completeAccountInformationSignup = async ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier,
organizationName organizationName,
providerAuthToken,
}: Props) => { }: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', { const { data } = await apiRequest.post('/api/v3/signup/complete-account/signup', {
email, email,
firstName, firstName,
lastName, lastName,
@@ -64,7 +66,8 @@ const completeAccountInformationSignup = async ({
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier,
organizationName organizationName,
providerAuthToken,
}); });
return data; return data;
-1
View File
@@ -12,7 +12,6 @@ interface Login1 {
const login1 = async (loginDetails: { const login1 = async (loginDetails: {
email: string; email: string;
clientPublicKey: string; clientPublicKey: string;
userId?: string;
}) => { }) => {
const response = await fetch("/api/v2/auth/login1", { const response = await fetch("/api/v2/auth/login1", {
method: "POST", method: "POST",
-1
View File
@@ -20,7 +20,6 @@ interface Login2Response {
const login2 = async (loginDetails: { const login2 = async (loginDetails: {
email: string; email: string;
clientProof: string; clientProof: string;
userId?: string;
}) => { }) => {
const response = await fetch('/api/v2/auth/login2', { const response = await fetch('/api/v2/auth/login2', {
method: 'POST', method: 'POST',
-2
View File
@@ -25,7 +25,6 @@ export default function Login() {
const [isLoginWithEmail, setIsLoginWithEmail] = useState(false); const [isLoginWithEmail, setIsLoginWithEmail] = useState(false);
const { const {
providerAuthToken, providerAuthToken,
userId,
email: providerEmail, email: providerEmail,
setProviderAuthToken setProviderAuthToken
} = useProviderAuth(); } = useProviderAuth();
@@ -57,7 +56,6 @@ export default function Login() {
if (providerAuthToken && step === 1) { if (providerAuthToken && step === 1) {
return ( return (
<PasswordInputStep <PasswordInputStep
userId={userId}
email={providerEmail} email={providerEmail}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
+5 -4
View File
@@ -35,7 +35,7 @@ export default function SignUp() {
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false); const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { providerAuthToken } = useProviderAuth(); const { email: providerEmail, providerAuthToken } = useProviderAuth();
if (providerAuthToken && step < 3) { if (providerAuthToken && step < 3) {
setStep(3); setStep(3);
@@ -99,7 +99,7 @@ export default function SignUp() {
return ( return (
<> <>
<button type='button' className='text-white' onClick={() => { <button type='button' className='text-white' onClick={() => {
window.open('/api/v1/auth/login/google') window.open('/api/v1/oauth/redirect/google')
}}> }}>
Continue with Google Continue with Google
</button> </button>
@@ -127,13 +127,14 @@ export default function SignUp() {
return ( return (
<UserInfoStep <UserInfoStep
incrementStep={incrementStep} incrementStep={incrementStep}
email={email} email={email || providerEmail}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
firstName={firstName} firstName={firstName}
setFirstName={setFirstName} setFirstName={setFirstName}
lastName={lastName} lastName={lastName}
setLastName={setLastName} setLastName={setLastName}
providerAuthToken={providerAuthToken}
/> />
) )
} }
@@ -142,7 +143,7 @@ export default function SignUp() {
return ( return (
<DownloadBackupPDF <DownloadBackupPDF
incrementStep={incrementStep} incrementStep={incrementStep}
email={email} email={email || providerEmail}
password={password} password={password}
name={`${firstName} ${lastName}`} name={`${firstName} ${lastName}`}
/> />