diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ActiveApiKeyList.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ActiveApiKeyList.tsx deleted file mode 100644 index d1b53436..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ActiveApiKeyList.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { PageHeading } from '@/components/PageHeading'; -import { T } from '@/components/ui/Typography'; -import { - Table as ShadcnTable, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { getActiveDeveloperKeys } from '@/data/user/unkey'; -import { format } from 'date-fns'; -import moment from 'moment'; -import { ConfirmRevokeTokenDialog } from './ConfirmRevokeTokenDialog'; - -export async function ActiveApiKeyList() { - const activeDeveloperKeys = await getActiveDeveloperKeys(); - const heading = ( - - ); - - if (activeDeveloperKeys.length) { - return ( -
- {heading} - - - - API Key - Generated On - Expires In - Actions - - - - {activeDeveloperKeys.map((apiKey) => { - return ( - - - {apiKey.masked_key} - - - {format(new Date(apiKey.created_at), 'PPP')} - - - - {apiKey.expires_at - ? moment(apiKey.expires_at).format('LL') - : 'No expiry'} - - - - - - ); - })} - - -
- ); - } else { - return ( -
- {heading} - No active API keys. -
- ); - } -} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ConfirmRevokeTokenDialog.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ConfirmRevokeTokenDialog.tsx deleted file mode 100644 index 357f43a7..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ConfirmRevokeTokenDialog.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@/components/ui/dialog'; -import { revokeUnkeyToken } from '@/data/user/unkey'; -import { useSAToastMutation } from '@/hooks/useSAToastMutation'; -import { useState } from 'react'; - -type Props = { - keyId: string; -}; - -export const ConfirmRevokeTokenDialog = ({ keyId }: Props) => { - const [open, setOpen] = useState(false); - const { mutate, isLoading } = useSAToastMutation(async (keyId: string) => { - return await revokeUnkeyToken(keyId); - }, { - onSettled: () => { - setOpen(false); - }, - loadingMessage: 'Revoking API Key...', - successMessage: 'API Key revoked!', - errorMessage(error) { - try { - if (error instanceof Error) { - return String(error.message); - } - return `Failed to revoke API Key ${String(error)}`; - } catch (_err) { - console.warn(_err); - return 'Failed to revoke API Key'; - } - }, - }); - - return ( - - - - - -
{ - event.preventDefault(); - mutate(keyId); - }} - > - - Revoke Token - - Are you sure you want to revoke this token? This action is - irreversible. - - - - - - - -
-
-
- ); -}; diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/GenerateApiKey.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/GenerateApiKey.tsx deleted file mode 100644 index 58590d7a..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/GenerateApiKey.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; - -import { generateUnkeyToken } from '@/data/user/unkey'; -import { useSAToastMutation } from '@/hooks/useSAToastMutation'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { ViewApiKeyDialog } from './ViewApiKeyDialog'; - -export function GenerateApiKey() { - const router = useRouter(); - const { mutate, isLoading } = useSAToastMutation(async () => { - return await generateUnkeyToken(); - }, { - onSuccess: (response) => { - if (response.status === 'success' && response.data) { - setStep('copy_modal'); - setKeyPreview(response.data.key); - } - }, - errorMessage(error) { - try { - if (error instanceof Error) { - return String(error.message); - } - return `Failed to generate API Key ${String(error)}`; - } catch (_err) { - console.warn(_err); - return 'Failed to generate API Key'; - } - }, - }); - - const [step, setStep] = useState<'form' | 'copy_modal' | 'complete'>('form'); - const [keyPreview, setKeyPreview] = useState(null); - - return ( - <> -
{ - event.preventDefault(); - mutate(); - }} - > -
- -
-
- - {step === 'copy_modal' && keyPreview ? ( - { - setStep('complete'); - router.refresh(); - }} - apiKey={keyPreview} - /> - ) : null} - - ); -} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/RevokedApiKeyList.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/RevokedApiKeyList.tsx deleted file mode 100644 index fa30b879..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/RevokedApiKeyList.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { PageHeading } from '@/components/PageHeading'; -import { - Table as ShadcnTable, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { getRevokedApiKeyList } from '@/data/user/unkey'; -import { format } from 'date-fns'; - -export async function RevokedApiKeyList() { - const revokedApiKeyList = await getRevokedApiKeyList(); - - if (!revokedApiKeyList.length) { - return

No revoked keys

; - } - - const heading = ( - - ); - - return ( -
- {heading} -
- - - - API Key - Generated On - Status - - - - {revokedApiKeyList.map((apiKey) => { - return ( - - - {apiKey.masked_key} - - - {format(new Date(apiKey.created_at), 'PPP')} - - Revoked - - ); - })} - - -
-
- ); -} diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ViewApiKeyDialog.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ViewApiKeyDialog.tsx deleted file mode 100644 index 9b59390f..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/ViewApiKeyDialog.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Copy, CopyCheck } from 'lucide-react'; -import { useState } from 'react'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; - -type Props = { - apiKey: string; - onCompleted: () => void; -}; - -export const ViewApiKeyDialog = ({ apiKey, onCompleted }: Props) => { - const [open, setOpen] = useState(true); - const [isCopied, setIsCopied] = useState(false); - - return ( - - - - API Key - - This key will never be displayed again. Please store it in a safe - place. - - -
- setIsCopied(true)}> -
- - {isCopied ? : } -
-
- {isCopied && Copied to clipboard!} -
- - - -
-
- ); -}; diff --git a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/page.tsx b/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/page.tsx deleted file mode 100644 index d51a11d6..00000000 --- a/src/app/(dynamic-pages)/(authenticated-pages)/(user-pages)/settings/developer/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { PageHeading } from '@/components/PageHeading'; -import { T } from '@/components/ui/Typography'; -import { Skeleton } from '@/components/ui/skeleton'; -import { getActiveDeveloperKeyCount } from '@/data/user/unkey'; -import { Suspense } from 'react'; -import { ActiveApiKeyList } from './ActiveApiKeyList'; -import { GenerateApiKey } from './GenerateApiKey'; -import { RevokedApiKeyList } from './RevokedApiKeyList'; - -export default async function DeveloperSettings() { - const activeDeveloperKeyCount = await getActiveDeveloperKeyCount(); - return ( -
- -
- }> - - - {activeDeveloperKeyCount < 3 ? ( - - ) : ( - You have reached API Key Limit. - )} -
- }> - - -
- ); -} diff --git a/src/app/api/invitations/view/[invitationId]/route.ts b/src/app/api/invitations/view/[invitationId]/route.ts index d4ff7d88..3addc7a6 100644 --- a/src/app/api/invitations/view/[invitationId]/route.ts +++ b/src/app/api/invitations/view/[invitationId]/route.ts @@ -1,5 +1,5 @@ -import { createSupabaseUserRouteHandlerClient } from '@/supabase-clients/user/createSupabaseUserRouteHandlerClient'; import { toSiteURL } from '@/utils/helpers'; +import { serverGetLoggedInUser } from '@/utils/server/serverGetLoggedInUser'; import { redirect } from 'next/navigation'; import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; @@ -24,12 +24,10 @@ export async function GET( } const { invitationId } = paramsSchema.parse(params); - const supabaseClient = createSupabaseUserRouteHandlerClient(); - const { data, error } = await supabaseClient.auth.getSession(); - if (error) { - throw error; - } - const user = data?.session?.user; + const user = await serverGetLoggedInUser(); + + // the below still won't work but at least we got rid of supabase user client on the server + // login form no longer exists so need to redirect to actual login but later if (!user) { const url = new URL(toSiteURL('/login')); diff --git a/src/app/api/token/refresh/route.tsx b/src/app/api/token/refresh/route.tsx deleted file mode 100644 index 8a985854..00000000 --- a/src/app/api/token/refresh/route.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { createSupabaseUserRouteHandlerClient } from '@/supabase-clients/user/createSupabaseUserRouteHandlerClient'; - -export const dynamic = 'force-dynamic'; - -export async function GET() { - const supabase = createSupabaseUserRouteHandlerClient(); - const { data, error } = await supabase.auth.getSession(); - if (error) { - return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); - } - if (!data?.session?.user) { - return new Response(JSON.stringify({ error: 'Not logged in' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); - } if (!data.session.refresh_token) { - return new Response(JSON.stringify({ error: 'No refresh token' }), { status: 401, headers: { 'Content-Type': 'application/json' } }); - } - - await supabase.auth.refreshSession({ - refresh_token: data.session?.refresh_token, - }); - - return new Response(JSON.stringify({ message: 'Refreshed' }), { headers: { 'Content-Type': 'application/json' } }); -} diff --git a/src/app/api/v1/me/Readme.md b/src/app/api/v1/me/Readme.md deleted file mode 100644 index 0d74d0eb..00000000 --- a/src/app/api/v1/me/Readme.md +++ /dev/null @@ -1 +0,0 @@ -This is a public facing API that can be used to perform actions on behalf of a user by using Unkey. diff --git a/src/app/api/v1/me/route.ts b/src/app/api/v1/me/route.ts deleted file mode 100644 index 1f14e191..00000000 --- a/src/app/api/v1/me/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createSupabaseUnkeyClient } from '@/supabase-clients/unkey/createSupabaseUnkeyClient'; -import { NextRequest, NextResponse } from 'next/server'; - -export async function GET(req: NextRequest) { - try { - const supabaseClient = await createSupabaseUnkeyClient(req); - const { data, error: userError } = await supabaseClient.auth.getUser(); - - if (userError) { - return NextResponse.json({ error: userError.message }, { status: 500 }); - } - - const { user } = data; - - if (!user) { - return NextResponse.json({ error: 'User not found' }, { status: 404 }); - } - - const responseHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }; - - return NextResponse.json(user, { - headers: responseHeaders, - }); - } catch (error) { - return new NextResponse(String(error), { status: 500 }); - } -} diff --git a/src/data/user/unkey.ts b/src/data/user/unkey.ts deleted file mode 100644 index 899eb131..00000000 --- a/src/data/user/unkey.ts +++ /dev/null @@ -1,146 +0,0 @@ -'use server'; -import { createSupabaseUserRouteHandlerClient } from '@/supabase-clients/user/createSupabaseUserRouteHandlerClient'; -import { createSupabaseUserServerActionClient } from '@/supabase-clients/user/createSupabaseUserServerActionClient'; -import { createSupabaseUserServerComponentClient } from '@/supabase-clients/user/createSupabaseUserServerComponentClient'; -import type { SAPayload } from '@/types'; -import { serverGetLoggedInUser } from '@/utils/server/serverGetLoggedInUser'; -import axios from 'axios'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; - -const generateKeyResponseSchema = z.object({ - keyId: z.string(), - key: z.string(), -}); - -function maskKey(key: string): string { - const start = key.substr(0, 3); - const end = key.substr(-3); - const masked = '*'.repeat(key.length - 6); - return start + masked + end; -} - -export async function generateUnkeyToken(): Promise< - SAPayload<{ keyId: string; key: string; createdAt: string }> -> { - const user = await serverGetLoggedInUser(); - const supabaseClient = createSupabaseUserRouteHandlerClient(); - const response = await axios.post( - 'https://api.unkey.dev/v1/keys', - { - apiId: process.env.UNKEY_API_ID, - ownerId: user.id, - prefix: 'st_', - }, - { - headers: { - Authorization: `Bearer ${process.env.UNKEY_ROOT_KEY}`, - 'Content-Type': 'application/json', - }, - }, - ); - const { keyId, key } = generateKeyResponseSchema.parse(response.data); - const { data: insertKeyResponse, error: insertKeyError } = - await supabaseClient - .from('user_api_keys') - .insert({ - key_id: keyId, - masked_key: maskKey(key), - user_id: user.id, - }) - .select('*') - .single(); - - if (insertKeyError) { - return { status: 'error', message: insertKeyError.message }; - } - - return { - status: 'success', - data: { - keyId, - key, - createdAt: insertKeyResponse.created_at, - }, - }; -} - -export async function revokeUnkeyToken( - keyId: string, -): Promise> { - const response = await axios.delete( - `https://api.unkey.dev/v1/keys/${keyId}`, - { - headers: { - Authorization: `Bearer ${process.env.UNKEY_ROOT_KEY}`, - }, - }, - ); - - const supabaseClient = createSupabaseUserServerActionClient(); - - const { error } = await supabaseClient - .from('user_api_keys') - .update({ - is_revoked: true, - }) - .eq('key_id', keyId) - .single(); - - if (error) { - return { status: 'error', message: error.message }; - } - - revalidatePath('/', 'layout'); - - return { status: 'success', data: { ok: true } }; -} - -export const getActiveDeveloperKeys = async () => { - const user = await serverGetLoggedInUser(); - const supabaseClient = createSupabaseUserServerComponentClient(); - - const { data, error } = await supabaseClient - .from('user_api_keys') - .select('*') - .eq('user_id', user.id) - .eq('is_revoked', false); - - if (error) { - throw error; - } - return data; -}; - -export const getActiveDeveloperKeyCount = async () => { - const user = await serverGetLoggedInUser(); - const supabaseClient = createSupabaseUserServerComponentClient(); - - const { count, error } = await supabaseClient - .from('user_api_keys') - .select('*', { count: 'exact', head: true }) - .eq('user_id', user.id) - .eq('is_revoked', false); - - if (error) { - console.log(error); - throw error; - } - return count ?? 0; -}; - -export const getRevokedApiKeyList = async () => { - const supabaseClient = createSupabaseUserServerComponentClient(); - const user = await serverGetLoggedInUser(); - - const { data, error } = await supabaseClient - .from('user_api_keys') - .select('*') - .eq('user_id', user.id) - .eq('is_revoked', true); - - if (error) { - throw error; - } - return data; -}; diff --git a/src/supabase-clients/unkey/createSupabaseUnkeyClient.ts b/src/supabase-clients/unkey/createSupabaseUnkeyClient.ts deleted file mode 100644 index 1b744241..00000000 --- a/src/supabase-clients/unkey/createSupabaseUnkeyClient.ts +++ /dev/null @@ -1,60 +0,0 @@ -'use server'; - -import { Database } from '@/lib/database.types'; -import { createClient } from '@supabase/supabase-js'; -import { verifyKey } from '@unkey/api'; -import jwt from 'jsonwebtoken'; -import { NextRequest } from 'next/server'; -import { z } from 'zod'; - -function createJWT(userId: string) { - const payload = { - sub: userId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour expiry - role: 'authenticated', - aud: 'authenticated', - iss: 'https://ultimate-demo.usenextbase.com', - iat: Math.floor(Date.now() / 1000) - 60, - }; - - const token = jwt.sign(payload, process.env.SUPABASE_JWT_SECRET); - return token; -} - -const resultSchema = z.object({ - ownerId: z.string(), - valid: z.boolean(), - expires: z.number().optional(), -}); - -export async function createSupabaseUnkeyClient(req: NextRequest) { - const authHeader = req.headers.get('Authorization'); - - if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new Error('Missing or invalid Authorization header'); - } - - const token = authHeader.split(' ')[1]; - - const { result, error } = await verifyKey(token); - if (error) { - throw error; - } - - const { ownerId: userId } = resultSchema.parse(result); - - const jwt = createJWT(userId); - const client = createClient( - process.env.NEXT_PUBLIC_SUPABASE_URL, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, - { - global: { - headers: { - Authorization: 'Bearer ' + jwt, - }, - }, - }, - ); - - return client; -} diff --git a/src/supabase-clients/user/createSupabaseUserRouteHandlerClient.ts b/src/supabase-clients/user/createSupabaseUserRouteHandlerClient.ts deleted file mode 100644 index 2a2d1dcc..00000000 --- a/src/supabase-clients/user/createSupabaseUserRouteHandlerClient.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Database } from '@/lib/database.types'; -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; -import { cookies } from 'next/headers'; - -type createRouteHandlerClientParams = NonNullable< - Parameters[1] ->; -type CookieOptions = createRouteHandlerClientParams['cookieOptions']; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -const optionalCookieOptions: CookieOptions = { - domain: isDevelopment ? undefined : '.digger.dev', - secure: !isDevelopment, - path: '/', - sameSite: 'lax', -}; - -// Outstanding bug -//https://github.com/vercel/next.js/issues/45371 -export const createSupabaseUserRouteHandlerClient = () => - createRouteHandlerClient( - { - cookies, - }, - { - cookieOptions: optionalCookieOptions, - }, - ); diff --git a/src/supabase-clients/user/createSupabaseUserServerActionClient.ts b/src/supabase-clients/user/createSupabaseUserServerActionClient.ts index 53453278..f9bf8ace 100644 --- a/src/supabase-clients/user/createSupabaseUserServerActionClient.ts +++ b/src/supabase-clients/user/createSupabaseUserServerActionClient.ts @@ -1,27 +1,6 @@ -import { Database } from '@/lib/database.types'; -import { createServerActionClient } from '@supabase/auth-helpers-nextjs'; -import { cookies } from 'next/headers'; +import { supabaseAdminClient } from '../admin/supabaseAdminClient'; -type createServerActionClientParams = NonNullable< - Parameters[1] ->; -type CookieOptions = createServerActionClientParams['cookieOptions']; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -const optionalCookieOptions: CookieOptions = { - domain: isDevelopment ? undefined : '.digger.dev', - secure: !isDevelopment, - path: '/', - sameSite: 'lax', -}; - -export const createSupabaseUserServerActionClient = () => - createServerActionClient( - { - cookies, - }, - { - cookieOptions: optionalCookieOptions, - }, - ); +// this used to be a user-scoped client +// switching to admin because we no longer use supabase auth +// it's only used on the server so security ok +export const createSupabaseUserServerActionClient = () => supabaseAdminClient; diff --git a/src/supabase-clients/user/createSupabaseUserServerComponentClient.ts b/src/supabase-clients/user/createSupabaseUserServerComponentClient.ts index 83df9eee..b7458a82 100644 --- a/src/supabase-clients/user/createSupabaseUserServerComponentClient.ts +++ b/src/supabase-clients/user/createSupabaseUserServerComponentClient.ts @@ -1,32 +1,7 @@ -import { Database } from '@/lib/database.types'; -import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; -import { cookies } from 'next/headers'; - -const isDevelopment = process.env.NODE_ENV === 'development'; - -type createServerComponentClientParams = NonNullable< - Parameters[1] ->; -type CookieOptions = createServerComponentClientParams['cookieOptions']; - -const optionalCookieOptions: CookieOptions = { - domain: isDevelopment ? undefined : '.digger.dev', - secure: !isDevelopment, - path: '/', - sameSite: 'lax', -}; +import { supabaseAdminClient } from '../admin/supabaseAdminClient'; +// this used to be a user-scoped client +// switching to admin because we no longer use supabase auth +// it's only used on the server so security ok export const createSupabaseUserServerComponentClient = () => - createServerComponentClient( - { - cookies, - }, - { - options: { - global: { - fetch, - }, - }, - cookieOptions: optionalCookieOptions, - }, - ); + supabaseAdminClient;