Skip to content

Commit

Permalink
Add revoke/create connections (#11)
Browse files Browse the repository at this point in the history
* Add revoke/create connections

* add connected endpoint

* delete

* ignore

* Add redirect back after auth

* fix css

* Improve css

* Link to package

* Improve fallback

* Improve prompt

* Add auth query headers

* Add proxy rewriting
  • Loading branch information
maccman authored Apr 21, 2023
1 parent 696670f commit 2d467db
Show file tree
Hide file tree
Showing 34 changed files with 725 additions and 120 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ yarn-error.log*
next-env.d.ts

.vscode
TODO.md
59 changes: 0 additions & 59 deletions TODO.md

This file was deleted.

5 changes: 2 additions & 3 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ export default async function AboutPage() {
</p>

<p>
AI, our new platform, needs it&apos;s own app-store. One not controlled by a
single rent-seeking company, but an open and free app-store built upon the open
web and the OpenAPI specification.
AI, our new platform, needs it&apos;s own app-store. An unrestricted app-store
built upon the open web and the OpenAPI specification.
</p>

<p>
Expand Down
2 changes: 2 additions & 0 deletions app/account/account-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export function AccountSidebar() {
<h3 className="text-xs font-semibold text-slate-900 dark:text-white">
Account
</h3>

<ul role="list" className="border-l border-slate-900/10 dark:border-white/5">
<li className="relative">
<MenuItem href="#auth">Emails</MenuItem>
<MenuItem href="#auth">Passkeys</MenuItem>
<MenuItem href="#packages">Packages</MenuItem>
<MenuItem href="#api-keys">API Keys</MenuItem>
<MenuItem href="#connections">Connections</MenuItem>
</li>
</ul>
</div>
Expand Down
14 changes: 7 additions & 7 deletions app/account/api-key-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {useRouter} from 'next/navigation'

import {jsonFetch} from '@/lib/json-fetch'
import {RedactedApiKey} from '@/server/db/api-keys/types'

export function ApiKeyItem({apiKey}: {apiKey: RedactedApiKey}) {
Expand All @@ -12,25 +13,24 @@ export function ApiKeyItem({apiKey}: {apiKey: RedactedApiKey}) {
return
}

const response = await fetch(`/api/api-keys/${apiKey.id}/revoke`, {
method: 'POST',
const {error} = await jsonFetch(`/api/api-keys/${apiKey.id}`, {
method: 'DELETE',
})

if (!response.ok) {
const error = await response.json()
alert(error.message ?? 'Unknown error')
if (error) {
alert(error.message)
throw new Error(error.message ?? 'Unknown error')
}

router.refresh()
}

return (
<li className="grid max-w-sm grid-cols-2">
<li className="grid max-w-sm grid-cols-3">
<span>
<code className="font-mono text-sm">sk-...{apiKey.keyExcerpt}</code>
</span>
<span>
<span className="col-span-2">
<button
type="button"
onClick={onRevoke}
Expand Down
5 changes: 2 additions & 3 deletions app/account/api-keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export async function ApiKeys({userId}: {userId: string}) {
const apiKeys = await getUnrevokedApiKeys({userId})

return (
<section className="space-y-3" id="api-keys">
<h2 className="text-base font-medium">API Keys</h2>
<div>
<ul>
{apiKeys.map((apiKey) => (
<ApiKeyItem key={apiKey.id} apiKey={apiKey} />
Expand All @@ -20,6 +19,6 @@ export async function ApiKeys({userId}: {userId: string}) {
<footer className="mt-5">
<NewApiKey />
</footer>
</section>
</div>
)
}
54 changes: 54 additions & 0 deletions app/account/connection-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client'

import Link from 'next/link'
import {useRouter} from 'next/navigation'

import {jsonFetch} from '@/lib/json-fetch'
import {UserConnection} from '@/server/db/user-connections/types'

export function ConnectionItem({connection}: {connection: UserConnection}) {
const router = useRouter()

const onRevoke = async () => {
if (
!confirm(
`Are you sure you want to revoke this connection to ${connection.package_id}?`,
)
) {
return
}

const {error} = await jsonFetch(`/api/connections/${connection.id}`, {
method: 'DELETE',
})

if (error) {
alert(error.message)
throw new Error(error.message ?? 'Unknown error')
}

router.refresh()
}

return (
<li className="grid max-w-sm grid-cols-3">
<Link
prefetch={false}
href={`/apis/${connection.package_id}`}
className="text-sm font-medium text-blue-500"
>
{connection.package_id}
</Link>

<span className="col-span-2">
<button
type="button"
onClick={onRevoke}
className="text-xs font-medium text-red-500"
>
Revoke
</button>
</span>
</li>
)
}
23 changes: 23 additions & 0 deletions app/account/connections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use server'

import {getConnectionsForUser} from '@/server/db/user-connections/getters'

import {ConnectionItem} from './connection-item'

export async function Connections({userId}: {userId: string}) {
const connections = await getConnectionsForUser(userId)

return (
<div>
<ul>
{connections.map((conn) => (
<ConnectionItem key={conn.id} connection={conn} />
))}
</ul>

{connections.length === 0 && (
<p className="text-sm text-slate-500">You don&apos;t have any connections yet.</p>
)}
</div>
)
}
5 changes: 2 additions & 3 deletions app/account/packages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export async function Packages({userId}: {userId: string}) {
const packages = await getAllPackagesForUserId(userId)

return (
<section className="space-y-3" id="packages">
<h2 className="text-base font-medium">Packages</h2>
<div>
<ul>
{packages.map((pkg) => (
<li key={pkg.id}>
Expand Down Expand Up @@ -46,6 +45,6 @@ export async function Packages({userId}: {userId: string}) {
New package...
</Link>
</footer>
</section>
</div>
)
}
20 changes: 16 additions & 4 deletions app/account/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {authOrRedirect} from '@/server/helpers/auth'

import {AccountSidebar} from './account-sidebar'
import {ApiKeys} from './api-keys'
import {Connections} from './connections'
import {Packages} from './packages'
import {Section} from './section'

const AccountProfile = dynamic(() => import('@/components/account-profile'), {
ssr: false,
Expand All @@ -31,10 +33,20 @@ export default async function AccountPage() {
<AccountProfile />
</div>

{/* @ts-expect-error Async Server Component */}
<Packages userId={userId} />
{/* @ts-expect-error Async Server Component */}
<ApiKeys userId={userId} />
<Section title="Packages">
{/* @ts-expect-error Async Server Component */}
<Packages userId={userId} />
</Section>

<Section title="API Keys">
{/* @ts-expect-error Async Server Component */}
<ApiKeys userId={userId} />
</Section>

<Section title="Connections">
{/* @ts-expect-error Async Server Component */}
<Connections userId={userId} />
</Section>
</div>
</div>
</div>
Expand Down
25 changes: 25 additions & 0 deletions app/account/section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ReactNode, Suspense} from 'react'

export function Section({title, children}: {title: string; children: ReactNode}) {
return (
<section className="space-y-3" id={dasherize(title)}>
<h2 className="font-medium">{title}</h2>

<Suspense fallback={<Fallback />}>{children}</Suspense>
</section>
)
}

function Fallback() {
return (
<div className="grid max-w-sm grid-rows-3 gap-3 rounded-md bg-slate-50 p-3 dark:bg-white/5">
<div className="h-3 animate-pulse rounded-md bg-slate-200 dark:bg-slate-800"></div>
<div className="h-3 animate-pulse rounded-md bg-slate-200 dark:bg-slate-800"></div>
<div className="h-3 animate-pulse rounded-md bg-slate-200 dark:bg-slate-800"></div>
</div>
)
}

function dasherize(str: string) {
return str.replace(/\s+/g, '-').toLowerCase()
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {getApiKey} from '@/server/db/api-keys/getters'
import {revokeApiKey} from '@/server/db/api-keys/setters'
import {withAuth} from '@/server/helpers/auth'

export const POST = withAuth(
export const DELETE = withAuth(
async (
req: Request,
{userId, params}: {userId: string; params: {apiKeyId: string}},
Expand Down
21 changes: 21 additions & 0 deletions app/api/connections/[connectionId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {NextResponse} from 'next/server'

import {assertString} from '@/lib/assert'
import {deleteUserConnection} from '@/server/db/user-connections/setters'
import {withAuth} from '@/server/helpers/auth'

export const DELETE = withAuth(
async (
req: Request,
{userId, params}: {userId: string; params: {connectionId: string}},
) => {
const {connectionId} = params

assertString(userId, 'Invalid user')
assertString(connectionId, 'Invalid connection id')

await deleteUserConnection({userId, connectionId})

return NextResponse.json({success: true})
},
)
46 changes: 46 additions & 0 deletions app/api/connections/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {NextResponse} from 'next/server'
import {z} from 'zod'

import {parseJsonSpec} from '@/helpers/openapi'
import {getPackageById} from '@/server/db/packages/getters'
import {createUserConnection} from '@/server/db/user-connections/setters'
import {withApiBuilder} from '@/server/helpers/api-builder'
import {withAuth} from '@/server/helpers/auth'
import {error} from '@/server/helpers/error'

const ApiSchema = z.object({
package_id: z.string(),
api_key: z.string().nullable(),
})

type ApiRequestParams = z.infer<typeof ApiSchema>

// Connects a user to a package

export const POST = withAuth(
withApiBuilder<ApiRequestParams, {userId: string}>(
ApiSchema,
async (req: Request, {userId, data}) => {
const packageRow = await getPackageById(data.package_id)

if (!packageRow) {
return error('Package does not exist')
}

const _doc = await parseJsonSpec(packageRow.openapi)

// Todo - validate the spec against
// the auth provided

const connection = await createUserConnection({
packageId: data.package_id,
userId: userId,
apiKey: data.api_key,
})

return NextResponse.json({
id: connection.id,
})
},
),
)
Loading

1 comment on commit 2d467db

@vercel
Copy link

@vercel vercel bot commented on 2d467db Apr 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.