Skip to content

Commit 92b8dc5

Browse files
committed
it actually works, lol
1 parent c30c3f0 commit 92b8dc5

File tree

8 files changed

+648
-101
lines changed

8 files changed

+648
-101
lines changed

exercises/99.final/01.solution/src/db/index.ts

Lines changed: 275 additions & 56 deletions
Large diffs are not rendered by default.

exercises/99.final/01.solution/src/db/migrations.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const migrations = [
99
name: 'initial_schema',
1010
up: async (db: D1Database) => {
1111
console.log('Starting initial schema migration...')
12+
13+
// TODO: maybe cascade deletes?
1214
try {
1315
await db.batch([
1416
db.prepare(sql`
@@ -18,9 +20,39 @@ const migrations = [
1820
applied_at INTEGER DEFAULT (CURRENT_TIMESTAMP) NOT NULL
1921
);
2022
`),
23+
db.prepare(sql`
24+
CREATE TABLE IF NOT EXISTS users (
25+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
26+
email text NOT NULL UNIQUE,
27+
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
28+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
29+
);
30+
`),
31+
// OAuth Access tokens. If user_id is null then it's not yet been claimed
32+
db.prepare(sql`
33+
CREATE TABLE IF NOT EXISTS access_tokens (
34+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
35+
token_value text NOT NULL UNIQUE,
36+
user_id integer,
37+
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
38+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
39+
);
40+
`),
41+
// A OTP emailed to the user to allow them to claim an access_token
42+
db.prepare(sql`
43+
CREATE TABLE IF NOT EXISTS validation_tokens (
44+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
45+
token_value text NOT NULL,
46+
email text NOT NULL,
47+
access_token_id integer NOT NULL,
48+
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
49+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
50+
);
51+
`),
2152
db.prepare(sql`
2253
CREATE TABLE IF NOT EXISTS entries (
2354
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
55+
user_id integer NOT NULL,
2456
title text NOT NULL,
2557
content text NOT NULL,
2658
mood text,
@@ -35,6 +67,7 @@ const migrations = [
3567
db.prepare(sql`
3668
CREATE TABLE IF NOT EXISTS tags (
3769
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
70+
user_id integer NOT NULL,
3871
name text NOT NULL UNIQUE,
3972
description text,
4073
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
@@ -44,6 +77,7 @@ const migrations = [
4477
db.prepare(sql`
4578
CREATE TABLE IF NOT EXISTS entry_tags (
4679
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
80+
user_id integer NOT NULL,
4781
entry_id integer NOT NULL,
4882
tag_id integer NOT NULL,
4983
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,

exercises/99.final/01.solution/src/db/schema.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@ const timestampSchema = z.preprocess((val) => {
1111
return val
1212
}, z.number())
1313

14+
export const userSchema = z.object({
15+
id: z.coerce.number(),
16+
email: z.string(),
17+
createdAt: timestampSchema,
18+
updatedAt: timestampSchema,
19+
})
20+
1421
// Schema Validation
1522
export const entrySchema = z.object({
1623
id: z.coerce.number(),
24+
userId: z.coerce.number(),
1725
title: z.string(),
1826
content: z.string(),
1927
mood: z.string().nullable(),
@@ -28,15 +36,16 @@ export const entrySchema = z.object({
2836
export const newEntrySchema = z.object({
2937
title: z.string(),
3038
content: z.string(),
31-
mood: z.string().nullable().default(null),
32-
location: z.string().nullable().default(null),
33-
weather: z.string().nullable().default(null),
34-
isPrivate: z.number().default(1),
35-
isFavorite: z.number().default(0),
39+
mood: z.string().optional().nullable().default(null),
40+
location: z.string().optional().nullable().default(null),
41+
weather: z.string().optional().nullable().default(null),
42+
isPrivate: z.number().optional().default(1),
43+
isFavorite: z.number().optional().default(0),
3644
})
3745

3846
export const tagSchema = z.object({
3947
id: z.coerce.number(),
48+
userId: z.coerce.number(),
4049
name: z.string(),
4150
description: z.string().nullable(),
4251
createdAt: timestampSchema,
@@ -50,6 +59,7 @@ export const newTagSchema = z.object({
5059

5160
export const entryTagSchema = z.object({
5261
id: z.coerce.number(),
62+
userId: z.coerce.number(),
5363
entryId: z.coerce.number(),
5464
tagId: z.coerce.number(),
5565
createdAt: timestampSchema,

exercises/99.final/01.solution/src/index.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
/// <reference path="../types/worker-configuration.d.ts" />
22

3+
import { OAuthProvider } from '@cloudflare/workers-oauth-provider'
34
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
45
import { McpAgent } from 'agents/mcp'
5-
import { DB, type Env } from './db'
6+
import { DB } from './db'
67
import { initializeTools } from './tools.ts'
8+
import { type Env } from './types'
79

8-
export class EpicMeMCP extends McpAgent {
10+
type State = {}
11+
type Props = { accessToken: string }
12+
13+
export class EpicMeMCP extends McpAgent<Env, State, Props> {
914
db!: DB
1015
server = new McpServer(
1116
{
@@ -32,10 +37,79 @@ You can also help users add tags to their entries and get all tags for an entry.
3237
}
3338

3439
async init() {
35-
initializeTools(this.server, this.db)
40+
initializeTools(this)
3641
}
3742
}
3843

39-
export default EpicMeMCP.mount('/mcp', {
44+
const epicMeMcpMount = EpicMeMCP.mount('/mcp', {
4045
binding: 'EPIC_ME_MCP_OBJECT',
4146
})
47+
48+
// Default handler for non-MCP routes
49+
const defaultHandler = {
50+
fetch: async (request: Request, env: Env) => {
51+
const url = new URL(request.url)
52+
if (url.pathname.endsWith('/authorize')) {
53+
try {
54+
const oauthReqInfo = await env.OAUTH_PROVIDER.parseAuthRequest(request)
55+
56+
const client = await env.OAUTH_PROVIDER.lookupClient(
57+
oauthReqInfo.clientId,
58+
)
59+
if (!client) {
60+
return new Response('Invalid client', { status: 400 })
61+
}
62+
63+
const userId = 'EpicMeMCP'
64+
// TODO: make this like crypto nice or whatever
65+
const accessToken = Math.random().toString(16).slice(2)
66+
67+
const result = await env.OAUTH_PROVIDER.completeAuthorization({
68+
request: oauthReqInfo,
69+
userId,
70+
props: { accessToken },
71+
scope: ['full'],
72+
metadata: {
73+
grantDate: new Date().toISOString(),
74+
},
75+
})
76+
77+
// Redirect to the client with the authorization code
78+
return new Response(null, {
79+
status: 302,
80+
headers: {
81+
Location: result.redirectTo,
82+
},
83+
})
84+
} catch (error) {
85+
console.error('Authorization error:', error)
86+
return new Response(
87+
error instanceof Error ? error.message : 'Authorization failed',
88+
{ status: 400 },
89+
)
90+
}
91+
}
92+
93+
// Default response for non-authorization requests
94+
return new Response('Not Found', { status: 404 })
95+
},
96+
}
97+
98+
// Create OAuth provider instance
99+
const oauthProvider = new OAuthProvider({
100+
apiRoute: '/mcp',
101+
// @ts-expect-error
102+
apiHandler: epicMeMcpMount,
103+
// @ts-expect-error
104+
defaultHandler,
105+
authorizeEndpoint: '/authorize',
106+
tokenEndpoint: '/oauth/token',
107+
clientRegistrationEndpoint: '/oauth/register',
108+
scopesSupported: ['full'],
109+
})
110+
111+
export default {
112+
fetch: (request: Request, env: Env, ctx: ExecutionContext) => {
113+
return oauthProvider.fetch(request, env, ctx)
114+
},
115+
}

0 commit comments

Comments
 (0)