diff --git a/package.json b/package.json index f0325298..06001f0a 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,7 @@ "moment": "^2.29.4", "nanoid": "^5.0.7", "next": "^14.2.5", + "next-auth": "5.0.0-beta.22", "next-mdx-remote": "^4.4.1", "next-nprogress-bar": "^2.1.2", "next-themes": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fb9f8cd..9b2e7d5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -377,6 +377,9 @@ importers: next: specifier: ^14.2.5 version: 14.2.5(@babel/core@7.24.9)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-auth: + specifier: 5.0.0-beta.22 + version: 5.0.0-beta.22(next@14.2.5(@babel/core@7.24.9)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1) next-mdx-remote: specifier: ^4.4.1 version: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -792,6 +795,20 @@ packages: resolution: {integrity: sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==} hasBin: true + '@auth/core@0.35.3': + resolution: {integrity: sha512-g6qfiqU4OtyvIEZ8J7UoIwAxEnNnLJV0/f/DW41U+4G5nhBlaCrnKhawJIJpU0D3uavXLeDT3B0BkjtiimvMDA==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + nodemailer: ^6.8.0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -2110,6 +2127,9 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3714,6 +3734,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/d3-array@3.2.1': resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} @@ -4765,6 +4788,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -6289,6 +6316,9 @@ packages: jose@4.15.9: resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@5.9.4: + resolution: {integrity: sha512-WBBl6au1qg6OHj67yCffCgFR3BADJBXN8MdRvCgJDuMv3driV2nHr7jdGvaKX9IolosAsn+M0XRArqLXUhyJHQ==} + js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -7164,6 +7194,22 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-auth@5.0.0-beta.22: + resolution: {integrity: sha512-QGBo9HGOjmnJBHGXvtFztl0tM5tL0porDlk74HVoCCzXd986ApOlIW3EmiCuho7YzEopgkFiwwmcXpoCrHAtYw==} + peerDependencies: + '@simplewebauthn/browser': ^9.0.1 + '@simplewebauthn/server': ^9.0.2 + next: ^14.0.0-0 || ^15.0.0-0 + nodemailer: ^6.6.5 + react: ^18.2.0 || ^19.0.0-0 + peerDependenciesMeta: + '@simplewebauthn/browser': + optional: true + '@simplewebauthn/server': + optional: true + nodemailer: + optional: true + next-mdx-remote@4.4.1: resolution: {integrity: sha512-1BvyXaIou6xy3XoNF4yaMZUCb6vD2GTAa5ciOa6WoO+gAUTYsb1K4rI/HSC2ogAWLrb/7VSV52skz07vOzmqIQ==} engines: {node: '>=14', npm: '>=7'} @@ -7301,6 +7347,9 @@ packages: oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + oauth4webapi@2.17.0: + resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} + ob1@0.80.9: resolution: {integrity: sha512-v9yOxowkZbxWhKOaaTyLjIm1aLy4ebMNcSn4NYJKOAI/Qv+SkfEfszpLr2GIxsccmb2Y2HA9qtsqiIJ80ucpVA==} engines: {node: '>=18'} @@ -7695,6 +7744,14 @@ packages: resolution: {integrity: sha512-rtqm2h22QxLGBrW2bLYzbRhliIrqgZ0k+gF0LkQ1SNdeD06YE5eilV0MxZppFSxC8TfH0+B0cWCuebEnreIDgQ==} engines: {node: '>=15.0.0'} + preact-render-to-string@5.2.3: + resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} + peerDependencies: + preact: '>=10' + + preact@10.11.3: + resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} + preact@10.22.1: resolution: {integrity: sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==} @@ -7723,6 +7780,9 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@3.8.0: + resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + pretty-hrtime@1.0.3: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} @@ -9685,6 +9745,18 @@ snapshots: '@antfu/ni@0.21.12': {} + '@auth/core@0.35.3(nodemailer@6.9.14)': + dependencies: + '@panva/hkdf': 1.2.1 + '@types/cookie': 0.6.0 + cookie: 0.6.0 + jose: 5.9.4 + oauth4webapi: 2.17.0 + preact: 10.11.3 + preact-render-to-string: 5.2.3(preact@10.11.3) + optionalDependencies: + nodemailer: 6.9.14 + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -11180,6 +11252,8 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@panva/hkdf@1.2.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -13084,6 +13158,8 @@ snapshots: dependencies: '@babel/types': 7.24.9 + '@types/cookie@0.6.0': {} + '@types/d3-array@3.2.1': {} '@types/d3-color@3.1.3': {} @@ -14291,6 +14367,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.6.0: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -16031,6 +16109,8 @@ snapshots: jose@4.15.9: {} + jose@5.9.4: {} + js-beautify@1.15.1: dependencies: config-chain: 1.1.13 @@ -17552,6 +17632,14 @@ snapshots: neo-async@2.6.2: {} + next-auth@5.0.0-beta.22(next@14.2.5(@babel/core@7.24.9)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.14)(react@18.3.1): + dependencies: + '@auth/core': 0.35.3(nodemailer@6.9.14) + next: 14.2.5(@babel/core@7.24.9)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + optionalDependencies: + nodemailer: 6.9.14 + next-mdx-remote@4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@mdx-js/mdx': 2.3.0 @@ -17678,6 +17766,8 @@ snapshots: oauth-sign@0.9.0: {} + oauth4webapi@2.17.0: {} + ob1@0.80.9: {} object-assign@4.1.1: {} @@ -18071,6 +18161,13 @@ snapshots: transitivePeerDependencies: - debug + preact-render-to-string@5.2.3(preact@10.11.3): + dependencies: + preact: 10.11.3 + pretty-format: 3.8.0 + + preact@10.11.3: {} + preact@10.22.1: {} prelude-ls@1.2.1: {} @@ -18100,6 +18197,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-format@3.8.0: {} + pretty-hrtime@1.0.3: {} prisma@5.20.0: diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 00000000..0a98352f --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from '@/auth'; + +export const { GET, POST } = handlers; diff --git a/src/app/route.ts b/src/app/route.ts index 778f1a41..b28cd2f8 100644 --- a/src/app/route.ts +++ b/src/app/route.ts @@ -1,8 +1,15 @@ -import { createSupabaseUserRouteHandlerClient } from '@/supabase-clients/user/createSupabaseUserRouteHandlerClient'; -import { NextResponse } from 'next/server'; +//import { createSupabaseUserRouteHandlerClient } from '@/supabase-clients/user/createSupabaseUserRouteHandlerClient'; + +import { redirect } from 'next/navigation'; export const dynamic = 'force-dynamic'; +export async function GET() { + redirect('/dashboard'); +} + +// TODO remove when move to authjs is complete +/* export async function GET() { const supabase = createSupabaseUserRouteHandlerClient(); @@ -31,3 +38,4 @@ export async function GET() { ); } } +*/ diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 00000000..2bae1518 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,13 @@ +import NextAuth from 'next-auth'; +import Auth0 from 'next-auth/providers/auth0'; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + //providers: [WorkOS({ connection: 'conn_01HVH5N4RFQVD9DH5QWGYT844V' })], + providers: [Auth0], + callbacks: { + authorized: async ({ auth }) => { + // Logged in users are authenticated, otherwise redirect to login page + return !!auth; + }, + }, +}); diff --git a/src/data/user/organizations.ts b/src/data/user/organizations.ts index 24f48ad0..1ff011ee 100644 --- a/src/data/user/organizations.ts +++ b/src/data/user/organizations.ts @@ -103,6 +103,7 @@ export const createOrganization = async ( return { status: 'error', message: orgMemberErrors.message }; } + // Why are we checking for onboarding deep in the data layer? Bad code. if (isOnboardingFlow) { const { error: updateError } = await supabaseClient .from('user_private_info') @@ -621,7 +622,9 @@ export async function getInitialOrganizationToRedirectTo(): Promise< }; } -export async function getMaybeInitialOrganizationToRedirectTo(): Promise> { +export async function getMaybeInitialOrganizationToRedirectTo(): Promise< + SAPayload +> { const initialOrganization = await getInitialOrganizationToRedirectTo(); if (initialOrganization.status === 'error') { return { diff --git a/src/data/user/projects.tsx b/src/data/user/projects.tsx index 07c1f0d6..4e826d00 100644 --- a/src/data/user/projects.tsx +++ b/src/data/user/projects.tsx @@ -196,7 +196,7 @@ export const createProjectCommentAction = async ( const user = await serverGetLoggedInUser(); const { data, error } = await supabaseClient .from("project_comments") - .insert({ project_id: projectId, text, user_id: user.id }) + .insert({ project_id: projectId, text, user_id: user.id! }) //TODO remove assertion or resolve .select("*, user_profiles(*)") .single(); if (error) { diff --git a/src/data/user/session.ts b/src/data/user/session.ts index 7852ae71..a641e6c7 100644 --- a/src/data/user/session.ts +++ b/src/data/user/session.ts @@ -1,9 +1,9 @@ -"use server" +'use server'; -import { createSupabaseUserServerActionClient } from "@/supabase-clients/user/createSupabaseUserServerActionClient"; -import { SAPayload } from "@/types"; +import { SAPayload } from '@/types'; export async function refreshSessionAction(): Promise { + /* const supabaseClient = createSupabaseUserServerActionClient(); const refreshSessionResponse = await supabaseClient.auth.refreshSession(); @@ -13,6 +13,9 @@ export async function refreshSessionAction(): Promise { message: refreshSessionResponse.error.message, }; } + */ + + //TODO re-implement with Auth.js or remove return { status: 'success', diff --git a/src/middleware.ts b/src/middleware.ts index 9f420f02..58cacbac 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,14 +1,9 @@ -import { - createMiddlewareClient, - type User, -} from '@supabase/auth-helpers-nextjs'; -import type { NextRequest } from 'next/server'; -import { NextResponse } from 'next/server'; // const matchAppAdmin = match('/app_admin_preview/(.*)?'); +import { auth } from '@/auth'; +import { NextResponse } from 'next/server'; import { match } from 'path-to-regexp'; -import type { Database } from './lib/database.types'; import { toSiteURL } from './utils/helpers'; -import { authUserMetadataSchema } from './utils/zod-schemas/authUserMetadata'; +import { serverGetLoggedInUser } from './utils/server/serverGetLoggedInUser'; const onboardingPaths = `/onboarding/(.*)?`; // Using a middleware to protect pages from unauthorized access @@ -48,7 +43,8 @@ function isUnprotectedPage(pathname: string) { }); } -function shouldOnboardUser(pathname: string, user: User | undefined) { +function shouldOnboardUser(pathname: string, userId: string) { + /* const matchOnboarding = match(onboardingPaths); const isOnboardingRoute = matchOnboarding(pathname); if (!isUnprotectedPage(pathname) && user && !isOnboardingRoute) { @@ -69,11 +65,17 @@ function shouldOnboardUser(pathname: string, user: User | undefined) { } console.log('user is onboarded'); return false; + */ + return true; + //TODO figure way to store user metadata (extend user_profile table?) } // this middleware refreshes the user's session and must be run // for any Server Component route that uses `createServerComponentSupabaseClient` -export async function middleware(req: NextRequest) { +// renamed while moving to auth.js + +/* +export async function middleware_NEXTBASE_LEGACY(req: NextRequest) { const res = NextResponse.next(); const supabase = createMiddlewareClient({ req, res }); const sessionResponse = await supabase.auth.getSession(); @@ -121,6 +123,17 @@ export async function middleware(req: NextRequest) { return res; } +*/ + +export default auth(async (req) => { + const user = await serverGetLoggedInUser(); + if (shouldOnboardUser(req.nextUrl.pathname, user.id)) { + return NextResponse.redirect(toSiteURL('/onboarding')); + } else { + return NextResponse.next(); + } +}); + export const config = { matcher: [ /* diff --git a/src/utils/server/serverGetLoggedInUser.ts b/src/utils/server/serverGetLoggedInUser.ts index e3c9c00f..50a8dccb 100644 --- a/src/utils/server/serverGetLoggedInUser.ts +++ b/src/utils/server/serverGetLoggedInUser.ts @@ -1,20 +1,19 @@ 'use server'; -import { cache } from 'react'; -import { getSession } from './verifySession'; -export const serverGetLoggedInUser = cache(async () => { - const { - data: { session }, - error: sessionError, - } = await getSession() +import { AuthUser, getSession } from './verifySession'; - if (sessionError) { - throw sessionError; +//TODO reintroduce cache; used to be cache from react +// removed because it was failing: "(0 , react__WEBPACK_IMPORTED_MODULE_2__.cache) is not a function" +export const serverGetLoggedInUser = async () => { + const session = await getSession(); + + if (!session) { + throw new Error('serverGetLoggedInUser: No session'); } if (!session?.user) { throw new Error('serverGetLoggedInUser: Not logged in'); } - return session.user; -}); + return session.user as AuthUser; +}; diff --git a/src/utils/server/serverGetLoggedInUserRole.ts b/src/utils/server/serverGetLoggedInUserRole.ts index 705e8dea..f2dbcbd1 100644 --- a/src/utils/server/serverGetLoggedInUserRole.ts +++ b/src/utils/server/serverGetLoggedInUserRole.ts @@ -1,5 +1,4 @@ 'use server'; -import { cache } from 'react'; import { serverGetLoggedInUser } from './serverGetLoggedInUser'; type UserRole = 'admin' | 'user'; @@ -9,11 +8,11 @@ type UserRole = 'admin' | 'user'; * You can use this to determine if the user is an admin or a regular user. * Based on this value you can show or hide certain UI elements. */ -export const serverGetLoggedInUserRole = cache(async () => { +export const serverGetLoggedInUserRole = async () => { const user = await serverGetLoggedInUser(); if ('user_role' in user) { return user.user_role as UserRole; } else { return 'user' as UserRole; } -}); +}; diff --git a/src/utils/server/serverGetUserType.ts b/src/utils/server/serverGetUserType.ts index 550e4028..31c89470 100644 --- a/src/utils/server/serverGetUserType.ts +++ b/src/utils/server/serverGetUserType.ts @@ -1,10 +1,12 @@ 'use server'; import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; import { userRoles } from '@/utils/userTypes'; -import { cache } from 'react'; // make sure to return one of UserRoles -export const serverGetUserType = cache(async () => { + +//TODO reintroduce cache; used to be cache from react +// removed because it was failing: "(0 , react__WEBPACK_IMPORTED_MODULE_2__.cache) is not a function" +export const serverGetUserType = async () => { const supabase = createSupabaseUserServerComponentClient(); const { data: { session }, @@ -27,5 +29,4 @@ export const serverGetUserType = cache(async () => { } return userRoles.USER; -} -) +}; diff --git a/src/utils/server/verifySession.ts b/src/utils/server/verifySession.ts index 4cd2bf13..44a9f5c5 100644 --- a/src/utils/server/verifySession.ts +++ b/src/utils/server/verifySession.ts @@ -1,18 +1,29 @@ 'use server'; -import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; + +import { auth } from '@/auth'; import { redirect } from 'next/navigation'; -import { cache } from 'react'; -export const getSession = cache(async () => { - const supabase = createSupabaseUserServerComponentClient(); - return await supabase.auth.getSession(); -}); +export interface AuthUser { + id: string; + email: string; + name: string; +} + +//TODO reintroduce cache; used to be cache from react +// removed because it was failing: "(0 , react__WEBPACK_IMPORTED_MODULE_2__.cache) is not a function" +export const getSession = async () => { + //const supabase = createSupabaseUserServerComponentClient(); + //return await supabase.auth.getSession(); + const session = await auth(); + return session; +}; -// This is a server-side function that verifies the session of the user. -// and runs in server components. -export const verifySession = cache(async () => { +//TODO reintroduce cache; used to be cache from react +// removed because it was failing: "(0 , react__WEBPACK_IMPORTED_MODULE_2__.cache) is not a function" +export const verifySession = async () => { + /* const { - data: { session }, + user: { session }, error: sessionError, } = await getSession(); @@ -25,4 +36,10 @@ export const verifySession = cache(async () => { } return session.user; -}); + */ + const session = await auth(); + if (!session?.user) { + redirect('/login'); //TODO update route - there's no longer embedded login pager. preserving as-is for now + } + return session?.user as AuthUser; +};