Files
protocol/sites/dev0x-portal/app/auth.server.ts
2023-04-06 20:56:53 +02:00

212 lines
7.6 KiB
TypeScript

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<User>(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<R extends Response>(
request: Request,
func: (user: User) => Promise<R>,
): Promise<R> {
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;
}
}
}