Skip to content
This repository was archived by the owner on Jan 10, 2025. It is now read-only.

Commit f391a0e

Browse files
committed
integrate logto on api
1 parent aa5beaf commit f391a0e

File tree

15 files changed

+152
-98
lines changed

15 files changed

+152
-98
lines changed

management-api/.dev.vars

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ SVIX_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTc1OTg4NzIsImV4cCI
22
SF_TOKEN="eyJhbGciOiJLTVNFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIwMzI5NTcyNjIsImp0aSI6Ijg4MGRlNTVkLTVjZDktNDY5MS04MDJjLTY1ZTBkMGI1NTgxOCIsImlhdCI6MTcxNzU5NzI2MiwiaXNzIjoiZGZ1c2UuaW8iLCJzdWIiOiIwem9qbzBmZjgxNDE5ZjA1ZTJjNjciLCJ2IjoxLCJha2kiOiJlMmZmMWFjMTZkMTczYmViYWJhNzkwZWU2OGY1YTJiOGZjZWYxMDFjODJhYTk5YmFmNzlmZjk4YzVmMGE0NGZiIiwidWlkIjoiMHpvam8wZmY4MTQxOWYwNWUyYzY3In0.Houp42QFgFbHPYzNw8ezbQ6k8YG8L82a3_SpeqWUt7agm1SHe4BqYkAp9Y23dCopoouFUQ1mXS_lpsPLrVTqIQ"
33
GUILD_ADMIN_TOKEN="test"
44
SUBSTREAM_LISTENER_HOST="http://localhost:4040"
5+
LOGTO_HOST="https://hokigy.logto.app"
6+
JWT_AUDIENCE="http://localhost:8787/graphql"

