Skip to main content
Glama

MCP Auth

by rubenpenap
index.tsx17.5 kB
import { Form } from 'react-router' import { migrate } from '#workers/db/migrations' import { sql } from '#workers/db/utils' import { type Route } from './+types/index' export function meta({}: Route.MetaArgs) { return [ { title: 'Epic Me' }, { name: 'description', content: 'The Epic Me Journaling App' }, ] } export async function loader({ context }: Route.LoaderArgs) { const users = await context.db.getAllUsers() const usersWithTheirData = await Promise.all( users.map(async (user) => { const entries = await context.db.getEntries(user.id) const tags = await context.db.getTags(user.id) // Get full entry data including createdAt for each entry const fullEntries = await Promise.all( entries.map(async (entry) => { const fullEntry = await context.db.getEntry(user.id, entry.id) if (!fullEntry) { return null // Ignore entries that aren't found } return { ...entry, createdAt: fullEntry.createdAt, formattedDate: new Date( fullEntry.createdAt * 1000, ).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }), } }), ) // Filter out null entries const validEntries = fullEntries.filter( (entry): entry is NonNullable<typeof entry> => entry !== null, ) // Get full tag data including createdAt for each tag const fullTags = await Promise.all( tags.map(async (tag) => { const fullTag = await context.db.getTag(user.id, tag.id) if (!fullTag) { return null // Ignore tags that aren't found } return { ...fullTag, formattedDate: new Date( fullTag.createdAt * 1000, ).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }), } }), ) // Filter out null tags const validTags = fullTags.filter( (tag): tag is NonNullable<typeof tag> => tag !== null, ) return { ...user, entries: validEntries, tags: validTags } }), ) // Get KV data const { cloudflare } = context const { env } = cloudflare const kvList = await env.OAUTH_KV.list() // Get all KV entries with their data const kvEntries = await Promise.all( kvList.keys.map(async (key) => { try { const value = await env.OAUTH_KV.get(key.name, { type: 'json' }) return { key: key.name, value: value, expiration: key.expiration, metadata: key.metadata, } } catch { // If JSON parsing fails, try to get as text const value = await env.OAUTH_KV.get(key.name, { type: 'text' }) return { key: key.name, value: value, expiration: key.expiration, metadata: key.metadata, } } }), ) return { users: usersWithTheirData, kvEntries } } export async function action({ request, context }: Route.ActionArgs) { const formData = await request.formData() const intent = formData.get('intent') if (intent === 'reset-database') { try { const { cloudflare } = context const { env } = cloudflare // Reset database by dropping all tables and rerunning migrations await env.EPIC_ME_DB.batch([ env.EPIC_ME_DB.prepare(sql`DROP TABLE IF EXISTS entry_tags`), env.EPIC_ME_DB.prepare(sql`DROP TABLE IF EXISTS tags`), env.EPIC_ME_DB.prepare(sql`DROP TABLE IF EXISTS entries`), env.EPIC_ME_DB.prepare(sql`DROP TABLE IF EXISTS users`), env.EPIC_ME_DB.prepare(sql`DROP TABLE IF EXISTS schema_versions`), ]) // Rerun migrations to recreate all tables and seed data await migrate(env.EPIC_ME_DB) // Clear all KV data const kvList = await env.OAUTH_KV.list() if (kvList.keys.length > 0) { for (const key of kvList.keys) { await env.OAUTH_KV.delete(key.name) } } return { success: true, message: 'Database and KV store reset successfully!', } } catch (error) { console.error('Error resetting database:', error) return { success: false, message: 'Failed to reset database and KV store', } } } return { success: false, message: 'Invalid action' } } type User = Route.ComponentProps['loaderData']['users'][number] type KVEntry = Route.ComponentProps['loaderData']['kvEntries'][number] // Component for empty states function EmptyState({ icon, message, className = 'flex h-[32rem] items-center justify-center', }: { icon: React.ReactNode message: string className?: string }) { return ( <div className={className}> <div className="rounded-lg border-2 border-dashed border-gray-300 p-8 text-center dark:border-gray-600"> {icon} <p className="mt-2 text-sm text-gray-500 dark:text-gray-400"> {message} </p> </div> </div> ) } // Component for individual entry display function EntryCard({ entry }: { entry: User['entries'][number] }) { return ( <div className="rounded-lg border border-gray-200 bg-white p-4 transition-all hover:border-blue-300 hover:bg-blue-50 dark:border-gray-600 dark:bg-gray-600 dark:hover:border-blue-500 dark:hover:bg-blue-900/20"> <div className="flex items-start justify-between"> <div className="flex-1"> <h5 className="font-semibold text-gray-900 dark:text-white"> {entry.title} </h5> <div className="mt-1 flex items-center gap-4 text-sm text-gray-600 dark:text-gray-300"> <span className="flex items-center gap-1"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> </svg> {entry.formattedDate} </span> <span className="flex items-center gap-1"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" /> </svg> {entry.tagCount} tag{entry.tagCount !== 1 ? 's' : ''} </span> <span className="text-xs text-gray-500 dark:text-gray-400"> ID: {entry.id} </span> </div> </div> </div> </div> ) } // Component for individual tag display function TagCard({ tag }: { tag: User['tags'][number] }) { return ( <div className="flex items-center justify-between rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-600 dark:bg-gray-600"> <div className="flex items-center gap-3"> <span className="text-xs text-gray-500 dark:text-gray-400"> ID: {tag.id} </span> <span className="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"> {tag.name} </span> {tag.description && ( <span className="text-xs text-gray-600 dark:text-gray-300"> {tag.description} </span> )} </div> <span className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400"> <svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> </svg> {tag.formattedDate} </span> </div> ) } // Component for individual user display function UserCard({ user }: { user: User }) { return ( <details className="group rounded-xl bg-gray-50 dark:bg-gray-700 [&>summary]:list-none"> <summary className="cursor-pointer p-6 [&::-webkit-details-marker]:hidden"> <div className="flex items-center gap-3"> <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white"> <span className="text-lg font-bold"> {user.email.charAt(0).toUpperCase()} </span> </div> <h3 className="text-xl font-semibold text-gray-900 dark:text-white"> {user.email} </h3> <div className="ml-auto flex items-center gap-4 text-sm text-gray-600 dark:text-gray-300"> <span>{user.entries.length} entries</span> <span>{user.tags.length} tags</span> <svg className="h-4 w-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </div> </div> </summary> <div className="border-t border-gray-200 p-6 dark:border-gray-600"> {user.entries.length > 0 ? ( <div className="space-y-3"> <h4 className="text-sm font-medium tracking-wide text-gray-600 uppercase dark:text-gray-400"> Entries ({user.entries.length}) </h4> <div className="grid gap-3"> {user.entries.map((entry) => ( <EntryCard key={entry.id} entry={entry} /> ))} </div> </div> ) : ( <EmptyState icon={ <svg className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> } message="No entries yet" className="rounded-lg border-2 border-dashed border-gray-300 p-8 text-center dark:border-gray-600" /> )} {user.tags.length > 0 && ( <div className="mt-6"> <h4 className="mb-3 text-sm font-medium tracking-wide text-gray-600 uppercase dark:text-gray-400"> Tags ({user.tags.length}) </h4> <div className="grid gap-2"> {user.tags.map((tag) => ( <TagCard key={tag.id} tag={tag} /> ))} </div> </div> )} </div> </details> ) } // Component for individual KV entry display function KVEntryCard({ kvEntry }: { kvEntry: KVEntry }) { return ( <details className="group rounded-xl bg-gray-50 dark:bg-gray-700 [&>summary]:list-none"> <summary className="cursor-pointer p-6 [&::-webkit-details-marker]:hidden"> <div className="flex items-center gap-3"> <div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-r from-green-500 to-emerald-600 text-white"> <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" /> </svg> </div> <h3 className="text-xl font-semibold break-all text-gray-900 dark:text-white"> {kvEntry.key} </h3> <div className="ml-auto flex items-center gap-4 text-sm text-gray-600 dark:text-gray-300"> <svg className="h-4 w-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </div> </div> </summary> <div className="border-t border-gray-200 p-6 dark:border-gray-600"> <div className="space-y-3"> <div> <h4 className="text-sm font-medium text-gray-600 dark:text-gray-400"> Value </h4> <pre className="mt-1 rounded bg-gray-100 p-3 text-sm dark:bg-gray-600"> {JSON.stringify(kvEntry.value, null, 2)} </pre> </div> <div className="grid grid-cols-2 gap-4 text-sm"> <div> <span className="font-medium text-gray-600 dark:text-gray-400"> Expiration: </span> <span className="ml-2 text-gray-900 dark:text-white"> {kvEntry.expiration ? new Date(kvEntry.expiration * 1000).toLocaleDateString( 'en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }, ) : 'Never'} </span> </div> {kvEntry.metadata && ( <div> <span className="font-medium text-gray-600 dark:text-gray-400"> Metadata: </span> <span className="ml-2 text-gray-900 dark:text-white"> {JSON.stringify(kvEntry.metadata)} </span> </div> )} </div> </div> </div> </details> ) } // Component for database reset section function DatabaseResetSection() { return ( <section> <h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white"> Database Reset </h2> <div className="rounded-lg border border-gray-200 bg-white p-6 shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-900/50"> <p className="mb-6 text-gray-600 dark:text-gray-300"> This allows you to completely reset the database and KV store. This will delete all data and rerun migrations to recreate the schema with fresh data. </p> <div className="mb-6 rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-700 dark:bg-yellow-900/20"> <div className="flex"> <div className="flex-shrink-0"> <svg className="h-5 w-5 text-yellow-400 dark:text-yellow-300" viewBox="0 0 20 20" fill="currentColor" > <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> </svg> </div> <div className="ml-3"> <h3 className="text-sm font-medium text-yellow-800 dark:text-yellow-200"> Warning </h3> <div className="mt-2 text-sm text-yellow-700 dark:text-yellow-300"> <p> This action will permanently delete all data in the database and KV store. </p> </div> </div> </div> </div> <Form method="post"> <input type="hidden" name="intent" value="reset-database" /> <button type="submit" className="rounded-lg bg-red-600 px-6 py-3 font-bold text-white transition-colors duration-200 hover:bg-red-700" onClick={(e) => { if ( !confirm( 'Are you absolutely sure you want to reset the database and KV store? This will delete ALL data and cannot be undone.', ) ) { e.preventDefault() } }} > Reset Database and KV Store </button> </Form> </div> </section> ) } export default function Home({ loaderData }: Route.ComponentProps) { const { users, kvEntries } = loaderData const kvContainerClass = kvEntries.length > 0 ? 'rounded-lg border border-gray-200 bg-white p-6 shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-900/50 h-[32rem] overflow-y-auto' : 'rounded-lg border border-gray-200 bg-white p-6 shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-900/50' return ( <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-8 dark:from-gray-900 dark:to-gray-800"> <div className="mx-auto max-w-6xl"> <h1 className="mb-8 text-4xl font-bold text-gray-900 dark:text-white"> Epic Me App </h1> <section className="mb-12"> <h2 className="mb-6 text-2xl font-bold text-gray-800 dark:text-gray-200"> Users & Entries </h2> <div className="h-[32rem] overflow-y-auto rounded-lg border border-gray-200 bg-white p-6 shadow-lg dark:border-gray-700 dark:bg-gray-800 dark:shadow-gray-900/50"> <div className="grid gap-8"> {users.map((user) => ( <UserCard key={user.id} user={user} /> ))} </div> </div> </section> <hr className="my-6" /> <section> <h2 className="mb-6 text-2xl font-bold text-gray-900 dark:text-white"> KV Store Entries ({kvEntries.length}) </h2> <div className={kvContainerClass}> {kvEntries.length > 0 ? ( <div className="grid gap-6"> {kvEntries.map((kvEntry) => ( <KVEntryCard key={kvEntry.key} kvEntry={kvEntry} /> ))} </div> ) : ( <EmptyState icon={ <svg className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" /> </svg> } message="No KV entries found" /> )} </div> </section> <hr className="my-6" /> <DatabaseResetSection /> </div> </div> ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rubenpenap/mcp-auth'

If you have feedback or need assistance with the MCP directory API, please join our Discord server