diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index c6e7ce3d3..85321680f 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -19,7 +19,6 @@ import express from 'express' import axios from 'axios' import { env } from '../../env' import url from 'url' -import { IntercomClient } from '../../utils/intercom' import { kx } from '../../datalayer/knex_config' import UserModel from '../../datalayer/user' import { buildLogger } from '../../utils/logger' @@ -45,13 +44,15 @@ import { } from '../../utils/auth' import { createUser } from '../../services/create_user' import { isErrorWithCode } from '../../resolvers' -import { initModels } from '../../server' -import { getRepository } from '../../entity/utils' +import { AppDataSource, initModels } from '../../server' +import { getRepository, setClaims } from '../../entity/utils' import { User } from '../../entity/user' import { sendConfirmationEmail, sendPasswordResetEmail, } from '../../services/send_emails' +import { createWebAuthToken } from './jwt_helpers' +import { createSsoToken, ssoRedirectURL } from '../../utils/sso' const logger = buildLogger('app.dispatch') const signToken = promisify(jwt.sign) @@ -311,53 +312,52 @@ export function authRouter() { newUser: boolean ): Promise { try { - const redirect = (res: express.Response): void => { - let redirectUri: string | null = null - if (req.query.state) { - // Google login case: redirect_uri is in query state param. - try { - const state = JSON.parse((req.query?.state || '') as string) - redirectUri = state?.redirect_uri - } catch (err) { - console.warn( - 'handleSuccessfulLogin: failed to parse redirect query state param', - err - ) - } - } - - const message = res.get('Message') - if (message) { - return res.redirect( - `${env.client.url}/home?message=${encodeURIComponent(message)}` + let redirectUri: string | null = null + if (req.query.state) { + // Google login case: redirect_uri is in query state param. + try { + const state = JSON.parse((req.query?.state || '') as string) + redirectUri = state?.redirect_uri + } catch (err) { + console.warn( + 'handleSuccessfulLogin: failed to parse redirect query state param', + err ) } - - if (newUser) { - if (redirectUri && redirectUri !== '/') { - return res.redirect( - url.resolve(env.client.url, decodeURIComponent(redirectUri)) - ) - } - return res.redirect( - `${env.client.url}/settings/installation/extensions` - ) - } - - return res.redirect( - url.resolve(env.client.url, decodeURIComponent(redirectUri || 'home')) - ) } - if (env.server.apiEnv && !env.dev.isLocal && IntercomClient) { - if (newUser) { - redirect(res) + if (newUser) { + if (redirectUri && redirectUri !== '/') { + redirectUri = url.resolve( + env.client.url, + decodeURIComponent(redirectUri) + ) } else { - redirect(res) + redirectUri = `${env.client.url}/home` } - } else { - redirect(res) } + + redirectUri = redirectUri ? redirectUri : `${env.client.url}/home` + + const message = res.get('Message') + if (message) { + const u = new URL(redirectUri) + u.searchParams.append('message', message) + redirectUri = u.toString() + } + + // If we do have an auth token, we want to try redirecting to the + // sso endpoint which will set a cookie for the client domain (omnivore.app) + // after we set a cookie for the API domain (api-prod.omnivore.app) + const authToken = await createWebAuthToken(user.id) + if (authToken) { + const ssoToken = createSsoToken(authToken, redirectUri) + redirectUri = ssoRedirectURL(ssoToken) + } + + await setAuthInCookie({ uid: user.id }, res) + + return res.redirect(redirectUri) } catch (error) { logger.info('handleSuccessfulLogin exception:', error) return res.redirect(`${env.client.url}/login?errorCodes=AUTH_FAILED`) @@ -377,7 +377,7 @@ export function authRouter() { if (!email || !password) { return res.redirect( - `${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}` + `${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}` ) } @@ -388,7 +388,7 @@ export function authRouter() { }) if (!user?.id) { return res.redirect( - `${env.client.url}/email-login?errorCodes=${LoginErrorCode.UserNotFound}` + `${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}` ) } @@ -399,14 +399,14 @@ export function authRouter() { name: user.name, }) return res.redirect( - `${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION` + `${env.client.url}/auth/email-login?errorCodes=PENDING_VERIFICATION` ) } if (!user?.password) { // user has no password, so they need to set one return res.redirect( - `${env.client.url}/email-login?errorCodes=${LoginErrorCode.WrongSource}` + `${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.WrongSource}` ) } @@ -414,16 +414,16 @@ export function authRouter() { const validPassword = await comparePassword(password, user.password) if (!validPassword) { return res.redirect( - `${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}` + `${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}` ) } - // set auth cookie in response header - await setAuthInCookie({ uid: user.id }, res) await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('email-login exception:', e) - res.redirect(`${env.client.url}/email-login?errorCodes=AUTH_FAILED`) + res.redirect( + `${env.client.url}/auth/email-login?errorCodes=AUTH_FAILED` + ) } } ) @@ -441,7 +441,7 @@ export function authRouter() { if (!email || !password || !name || !username) { return res.redirect( - `${env.client.url}/email-signup?errorCodes=INVALID_CREDENTIALS` + `${env.client.url}/auth/email-signup?errorCodes=INVALID_CREDENTIALS` ) } const lowerCasedUsername = username.toLowerCase() @@ -462,15 +462,17 @@ export function authRouter() { pendingConfirmation: true, }) - res.redirect(`${env.client.url}/email-login?message=SIGNUP_SUCCESS`) + res.redirect( + `${env.client.url}/auth/verify-email?message=SIGNUP_SUCCESS` + ) } catch (e) { logger.info('email-signup exception:', e) if (isErrorWithCode(e)) { return res.redirect( - `${env.client.url}/email-signup?errorCodes=${e.errorCode}` + `${env.client.url}/auth/email-signup?errorCodes=${e.errorCode}` ) } - res.redirect(`${env.client.url}/email-signup?errorCodes=UNKNOWN`) + res.redirect(`${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`) } } ) @@ -491,36 +493,47 @@ export function authRouter() { const claims = await getClaimsByToken(token) if (!claims) { return res.redirect( - `${env.client.url}/confirm-email?errorCodes=INVALID_TOKEN` + `${env.client.url}/auth/confirm-email?errorCodes=INVALID_TOKEN` ) } const user = await getRepository(User).findOneBy({ id: claims.uid }) if (!user) { return res.redirect( - `${env.client.url}/confirm-email?errorCodes=USER_NOT_FOUND` + `${env.client.url}/auth/confirm-email?errorCodes=USER_NOT_FOUND` ) } if (user.status === StatusType.Pending) { - await getRepository(User).update( - { id: user.id }, - { status: StatusType.Active } + const updated = await AppDataSource.transaction( + async (entityManager) => { + await setClaims(entityManager, user.id) + return entityManager + .getRepository(User) + .update({ id: user.id }, { status: StatusType.Active }) + } ) + + if (!updated.affected) { + return res.redirect( + `${env.client.url}/auth/confirm-email?errorCodes=UNKNOWN` + ) + } } res.set('Message', 'EMAIL_CONFIRMED') - await setAuthInCookie({ uid: user.id }, res) await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('confirm-email exception:', e) if (e instanceof jwt.TokenExpiredError) { return res.redirect( - `${env.client.url}/confirm-email?errorCodes=TOKEN_EXPIRED` + `${env.client.url}/auth/confirm-email?errorCodes=TOKEN_EXPIRED` ) } - res.redirect(`${env.client.url}/confirm-email?errorCodes=INVALID_TOKEN`) + res.redirect( + `${env.client.url}/auth/confirm-email?errorCodes=INVALID_TOKEN` + ) } } ) @@ -537,7 +550,7 @@ export function authRouter() { const email = req.body.email if (!email) { return res.redirect( - `${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL` + `${env.client.url}/auth/forgot-password?errorCodes=INVALID_EMAIL` ) } @@ -546,28 +559,26 @@ export function authRouter() { email, }) if (!user) { - return res.redirect( - `${env.client.url}/forgot-password?errorCodes=USER_NOT_FOUND` - ) + return res.redirect(`${env.client.url}/auth/reset-sent`) } if (user.status === StatusType.Pending) { - return res.redirect( - `${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION` - ) + return res.redirect(`${env.client.url}/auth/reset-sent`) } if (!(await sendPasswordResetEmail(user))) { return res.redirect( - `${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL` + `${env.client.url}/auth/forgot-password?errorCodes=INVALID_EMAIL` ) } - res.redirect(`${env.client.url}/forgot-password?message=SUCCESS`) + res.redirect(`${env.client.url}/auth/reset-sent`) } catch (e) { logger.info('forgot-password exception:', e) - res.redirect(`${env.client.url}/forgot-password?errorCodes=UNKNOWN`) + res.redirect( + `${env.client.url}/auth/forgot-password?errorCodes=UNKNOWN` + ) } } ) @@ -588,51 +599,55 @@ export function authRouter() { const claims = await getClaimsByToken(token) if (!claims) { return res.redirect( - `${env.client.url}/reset-password?errorCodes=INVALID_TOKEN` + `${env.client.url}/auth/reset-password/${token}?errorCodes=INVALID_TOKEN` ) } - if (!password) { + if (!password || password.length < 8) { return res.redirect( - `${env.client.url}/reset-password?errorCodes=INVALID_PASSWORD` + `${env.client.url}/auth/reset-password/${token}?errorCodes=INVALID_PASSWORD` ) } const user = await getRepository(User).findOneBy({ id: claims.uid }) if (!user) { return res.redirect( - `${env.client.url}/reset-password?errorCodes=USER_NOT_FOUND` + `${env.client.url}/auth/reset-password/${token}?errorCodes=USER_NOT_FOUND` ) } if (user.status === StatusType.Pending) { return res.redirect( - `${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION` + `${env.client.url}/auth/email-login?errorCodes=PENDING_VERIFICATION` ) } const hashedPassword = await hashPassword(password) - const updated = await getRepository(User).update( - { id: user.id }, - { password: hashedPassword } + const updated = await AppDataSource.transaction( + async (entityManager) => { + await setClaims(entityManager, user.id) + return entityManager + .getRepository(User) + .update({ id: user.id }, { password: hashedPassword }) + } ) if (!updated.affected) { return res.redirect( - `${env.client.url}/reset-password?errorCodes=UNKNOWN` + `${env.client.url}/auth/reset-password/${token}?errorCodes=UNKNOWN` ) } - res.redirect(`${env.client.url}/reset-password?message=SUCCESS`) + await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('reset-password exception:', e) if (e instanceof jwt.TokenExpiredError) { return res.redirect( - `${env.client.url}/reset-password?errorCodes=TOKEN_EXPIRED` + `${env.client.url}/auth/reset-password/?errorCodes=TOKEN_EXPIRED` ) } res.redirect( - `${env.client.url}/reset-password?errorCodes=INVALID_TOKEN` + `${env.client.url}/auth/reset-password/?errorCodes=INVALID_TOKEN` ) } } diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts index a6fbf6d42..2ebefd3d1 100644 --- a/packages/api/src/services/send_emails.ts +++ b/packages/api/src/services/send_emails.ts @@ -9,7 +9,7 @@ export const sendConfirmationEmail = async (user: { }): Promise => { // generate confirmation link const token = generateVerificationToken(user.id) - const link = `${env.client.url}/confirm-email/${token}` + const link = `${env.client.url}/auth/confirm-email/${token}` // send email const dynamicTemplateData = { name: user.name, @@ -31,7 +31,7 @@ export const sendPasswordResetEmail = async (user: { }): Promise => { // generate link const token = generateVerificationToken(user.id) - const link = `${env.client.url}/reset-password/${token}` + const link = `${env.client.url}/auth/reset-password/${token}` // send email const dynamicTemplateData = { name: user.name, diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index 526ccce02..721f19c55 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -61,7 +61,7 @@ describe('auth router', () => { sinon.restore() }) - it('redirects to login page', async () => { + it('redirects to verify email', async () => { const res = await signupRequest( email, password, @@ -69,7 +69,7 @@ describe('auth router', () => { username ).expect(302) expect(res.header.location).to.endWith( - '/email-login?message=SIGNUP_SUCCESS' + '/verify-email?message=SIGNUP_SUCCESS' ) }) @@ -173,9 +173,9 @@ describe('auth router', () => { password = correctPassword }) - it('redirects to home page', async () => { + it('redirects to sso page', async () => { const res = await loginRequest(email, password).expect(302) - expect(res.header.location).to.endWith('/home') + expect(res.header.location).to.contain('/api/client/auth?tok') }) it('set auth token in cookie', async () => { @@ -297,9 +297,9 @@ describe('auth router', () => { expect(res.header['set-cookie'][0]).to.contain('auth') }) - it('redirects to home page', async () => { + it('redirects to sso page', async () => { const res = await confirmEmailRequest(token).expect(302) - expect(res.header.location).to.endWith('/home?message=EMAIL_CONFIRMED') + expect(res.header.location).to.contain('/api/client/auth?tok') }) it('sets user as active', async () => { @@ -395,7 +395,7 @@ describe('auth router', () => { it('redirects to forgot-password page with success message', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/forgot-password?message=SUCCESS' + '/auth/reset-sent' ) }) }) @@ -432,7 +432,7 @@ describe('auth router', () => { it('redirects to email-login page with error code PENDING_VERIFICATION', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/email-login?errorCodes=PENDING_VERIFICATION' + '/auth/reset-sent' ) }) }) @@ -446,7 +446,7 @@ describe('auth router', () => { it('redirects to forgot-password page with error code USER_NOT_FOUND', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/forgot-password?errorCodes=USER_NOT_FOUND' + '/auth/reset-sent' ) }) }) @@ -498,8 +498,8 @@ describe('auth router', () => { const res = await resetPasswordRequest(token, 'new_password').expect( 302 ) - expect(res.header.location).to.endWith( - '/reset-password?message=SUCCESS' + expect(res.header.location).to.contain( + '/api/client/auth?tok' ) }) @@ -517,8 +517,8 @@ describe('auth router', () => { context('when password is empty', () => { it('redirects to reset-password page with error code INVALID_PASSWORD', async () => { const res = await resetPasswordRequest(token, '').expect(302) - expect(res.header.location).to.endWith( - '/reset-password?errorCodes=INVALID_PASSWORD' + expect(res.header.location).to.match( + /.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_PASSWORD/g ) }) }) @@ -530,8 +530,8 @@ describe('auth router', () => { 'invalid_token', 'new_password' ).expect(302) - expect(res.header.location).to.endWith( - '/reset-password?errorCodes=INVALID_TOKEN' + expect(res.header.location).to.match( + /.*\/auth\/reset-password\/(.*)?\?errorCodes=INVALID_TOKEN/g ) }) @@ -545,7 +545,7 @@ describe('auth router', () => { 302 ) expect(res.header.location).to.endWith( - '/reset-password?errorCodes=TOKEN_EXPIRED' + '/auth/reset-password/?errorCodes=TOKEN_EXPIRED' ) }) }) diff --git a/packages/web/components/templates/LoginForm.tsx b/packages/web/components/templates/LoginForm.tsx index c48ef75f2..9af721277 100644 --- a/packages/web/components/templates/LoginForm.tsx +++ b/packages/web/components/templates/LoginForm.tsx @@ -106,33 +106,6 @@ export function LoginForm(props: LoginFormProps): JSX.Element { /> )} -{/* - - - Use your email address to{' '} - - - Login - - {' '} - or{' '} - - - Signup - - {' '} - with your email address. - - */} - diff --git a/packages/web/components/templates/EmailResetPassword.tsx b/packages/web/components/templates/auth/EmailForgotPassword.tsx similarity index 75% rename from packages/web/components/templates/EmailResetPassword.tsx rename to packages/web/components/templates/auth/EmailForgotPassword.tsx index 4d4b256db..b572ec2d4 100644 --- a/packages/web/components/templates/EmailResetPassword.tsx +++ b/packages/web/components/templates/auth/EmailForgotPassword.tsx @@ -1,23 +1,21 @@ -import { SpanBox, VStack } from '../elements/LayoutPrimitives' -import { Button } from '../elements/Button' -import { StyledText } from '../elements/StyledText' +import { SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' import { useEffect, useState } from 'react' -import { FormInput } from '../elements/FormElements' -import { TermAndConditionsFooter } from './LoginForm' -import { fetchEndpoint } from '../../lib/appConfig' -import { logoutMutation } from '../../lib/networking/mutations/logoutMutation' +import { FormInput } from '../../elements/FormElements' +import { fetchEndpoint } from '../../../lib/appConfig' +import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation' import { styled } from '@stitches/react' import { useRouter } from 'next/router' -import { formatMessage } from '../../locales/en/messages' -import { parseErrorCodes } from '../../lib/queryParamParser' - -const StyledTextSpan = styled('span', StyledText) +import { formatMessage } from '../../../locales/en/messages' +import { parseErrorCodes } from '../../../lib/queryParamParser' const BorderedFormInput = styled(FormInput, { height: '40px', paddingLeft: '6px', borderRadius: '6px', background: 'white', + color: '$omnivoreGray', border: `1px solid 1px solid rgba(0, 0, 0, 0.06)`, }) @@ -26,9 +24,9 @@ const FormLabel = styled('label', { color: '$omnivoreGray', }) -export function EmailResetPassword(): JSX.Element { +export function EmailForgotPassword(): JSX.Element { const router = useRouter() - const [email, setEmail] = useState(undefined) + const [email, setEmail] = useState('') const [errorMessage, setErrorMessage] = useState(undefined) useEffect(() => { @@ -41,7 +39,7 @@ export function EmailResetPassword(): JSX.Element { }, [router.isReady, router.query]) return ( -
+ Reset your password diff --git a/packages/web/components/templates/EmailLogin.tsx b/packages/web/components/templates/auth/EmailLogin.tsx similarity index 83% rename from packages/web/components/templates/EmailLogin.tsx rename to packages/web/components/templates/auth/EmailLogin.tsx index f644974dd..cca01788c 100644 --- a/packages/web/components/templates/EmailLogin.tsx +++ b/packages/web/components/templates/auth/EmailLogin.tsx @@ -1,15 +1,15 @@ -import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' -import { Button } from '../elements/Button' -import { StyledText } from '../elements/StyledText' +import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' import { useEffect, useState } from 'react' -import { FormInput } from '../elements/FormElements' -import { TermAndConditionsFooter } from './LoginForm' -import { fetchEndpoint } from '../../lib/appConfig' -import { logoutMutation } from '../../lib/networking/mutations/logoutMutation' +import { FormInput } from '../../elements/FormElements' +import { TermAndConditionsFooter } from '../LoginForm' +import { fetchEndpoint } from '../../../lib/appConfig' +import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation' import { styled } from '@stitches/react' import { useRouter } from 'next/router' -import { parseErrorCodes } from '../../lib/queryParamParser' -import { formatMessage } from '../../locales/en/messages' +import { parseErrorCodes } from '../../../lib/queryParamParser' +import { formatMessage } from '../../../locales/en/messages' import Link from 'next/link' const StyledTextSpan = styled('span', StyledText) @@ -45,7 +45,7 @@ export function EmailLogin(): JSX.Element { return ( - Login + Login Email @@ -111,7 +111,7 @@ export function EmailLogin(): JSX.Element { }} > Don't have an account? {' '} - + Sign up @@ -126,7 +126,7 @@ export function EmailLogin(): JSX.Element { }} > Forgot your password? {' '} - + Click here diff --git a/packages/web/components/templates/auth/EmailResetPassword.tsx b/packages/web/components/templates/auth/EmailResetPassword.tsx new file mode 100644 index 000000000..43b8e65d8 --- /dev/null +++ b/packages/web/components/templates/auth/EmailResetPassword.tsx @@ -0,0 +1,81 @@ +import { SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' +import { useEffect, useState } from 'react' +import { FormInput } from '../../elements/FormElements' +import { TermAndConditionsFooter } from '../LoginForm' +import { fetchEndpoint } from '../../../lib/appConfig' +import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation' +import { styled } from '@stitches/react' +import { useRouter } from 'next/router' +import { formatMessage } from '../../../locales/en/messages' +import { parseErrorCodes } from '../../../lib/queryParamParser' +import { LoadingView } from '../../patterns/LoadingView' + +const BorderedFormInput = styled(FormInput, { + height: '40px', + paddingLeft: '6px', + borderRadius: '6px', + background: 'white', + color: '$omnivoreGray', + border: `1px solid 1px solid rgba(0, 0, 0, 0.06)`, +}) + +const FormLabel = styled('label', { + fontSize: '16px', + color: '$omnivoreGray', +}) + +export function EmailResetPassword(): JSX.Element { + const router = useRouter() + const [token, setToken] = useState(undefined) + const [password, setPassword] = useState('') + const [errorMessage, setErrorMessage] = useState(undefined) + + useEffect(() => { + if (!router.isReady) return + const errorCode = parseErrorCodes(router.query) + const errorMsg = errorCode + ? formatMessage({ id: `error.${errorCode}` }) + : undefined + + console.log('errorCode', errorCode, errorMsg) + + setErrorMessage(errorMsg) + setToken(router.query.token as string) + }, [router.isReady, router.query]) + + if (!token) { + return + } + + return ( + + + + + Enter new password + { e.preventDefault(); setPassword(e.target.value); }} + /> + (Password must be at least 8 chars) + + + + + + {errorMessage && ( + {errorMessage} + )} + + + + ) +} diff --git a/packages/web/components/templates/EmailSignup.tsx b/packages/web/components/templates/auth/EmailSignup.tsx similarity index 88% rename from packages/web/components/templates/EmailSignup.tsx rename to packages/web/components/templates/auth/EmailSignup.tsx index 8c12b5b30..dc4948100 100644 --- a/packages/web/components/templates/EmailSignup.tsx +++ b/packages/web/components/templates/auth/EmailSignup.tsx @@ -1,16 +1,16 @@ -import { HStack, SpanBox, VStack } from '../elements/LayoutPrimitives' -import { Button } from '../elements/Button' -import { StyledText } from '../elements/StyledText' +import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { Button } from '../../elements/Button' +import { StyledText } from '../../elements/StyledText' import { useCallback, useEffect, useMemo, useState } from 'react' -import { FormInput } from '../elements/FormElements' -import { TermAndConditionsFooter } from './LoginForm' -import { fetchEndpoint } from '../../lib/appConfig' -import { useValidateUsernameQuery } from '../../lib/networking/queries/useValidateUsernameQuery' -import { logoutMutation } from '../../lib/networking/mutations/logoutMutation' +import { FormInput } from '../../elements/FormElements' +import { TermAndConditionsFooter } from '../LoginForm' +import { fetchEndpoint } from '../../../lib/appConfig' +import { useValidateUsernameQuery } from '../../../lib/networking/queries/useValidateUsernameQuery' +import { logoutMutation } from '../../../lib/networking/mutations/logoutMutation' import { styled } from '@stitches/react' import { useRouter } from 'next/router' -import { formatMessage } from '../../locales/en/messages' -import { parseErrorCodes } from '../../lib/queryParamParser' +import { formatMessage } from '../../../locales/en/messages' +import { parseErrorCodes } from '../../../lib/queryParamParser' import Link from 'next/link' const StyledTextSpan = styled('span', StyledText) @@ -173,7 +173,7 @@ export function EmailSignup(): JSX.Element { }} > Already have an account? {' '} - + Login instead diff --git a/packages/web/components/templates/auth/ResetSent.tsx b/packages/web/components/templates/auth/ResetSent.tsx new file mode 100644 index 000000000..6c64dc3af --- /dev/null +++ b/packages/web/components/templates/auth/ResetSent.tsx @@ -0,0 +1,33 @@ +import { Box, HStack } from '../../elements/LayoutPrimitives' +import type { LoginFormProps } from '../LoginForm' + +export function ResetSent(props: LoginFormProps): JSX.Element { + return ( + <> + + +

Reset email sent

+ + If there is an account assosciated with the email specified we sent a + password reset link. Click the link to reset your password. You may need + to check your spam folder. + +
+
+ + ) +} diff --git a/packages/web/components/templates/auth/VerifyEmail.tsx b/packages/web/components/templates/auth/VerifyEmail.tsx new file mode 100644 index 000000000..b7f0279f5 --- /dev/null +++ b/packages/web/components/templates/auth/VerifyEmail.tsx @@ -0,0 +1,35 @@ +import { Box, HStack, MediumBreakpointBox, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import type { LoginFormProps } from '../LoginForm' +import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo' +import { theme } from '../../tokens/stitches.config' + +export function VerifyEmail(props: LoginFormProps): JSX.Element { + return ( + <> + + +

Verify your email address

+ + We sent a verification link to the email you provided. + Click the link to verify your email. You may need to check + your spam folder. + +
+
+ + ) +} diff --git a/packages/web/locales/en/messages.ts b/packages/web/locales/en/messages.ts index 1efc879fc..306e427c1 100644 --- a/packages/web/locales/en/messages.ts +++ b/packages/web/locales/en/messages.ts @@ -24,6 +24,8 @@ const errorMessages: Record = { 'error.EXPIRED_TOKEN': "Your sign up page has timed out, you'll be redirected to Google sign in page to authenticate again.", 'error.USER_EXISTS': 'User with this email exists already', + 'error.UNKNOWN': 'An unknown error occurred', + 'error.INVALID_PASSWORD': 'Invalid password. Password must be at least 8 chars.' } const loginPageMessages: Record = { diff --git a/packages/web/pages/auth/confirm-email/[token].tsx b/packages/web/pages/auth/confirm-email/[token].tsx new file mode 100644 index 000000000..892fb7c39 --- /dev/null +++ b/packages/web/pages/auth/confirm-email/[token].tsx @@ -0,0 +1,40 @@ +import { useEffect, useRef, useState } from 'react' +import { useRouter } from 'next/router' + +import { HStack } from '../../../components/elements/LayoutPrimitives' +import { fetchEndpoint } from '../../../lib/appConfig' +import { LoadingView } from '../../../components/patterns/LoadingView' +import { PageMetaData } from '../../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../../components/templates/ProfileLayout' + +export default function ConfirmEmail(): JSX.Element { + const authForm = useRef(null) + const router = useRouter() + + useEffect(() => { + if (!router || !router.isReady || !authForm.current) { + return + } + + authForm.current?.submit() + }, [router, authForm]) + + return ( + <> + + +
+ +
+ + + +
+
+ + ) +} diff --git a/packages/web/pages/auth/email-login.tsx b/packages/web/pages/auth/email-login.tsx new file mode 100644 index 000000000..ae36ba62b --- /dev/null +++ b/packages/web/pages/auth/email-login.tsx @@ -0,0 +1,15 @@ +import { PageMetaData } from '../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../components/templates/ProfileLayout' +import { EmailLogin } from '../../components/templates/auth/EmailLogin' + +export default function EmailLoginPage(): JSX.Element { + return ( + <> + + + + +
+ + ) +} diff --git a/packages/web/pages/auth/email-signup.tsx b/packages/web/pages/auth/email-signup.tsx new file mode 100644 index 000000000..7c2d24d8d --- /dev/null +++ b/packages/web/pages/auth/email-signup.tsx @@ -0,0 +1,15 @@ +import { PageMetaData } from '../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../components/templates/ProfileLayout' +import { EmailSignup } from '../../components/templates/auth/EmailSignup' + +export default function EmailRegistrationPage(): JSX.Element { + return ( + <> + + + + +
+ + ) +} diff --git a/packages/web/pages/auth/forgot-password.tsx b/packages/web/pages/auth/forgot-password.tsx new file mode 100644 index 000000000..05e0c26da --- /dev/null +++ b/packages/web/pages/auth/forgot-password.tsx @@ -0,0 +1,24 @@ +import { PageMetaData } from '../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../components/templates/ProfileLayout' +import { EmailForgotPassword } from '../../components/templates/auth/EmailForgotPassword' +import { useEffect } from 'react' +import { useRouter } from 'next/router' +import toast, { Toaster } from 'react-hot-toast' +import { showSuccessToast } from '../../lib/toastHelpers' + +export default function ForgotPassword(): JSX.Element { + return ( + <> + + + + + +
+ + ) +} diff --git a/packages/web/pages/auth/reset-password/[token].tsx b/packages/web/pages/auth/reset-password/[token].tsx new file mode 100644 index 000000000..f43e772e6 --- /dev/null +++ b/packages/web/pages/auth/reset-password/[token].tsx @@ -0,0 +1,15 @@ +import { PageMetaData } from '../../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../../components/templates/ProfileLayout' +import { EmailResetPassword } from '../../../components/templates/auth/EmailResetPassword' + +export default function EmailRegistrationPage(): JSX.Element { + return ( + <> + + + + +
+ + ) +} diff --git a/packages/web/pages/auth/reset-sent.tsx b/packages/web/pages/auth/reset-sent.tsx new file mode 100644 index 000000000..3800fc357 --- /dev/null +++ b/packages/web/pages/auth/reset-sent.tsx @@ -0,0 +1,15 @@ +import { PageMetaData } from '../../components/patterns/PageMetaData' +import { ProfileLayout } from '../../components/templates/ProfileLayout' +import { ResetSent } from '../../components/templates/auth/ResetSent' + +export default function EmailResetSent(): JSX.Element { + return ( + <> + + + + +
+ + ) +} diff --git a/packages/web/pages/auth/verify-email.tsx b/packages/web/pages/auth/verify-email.tsx new file mode 100644 index 000000000..461f62bba --- /dev/null +++ b/packages/web/pages/auth/verify-email.tsx @@ -0,0 +1,12 @@ +import { PageMetaData } from '../../components/patterns/PageMetaData' +import { VerifyEmail } from '../../components/templates/auth/VerifyEmail' + +export default function VerifyEmailPage(): JSX.Element { + return ( + <> + + +
+ + ) +} diff --git a/packages/web/pages/email-login.tsx b/packages/web/pages/email-login.tsx deleted file mode 100644 index 6011118f0..000000000 --- a/packages/web/pages/email-login.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { PageMetaData } from '../components/patterns/PageMetaData' -import { ProfileLayout } from '../components/templates/ProfileLayout' -import { EmailLogin } from '../components/templates/EmailLogin' - -export default function EmailLoginPage(): JSX.Element { - return ( - <> - - - - -
- - ) -} - -// export default function EmailLogin(): JSX.Element { -// const [errorMessage, setErrorMessage] = useState( -// undefined -// ) -// const [message, setMessage] = useState(undefined) -// const router = useRouter() - -// useEffect(() => { -// if (!router.isReady) return -// const errorCode = parseErrorCodes(router.query) -// const errorMsg = errorCode -// ? formatMessage({ id: `error.${errorCode}` }) -// : undefined -// setErrorMessage(errorMsg) - -// const message = router.query.message -// ? formatMessage({ id: `login.${router.query.message}` }) -// : undefined -// setMessage(message) -// }, [router.isReady, router.query]) - -// return ( -// -// {message && {message}} -//

Email Login

-//
-//
-// -// -//
-//
-// -// -//
-// {errorMessage && {errorMessage}} -// -//
-//
-// ) -// } diff --git a/packages/web/pages/email-reset-password.tsx b/packages/web/pages/email-reset-password.tsx deleted file mode 100644 index 88b0e4b8c..000000000 --- a/packages/web/pages/email-reset-password.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PageMetaData } from '../components/patterns/PageMetaData' -import { ProfileLayout } from '../components/templates/ProfileLayout' -import { EmailResetPassword } from '../components/templates/EmailResetPassword' - -export default function EmailRegistrationPage(): JSX.Element { - return ( - <> - - - - -
- - ) -} diff --git a/packages/web/pages/email-signup.tsx b/packages/web/pages/email-signup.tsx deleted file mode 100644 index 5fa84d37b..000000000 --- a/packages/web/pages/email-signup.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { PageMetaData } from '../components/patterns/PageMetaData' -import { ProfileLayout } from '../components/templates/ProfileLayout' -import { EmailSignup } from '../components/templates/EmailSignup' - -export default function EmailRegistrationPage(): JSX.Element { - return ( - <> - - - - -
- - ) -}