management-api/drizzle/0000_famous_hobgoblin.sql renamed to management-api/drizzle/0000_new_lyja.sql

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ CREATE TABLE `project` (
99
`id` text PRIMARY KEY NOT NULL,
1010
`name` text NOT NULL,
1111
`configuration` text NOT NULL,
12-
`creator_id` integer NOT NULL,
12+
`creator_id` text NOT NULL,
1313
`organization_id` integer NOT NULL,
1414
`created_at` integer DEFAULT (strftime('%s', 'now')),
1515
`updated_at` integer DEFAULT (strftime('%s', 'now')),
@@ -18,14 +18,13 @@ CREATE TABLE `project` (
1818
);
1919
--> statement-breakpoint
2020
CREATE TABLE `user` (
21-
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
22-
`name` text NOT NULL,
21+
`id` text PRIMARY KEY NOT NULL,
2322
`created_at` integer DEFAULT (strftime('%s', 'now')),
2423
`updated_at` integer DEFAULT (strftime('%s', 'now'))
2524
);
2625
--> statement-breakpoint
2726
CREATE TABLE `users_to_organizations` (
28-
`user_id` integer NOT NULL,
27+
`user_id` text NOT NULL,
2928
`organization_id` integer NOT NULL,
3029
PRIMARY KEY(`organization_id`, `user_id`),
3130
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,

management-api/drizzle/meta/0000_snapshot.json

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"version": "5",
33
"dialect": "sqlite",
4-
"id": "f863f5ee-aa68-422c-abbb-dd1bd42e6a20",
4+
"id": "62b8f3b0-26d4-41ef-b2f0-8ad1e534587f",
55
"prevId": "00000000-0000-0000-0000-000000000000",
66
"tables": {
77
"organization": {
@@ -69,7 +69,7 @@
6969
},
7070
"creator_id": {
7171
"name": "creator_id",
72-
"type": "integer",
72+
"type": "text",
7373
"primaryKey": false,
7474
"notNull": true,
7575
"autoincrement": false
@@ -133,15 +133,8 @@
133133
"columns": {
134134
"id": {
135135
"name": "id",
136-
"type": "integer",
137-
"primaryKey": true,
138-
"notNull": true,
139-
"autoincrement": true
140-
},
141-
"name": {
142-
"name": "name",
143136
"type": "text",
144-
"primaryKey": false,
137+
"primaryKey": true,
145138
"notNull": true,
146139
"autoincrement": false
147140
},
@@ -172,7 +165,7 @@
172165
"columns": {
173166
"user_id": {
174167
"name": "user_id",
175-
"type": "integer",
168+
"type": "text",
176169
"primaryKey": false,
177170
"notNull": true,
178171
"autoincrement": false

management-api/drizzle/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "5",
8-
"when": 1715030274652,
9-
"tag": "0000_famous_hobgoblin",
8+
"when": 1718830504856,
9+
"tag": "0000_new_lyja",
1010
"breakpoints": true
1111
}
1212
]

management-api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
"dependencies": {
2525
"@pothos/core": "^3.41.1",
2626
"@pothos/plugin-relay": "^3.46.0",
27+
"@pothos/plugin-scope-auth": "^3.22.0",
2728
"@pothos/plugin-with-input": "^3.10.1",
2829
"drizzle-orm": "^0.30.10",
2930
"fets": "^0.8.0",
3031
"graphql": "^16.8.1",
3132
"graphql-scalars": "^1.23.0",
3233
"graphql-yoga": "^5.3.0",
34+
"jose": "^5.4.1",
3335
"utils": "workspace:*",
3436
"uuid": "^9.0.1",
3537
"viem": "^2.9.31"

management-api/schema.graphql

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,6 @@ scalar EthAddress
5353

5454
type Mutation {
5555
createProject(input: CreateProjectInput!): CreateProjectPayload!
56-
createUser(input: MutationCreateUserInput!): User!
57-
}
58-
59-
input MutationCreateUserInput {
60-
"""
61-
Name of the user
62-
"""
63-
name: String!
6456
}
6557

6658
interface Node {
@@ -119,7 +111,6 @@ User of the system
119111
"""
120112
type User {
121113
id: ID!
122-
name: String!
123114
}
124115

125116
"""

management-api/src/context.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { drizzle } from "drizzle-orm/d1";
22
import { svixClient } from "./svix-api";
3+
import type * as dbSchema from "./db-schema";
34

45
export interface Env {
56
DB: D1Database;
@@ -8,9 +9,12 @@ export interface Env {
89
SF_TOKEN: string;
910
GUILD_ADMIN_TOKEN: string;
1011
SUBSTREAM_LISTENER_HOST: string;
12+
LOGTO_HOST: string;
13+
JWT_AUDIENCE: string;
1114
}
1215

1316
export interface Context extends Env {
14-
db: ReturnType<typeof drizzle>;
17+
db: ReturnType<typeof drizzle<typeof dbSchema>>;
1518
svix: ReturnType<typeof svixClient>;
19+
authUserId: string | null;
1620
}

management-api/src/db-schema.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { v4 } from "uuid";
99
import { relations, sql } from "drizzle-orm";
1010

1111
export const user = sqliteTable("user", {
12-
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
13-
name: text("name").notNull(),
12+
id: text("id").primaryKey(),
1413
createdAt: integer("created_at", { mode: "timestamp" }).default(
1514
sql`(strftime('%s', 'now'))`,
1615
),
@@ -45,7 +44,7 @@ export const organizationRelations = relations(organization, ({ many }) => ({
4544
export const usersToOrgs = sqliteTable(
4645
"users_to_organizations",
4746
{
48-
userId: integer("user_id")
47+
userId: text("user_id")
4948
.notNull()
5049
.references(() => user.id),
5150
orgId: integer("organization_id")
@@ -78,7 +77,7 @@ export const project = sqliteTable(
7877
name: text("name").notNull(),
7978
// we store a zod processed objects. Allowing us more flexibility in the future
8079
configuration: text("configuration", { mode: "json" }).notNull(),
81-
creator: integer("creator_id")
80+
creator: text("creator_id")
8281
.references(() => user.id)
8382
.notNull(),
8483
organization: integer("organization_id")

management-api/src/index.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,83 @@ import { schema } from "./schema";
33
import { drizzle } from "drizzle-orm/d1";
44
import { Env } from "context";
55
import { svixClient } from "svix-api";
6+
import { createRemoteJWKSet, jwtVerify } from "jose";
7+
import * as dbSchema from "./db-schema";
8+
import { eq } from "drizzle-orm";
69

710
const yoga = createYoga<Env>({
811
schema,
912
maskedErrors: false,
10-
context: (ctx) => {
13+
context: async (ctx) => {
14+
const db = drizzle(ctx.DB, {
15+
schema: dbSchema,
16+
});
17+
18+
const jwt = ctx.request.headers.get("authorization")?.split(" ")?.[1];
19+
let authUserId: string | null = null;
20+
21+
if (jwt) {
22+
const { payload } = await jwtVerify(
23+
jwt, // The raw Bearer Token extracted from the request header
24+
createRemoteJWKSet(new URL(`${ctx.LOGTO_HOST}/oidc/jwks`)), // generate a jwks using jwks_uri inquired from Logto server
25+
{
26+
issuer: `${ctx.LOGTO_HOST}/oidc`,
27+
audience: ctx.JWT_AUDIENCE,
28+
},
29+
);
30+
31+
authUserId = payload.sub || null;
32+
33+
if (!authUserId) {
34+
throw new Error("Missing sub in token");
35+
}
36+
37+
const user = await db.query.user.findFirst({
38+
where: eq(dbSchema.user.id, authUserId),
39+
});
40+
41+
if (!user) {
42+
const createOrg = await db
43+
.insert(dbSchema.organization)
44+
.values({
45+
name: `${authUserId}'s Projects`,
46+
})
47+
.returning();
48+
49+
const createdOrg = createOrg[0];
50+
51+
if (!createdOrg) {
52+
throw new Error("Unable to create organization");
53+
}
54+
55+
const createUser = await db
56+
.insert(dbSchema.user)
57+
.values({
58+
id: authUserId,
59+
})
60+
.returning();
61+
62+
const createdUser = createUser[0];
63+
64+
if (!createdUser) {
65+
throw new Error("Unable to create user");
66+
}
67+
68+
await db.insert(dbSchema.usersToOrgs).values({
69+
userId: createdUser.id,
70+
orgId: createdOrg.id,
71+
});
72+
}
73+
}
74+
1175
return {
1276
...ctx,
1377
svix: svixClient({
1478
endpoint: ctx.SVIX_HOST,
1579
GUILD_ADMIN_TOKEN: ctx.GUILD_ADMIN_TOKEN,
1680
}),
17-
db: drizzle(ctx.DB),
81+
authUserId,
82+
db,
1883
};
1984
},
2085
});

management-api/src/schema/project.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { project } from "../db-schema";
1+
import { project, user } from "../db-schema";
22
import { builder, notEmpty } from "./utils";
33
import { v4 as uuidv4 } from "uuid";
44
import { ProjectConfigurationSchema, SUPPORTED_CHAINS } from "utils";
@@ -24,6 +24,7 @@ builder.node(Project, {
2424
ProjectConfigurationSchema.parse(obj.configuration).chain,
2525
}),
2626
}),
27+
2728
loadOne: (id, { db }) =>
2829
db
2930
.select()
@@ -42,6 +43,9 @@ builder.queryField("projects", (t) => {
4243
return t.connection({
4344
type: Project,
4445
description: "List of projects",
46+
authScopes: {
47+
isAuthenticated: true,
48+
},
4549
resolve: async (_parent, { first, after }, { db }) => {
4650
const limit = first ?? 10;
4751

@@ -123,18 +127,26 @@ builder.relayMutationField(
123127
}),
124128
},
125129
{
130+
authScopes: {
131+
isAuthenticated: true,
132+
},
126133
resolve: async (
127134
_parent,
128135
{ input },
129136
{
130137
db,
138+
authUserId,
131139
svix,
132140
SVIX_TOKEN,
133141
SF_TOKEN,
134142
GUILD_ADMIN_TOKEN,
135143
SUBSTREAM_LISTENER_HOST,
136144
},
137145
) => {
146+
if (!authUserId) {
147+
throw new Error("User not authenticated");
148+
}
149+
138150
const configuration = await ProjectConfigurationSchema.safeParseAsync({
139151
...input,
140152
webhookUrl: input.webhookUrl?.toString(),
@@ -183,14 +195,24 @@ builder.relayMutationField(
183195
throw new Error("Failed to register webhook");
184196
}
185197

198+
// For now we just have one organization that is user's default.
199+
// TODO: rework this in future to handle multiple organizations
200+
const org = await db.query.usersToOrgs.findFirst({
201+
where: eq(user.id, authUserId),
202+
});
203+
204+
if (!org) {
205+
throw new Error("User is not part of any organization");
206+
}
207+
186208
const createdProj = await db
187209
.insert(project)
188210
.values({
189211
name: input.name,
190212
configuration: configuration.data,
191-
creator: 1, // TODO: get from the context user
213+
creator: authUserId,
192214
id,
193-
organization: 1, // TODO: get from the context user
215+
organization: org.orgId,
194216
})
195217
.returning();
196218

0 commit comments

Comments
 (0)