import { createCookieSessionStorage } from '@remix-run/node'; import { addMinutes } from 'date-fns'; import { Authenticator, AuthorizationError } from 'remix-auth'; import { FormStrategy } from 'remix-auth-form'; import { GoogleStrategy } from 'remix-auth-socials'; import { doesSessionExist, loginWithEmailAndPassword } from './data/zippo.server'; import { env } from './env.server'; import { UserDoesNotExistException } from './exceptions/authExeptions'; import { zxcvbn, zxcvbnOptions } from '@zxcvbn-ts/core'; import zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; import zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import { z } from 'zod'; const ZXCVBN_OPTIONS = { translations: zxcvbnEnPackage.translations, graphs: zxcvbnCommonPackage.adjacencyGraphs, useLevenshteinDistance: true, dictionary: { ...zxcvbnCommonPackage.dictionary, ...zxcvbnEnPackage.dictionary, }, } as const; zxcvbnOptions.setOptions(ZXCVBN_OPTIONS); export const sessionStorage = createCookieSessionStorage({ cookie: { name: '__session', httpOnly: true, path: '/', sameSite: 'lax', secrets: [env.SESSION_SECRET], // This should be an env variable secure: env.NODE_ENV === 'production', }, }); export type User = { id: string; email: string; sessionToken: string; expiresAt: string; teamId: string; teamName?: string; }; export const PASSWORD_MAX_STRENGTH = 4 as const; const zodEmailPasswordModel = z.object({ email: z.string().email(), password: z.string().min(8), }); export const auth = new Authenticator(sessionStorage); auth.use( new FormStrategy(async ({ form }) => { const email = form.get('email'); const password = form.get('password'); // replace the code below with your own authentication logic if (!password) throw new AuthorizationError('Password is required'); if (!email) throw new AuthorizationError('Email is required'); const parsed = zodEmailPasswordModel.safeParse({ email, password }); if (!parsed.success) { throw new AuthorizationError('Invalid credentials'); } const user = await loginWithEmailAndPassword({ email: parsed.data.email.toLowerCase(), password: parsed.data.password, }); if (user.result === 'ERROR') { throw new AuthorizationError('Invalid credentials'); } return user.data; }), 'email-pw', ); // Google OAuth2 Strategy auth.use( new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID || '', clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', callbackURL: process.env.GOOGLE_CALLBACK_URL || '', }, async ({ profile }) => { // method is stubbed out for now // If the user exists, return it if (profile?.emails[0].value === 'dennis@0xproject.com') { return { id: '1337', email: profile.emails[0].value, teamName: 'dev0x', sessionToken: '123', expiresAt: addMinutes(new Date(), 15).toISOString(), teamId: '123', }; } // If the user doesn't exist, notify the app throw new UserDoesNotExistException('User does not exist'); }, ), 'google', ); async function verifySession(user: User) { // currently stubbed, but this is where we would check with the backend to see if the session is still valid return doesSessionExist({ userId: user.id, sessionToken: user.sessionToken }); } export async function getSignedInUser(request: Request) { const user = (await auth.isAuthenticated(request)) || null; const headers = new Headers(); if (user && new Date(user.expiresAt) < new Date()) { // session has expired, we need to check with the backend if the session is still valid // if it is, we need to update the session with the new expiry date // if it isn't, we need to redirect to the login page const sessionIsValid = await verifySession(user); if (!sessionIsValid) { throw await auth.logout(request, { redirectTo: '/login' }); } // we extend the session by 15 minutes user.expiresAt = addMinutes(new Date(), 15).toISOString(); const session = await sessionStorage.getSession(request.headers.get('Cookie')); session.set(auth.sessionKey, user); headers.append('Set-Cookie', await sessionStorage.commitSession(session)); } // at this point, we know the user is authenticated and the session is valid return [user, headers] as const; } export function getPasswordStrength(password: string): [number, { suggestions: string[]; warning: string }] { // currently stubbed, but this is where we would check with the backend to see if the password is strong enough const result = zxcvbn(password); return [result.score, result.feedback]; } export async function withSignedInUser( request: Request, func: (user: User) => Promise, ): Promise { const user = await auth.isAuthenticated(request, { failureRedirect: '/', }); let userUpdated = false; if (new Date(user.expiresAt) < new Date()) { // session has expired, we need to check with the backend if the session is still valid // if it is, we need to update the session with the new expiry date // if it isn't, we need to redirect to the login page const sessionIsValid = await verifySession(user); if (!sessionIsValid) { return auth.logout(request, { redirectTo: '/login' }); } // we extend the session by 15 minutes user.expiresAt = addMinutes(new Date(), 15).toISOString(); userUpdated = true; } // at this point, we know the user is authenticated and the session is valid // we can now call the function that was passed in try { const response = await func(user); // if this request updated the session, we need to make sure we update the session cookie if (userUpdated) { const session = await sessionStorage.getSession(request.headers.get('Cookie')); session.set(auth.sessionKey, user); response.headers.append('Set-Cookie', await sessionStorage.commitSession(session)); } return response; } catch (e) { // in remix we can throw a response to immidiately abort the request // if this is the case, we need to make sure we update the session if we extended it if (e instanceof Response) { if (userUpdated) { const session = await sessionStorage.getSession(request.headers.get('Cookie')); session.set(auth.sessionKey, user); e.headers.append('Set-Cookie', await sessionStorage.commitSession(session)); } throw e; } else { // if the exception is not a response, we can return a 500 error const errorResponse = new Response('Internal Server Error', { status: 500, }); // we still need to make sure we update the session if we extended it if (userUpdated) { const session = await sessionStorage.getSession(request.headers.get('Cookie')); session.set(auth.sessionKey, user); errorResponse.headers.append('Set-Cookie', await sessionStorage.commitSession(session)); } throw errorResponse; } } }