diff --git a/components/Layouts/LayoutInternal/index.tsx b/components/Layouts/LayoutInternal/index.tsx index 45ddd1d..3b6db84 100644 --- a/components/Layouts/LayoutInternal/index.tsx +++ b/components/Layouts/LayoutInternal/index.tsx @@ -1,15 +1,17 @@ import { AppShell } from '@mantine/core' import { NotificationsProvider } from '@mantine/notifications' import { NavbarNested } from '../../Navbar/NavbarNested' +import { User } from '@supabase/supabase-js' type Props = { + user: User children?: JSX.Element | JSX.Element[] } -const LayoutInternal = ({ children }: Props) => { +const LayoutInternal = ({ user, children }: Props) => { return } + aside={} > {children} diff --git a/components/LoginSignup/index.tsx b/components/LoginSignup/index.tsx index 7d4ce20..094184b 100644 --- a/components/LoginSignup/index.tsx +++ b/components/LoginSignup/index.tsx @@ -77,12 +77,12 @@ export function LoginSignup({ variation }: LoginSignupProps) { const loginHandler = async () => { setIsLoading(true) - const data: { username: string, password: string } = { + const loginData: { username: string, password: string } = { username: email, password: password } - const info = validateFormInput(data) + const info = validateFormInput(loginData) if (info.length > 0 ){ setIsLoading(false) showNotification({ @@ -99,9 +99,11 @@ export function LoginSignup({ variation }: LoginSignupProps) { await fetchJson('/api/users/authenticate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(loginData), }) + router.push('/dashboard') + } catch (error) { setIsLoading(false) if (error instanceof FetchError) { @@ -120,13 +122,13 @@ export function LoginSignup({ variation }: LoginSignupProps) { const signupHandler = async () => { setIsLoading(true) - const data: { username: string, password: string, firstName: string, lastName: string } = { + const loginData: { username: string, password: string, firstName: string, lastName: string } = { username: email, password, firstName, lastName } - const info = validateFormInput(data) + const info = validateFormInput(loginData) if (info.length > 0 ){ setIsLoading(false) showNotification({ @@ -139,12 +141,28 @@ export function LoginSignup({ variation }: LoginSignupProps) { return } try { - await fetchJson('/api/users/signup', { + // Create the user in our DB + const { error } = await fetchJson('/api/users/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), + body: JSON.stringify(loginData), }) - router.push('/dashboard') + + + if (!error) { + router.push('/dashboard') + return + } else { + setIsLoading(false) + showNotification({ + title: 'Error', + message: error.message, + autoClose: 3000, + color: 'red', + icon: , + }) + return + } } catch (error) { setIsLoading(false) if (error instanceof FetchError) { diff --git a/components/Navbar/NavbarNested/index.tsx b/components/Navbar/NavbarNested/index.tsx index 18d446e..215951c 100644 --- a/components/Navbar/NavbarNested/index.tsx +++ b/components/Navbar/NavbarNested/index.tsx @@ -3,8 +3,8 @@ import { UserButton } from '../UserButton' import { LinksGroup } from '../NavbarLinksGroup' import { Logo } from './Logo' import { useRouter } from 'next/router' -import useUser from '../../../lib/useUser' import { NavBarItems } from '../../../types/components' +import { User } from '@supabase/supabase-js' const navbarItems: NavBarItems[] = [ { @@ -60,10 +60,9 @@ const useStyles = createStyles((theme) => ({ })) -export function NavbarNested() { +export function NavbarNested({ user }: { user: User }) { const { classes } = useStyles() const router = useRouter() - const { user } = useUser() const links = navbarItems.map((item) => ) return ( @@ -79,14 +78,15 @@ export function NavbarNested() { - {user?.firstName && + {user?.user_metadata?.first_name && } ) -} \ No newline at end of file +} + diff --git a/pages/api/square/callback.ts b/pages/api/square/callback.ts index 68e62f3..e7d378a 100644 --- a/pages/api/square/callback.ts +++ b/pages/api/square/callback.ts @@ -1,9 +1,12 @@ import { NextApiRequest, NextApiResponse } from 'next' -import { Error } from 'square' import { SquareData } from '../../../types' -import { authorizeUser, getUser, updateUser } from '../../../lib/database' -import { decodeJWT, isString } from '../../../utils/helpers' +import { isString } from '../../../utils/helpers' import {getOauthClient} from '../../../utils/oauth-client' +import createClient from '../../../utils/supabase/api' +import { encryptToken } from '../../../utils/server-helpers' +import { SCOPES } from '../../../constants' +import createAdminClient from '../../../utils/supabase/admin' + // TODO: Confirm this method handles all potential error cases gracefully export default async function handler(req: NextApiRequest, res: NextApiResponse<{ status: string } | { error: string } | Error[]>) { @@ -13,22 +16,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } else if (req.query['error']) { // Check to see if the seller clicked the 'deny' button and handle it as a special case. if (('access_denied' === req.query['error']) && ('user_denied' === req.query['error_description'])) { - const id = await decodeJWT(req) - const user = await getUser(id) - if (user) { - await updateUser({ - id, - user: { - ...user, + const supabase = createClient(req, res) + const {data: {user}} = await supabase.auth.getUser() + if (!user) { + throw new Error("user not found") + } + const adminSupabase = createAdminClient() + const { error } = await adminSupabase.auth.admin.updateUserById( + user.id, + { app_metadata: { + squareData: { userDeniedSquare: true } - }) - } - res.redirect('/settings') + } } + ) + return res.redirect('/settings') } // Display the error and description for all other errors. else { - res.status(400).json({ error: `${req.query['error']}: ${req.query['error_description']}` }) + return res.status(400).json({ error: `${req.query['error']}: ${req.query['error_description']}` }) } } // When the response_type is "code", the seller clicked Allow @@ -62,35 +68,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< merchantId } = result - // Prepare the data to be written to the database + const tokens = JSON.stringify({accessToken, refreshToken}) + const { iv, encrypted } = encryptToken(tokens) + // NOTE: We will encrypt the access and refresh tokens before storing it. const squareData: SquareData = { - tokens: JSON.stringify({ - accessToken, - refreshToken - }), + tokens: encrypted, expiresAt, - merchantId + merchantId, + iv, + scopes: SCOPES.join(','), + userDeniedSquare: false } - // grab the user id from the JWT - const id = await decodeJWT(req) - // update user object to reflect that they have authorized Square - const user = await getUser(id) - if (user?.userDeniedSquare) { - await updateUser({ - id, - user: { - ...user, - userDeniedSquare: false - } - }) - } - // Update the database with the authorized Square data - await authorizeUser({ - id, - squareData - }) + const supabase = createClient(req, res) + const {data: {user}} = await supabase.auth.getUser() + if (!user) { + throw new Error("user not found") + } + const adminSupabase = createAdminClient() + const { error } = await adminSupabase.auth.admin.updateUserById( + user.id, + { app_metadata: { + squareData + } } + ) + if (error) { + console.log('failed to update user: ', error) + } res.redirect('/dashboard') } catch (error) { // The response from the Obtain Token endpoint did not include an access token. Something went wrong. diff --git a/pages/api/users/authenticate.ts b/pages/api/users/authenticate.ts index 0b63542..7769e9a 100644 --- a/pages/api/users/authenticate.ts +++ b/pages/api/users/authenticate.ts @@ -3,6 +3,7 @@ import { NextApiUserRequest } from '../../../types' import { setCookie } from '../../../utils/cookies' import { createJWT, errorResponse, isPasswordCorrect } from '../../../utils/server-helpers' import { getUserByUsername } from '../../../lib/database' +import createClient from '../../../utils/supabase/api' export default function handler(req: NextApiUserRequest, res: NextApiResponse) { @@ -16,15 +17,17 @@ export default function handler(req: NextApiUserRequest, res: NextApiResponse) { async function authenticate() { try { const { username, password } = req.body - const user = await getUserByUsername(username) - if (!isPasswordCorrect(user?.password || '', user?.salt || '', password)) { - return res.status(404).json({ message: 'username or password is incorrect' }) - } + const supabase = createClient(req,res) - const token = await createJWT({ sub: user?.id }) - // return basic user details and token - setCookie(res, 'token', token) - return res.status(200).json({}) + const { error } = await supabase.auth.signInWithPassword({ + email: username, + password: password, + }) + if (error) { + return res.status(error?.status || 500).json({message: error.code}) + } else { + return res.status(200).json({}) + } } catch (e) { return errorResponse(res, e) } diff --git a/pages/api/users/logout.ts b/pages/api/users/logout.ts index 6470a1e..705f806 100644 --- a/pages/api/users/logout.ts +++ b/pages/api/users/logout.ts @@ -1,5 +1,6 @@ import { NextApiResponse } from 'next' import { NextApiUserRequest } from '../../../types' +import createClient from '../../../utils/supabase/api' export default function handler(req: NextApiUserRequest, res: NextApiResponse) { switch (req.method) { @@ -10,7 +11,11 @@ export default function handler(req: NextApiUserRequest, res: NextApiResponse) { } async function logout() { - res.setHeader('Set-Cookie', 'token=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT') + const supabase = createClient(req, res) + const { error } = await supabase.auth.signOut() + if (error) { + return res.status(500).json({ error: error.message }) + } res.redirect('/') } } \ No newline at end of file diff --git a/pages/api/users/signup.ts b/pages/api/users/signup.ts index f72b90f..1317e81 100644 --- a/pages/api/users/signup.ts +++ b/pages/api/users/signup.ts @@ -1,8 +1,7 @@ import { NextApiRequest, NextApiResponse } from 'next' -import { setCookie } from '../../../utils/cookies' -import { getUserByUsername, createUser } from '../../../lib/database' -import { createJWT, errorResponse, hashPassword } from '../../../utils/server-helpers' +import createClient from '../../../utils/supabase/api' import Crypto from 'crypto' +import { errorResponse } from '../../../utils/server-helpers' export default function handler(req: NextApiRequest, res: NextApiResponse) { switch (req.method) { @@ -14,32 +13,27 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { async function signup() { const { username, password, firstName, lastName } = req.body + const supabase = await createClient(req, res) try { - let user = await getUserByUsername(username) - if (user?.username) { - return res.status(409).json({ message: 'User already exists' }) - } - const { hash, salt } = await hashPassword(password) - const newUser = await createUser({ - username, - password: hash, - firstName, - lastName, - salt, - avatar: Crypto.createHash('md5').update(username).digest('hex') - }) - - // create a jwt token that is valid for 7 days - const token = await createJWT({ sub: newUser.id }) + const { data, error } = await supabase.auth.signUp({ + email: username, + password: password, + options: { + data: { + first_name: firstName, + last_name: lastName, + avatar: Crypto.createHash('md5').update(username).digest('hex') + } + } + }) // return basic user details and token - - setCookie(res, 'token', token) - // return basic user details and token - return res.status(200).json({ - id: newUser.id, - username: newUser.username, - token - }) + if (!error) { + return res.status(200).json({ + data, + }) + } else { + return res.status(error?.status || 400).json({ message: error.code }) + } } catch (e) { console.error(e) return errorResponse(res, e) diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 5d3d167..af7179a 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -6,12 +6,15 @@ import { useRouter } from 'next/router' import LayoutInternal from '../../components/Layouts/LayoutInternal' import Metrics from '../../components/Metrics' import { createStyles } from '@mantine/core' -import useUser from '../../lib/useUser' import { Arrow } from '../../components/Tasks/Task/Arrow' import { Check } from '../../components/Tasks/Task/Check' import useSWR from 'swr' import { AuthStatus } from '../../types/user' +import type { User } from '@supabase/supabase-js' +import type { GetServerSidePropsContext } from 'next' + +import { createClient } from '../../utils/supabase/server-props' const useStyles = createStyles(() => ({ header: { @@ -29,10 +32,9 @@ const useStyles = createStyles(() => ({ } })) -export default function Dashboard() { +export default function Dashboard({ user }: { user: User }) { const router = useRouter() const { classes } = useStyles() - const { user } = useUser() // if the user has data from Square const [hasSquareData, setHasSquareData] = useState(false) @@ -55,7 +57,7 @@ export default function Dashboard() { const mockTaskList: TaskProps[] = [ { - title: user?.firstName ? `Welcome to Order Hoarder, ${user?.firstName}! ${String.fromCodePoint(0x1f389)}` : 'Welcome to Order Hoarder', + title: user?.email ? `Welcome to Order Hoarder, ${user?.user_metadata?.first_name}! ${String.fromCodePoint(0x1f389)}` : 'Welcome to Order Hoarder', description: 'In order to start using Order Hoarder, we need data from Square', color: '#151C1F', }, @@ -91,7 +93,7 @@ export default function Dashboard() { } return ( - +

Dashboard

{isLoading ?
: } @@ -99,3 +101,24 @@ export default function Dashboard() {
) } + +export async function getServerSideProps(context: GetServerSidePropsContext) { + const supabase = createClient(context) + + const { data, error } = await supabase.auth.getUser() + + if (error || !data) { + return { + redirect: { + destination: '/', + permanent: false, + }, + } + } + + return { + props: { + user: data.user, + }, + } + } diff --git a/utils/supabase/admin.ts b/utils/supabase/admin.ts new file mode 100644 index 0000000..244d6ca --- /dev/null +++ b/utils/supabase/admin.ts @@ -0,0 +1,16 @@ +import { createClient } from '@supabase/supabase-js' + +export default function createAdminClient() { + const supabase = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } + ) + + return supabase +} \ No newline at end of file diff --git a/utils/supabase/api.ts b/utils/supabase/api.ts new file mode 100644 index 0000000..bf298e5 --- /dev/null +++ b/utils/supabase/api.ts @@ -0,0 +1,26 @@ +import { type NextApiRequest, type NextApiResponse } from 'next' +import { createServerClient, serializeCookieHeader } from '@supabase/ssr' + +export default function createClient(req: NextApiRequest, res: NextApiResponse) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return Object.keys(req.cookies).map((name) => ({ name, value: req.cookies[name] || '' })) + }, + setAll(cookiesToSet) { + res.setHeader( + 'Set-Cookie', + cookiesToSet.map(({ name, value, options }) => + serializeCookieHeader(name, value, options) + ) + ) + }, + }, + } + ) + + return supabase +} \ No newline at end of file diff --git a/utils/supabase/server-props.ts b/utils/supabase/server-props.ts new file mode 100644 index 0000000..8721463 --- /dev/null +++ b/utils/supabase/server-props.ts @@ -0,0 +1,26 @@ +import { type GetServerSidePropsContext } from 'next' +import { createServerClient, serializeCookieHeader } from '@supabase/ssr' + +export function createClient({ req, res }: GetServerSidePropsContext) { + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return Object.keys(req.cookies).map((name) => ({ name, value: req.cookies[name] || '' })) + }, + setAll(cookiesToSet) { + res.setHeader( + 'Set-Cookie', + cookiesToSet.map(({ name, value, options }) => + serializeCookieHeader(name, value, options) + ) + ) + }, + }, + } + ) + + return supabase +} \ No newline at end of file