Skip to content

Commit b7237c9

Browse files
feat: authorizer (#60)
* feat: clerk integration * uncouple context init and token retrieval * init * get sub, set sourceControl * remove test-stack * package lock * add sourceControl and userId to event * source control type fixed * added token generation * handleClick * fix? * fix??? * please work --------- Co-authored-by: dejan-crocoder <[email protected]>
1 parent 84ad1a5 commit b7237c9

File tree

16 files changed

+211
-40
lines changed

16 files changed

+211
-40
lines changed

apps/dashboard/src/app/api/integrations/source-control/github/route.ts

Whitespace-only changes.

apps/dashboard/src/app/api/integrations/source-control/gitlab/route.ts

Whitespace-only changes.

apps/dashboard/src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { UserButton, OrganizationSwitcher } from "@clerk/nextjs";
22
import { MainNav } from "~/components/ui/main-nav";
3+
import { GenerateToken } from "~/components/generate-token";
34

45
export default function Page() {
56
return (
@@ -15,6 +16,7 @@ export default function Page() {
1516
</div>
1617
</div>
1718
</div>
19+
<GenerateToken />
1820
</>
1921
);
2022
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client"
2+
import { useAuth } from '@clerk/nextjs';
3+
4+
5+
export function GenerateToken() {
6+
const { getToken } = useAuth();
7+
const handleClick = async () => {
8+
const token = await getToken({ template: 'dashboard' });
9+
console.log(token);
10+
};
11+
12+
13+
return (
14+
<button onClick={() => { handleClick().catch(console.log) }}>
15+
Generate Token
16+
</button>
17+
);
18+
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
import { useUser } from "@clerk/clerk-react";
3+
4+
export default function Home() {
5+
const { isSignedIn, user, isLoaded } = useUser();
6+
7+
if (!isLoaded) {
8+
return null;
9+
}
10+
11+
if (isSignedIn) {
12+
return <pre>{JSON.stringify(user, null, 2)}</pre>;
13+
}
14+
15+
return <div>Not signed in</div>;
16+
}
17+
18+

apps/extract-stack/.sst/types/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ declare module "sst/node/config" {
3939
value: string;
4040
}
4141
}
42+
}import "sst/node/config";
43+
declare module "sst/node/config" {
44+
export interface SecretResources {
45+
"CLERK_SECRET_KEY": {
46+
value: string;
47+
}
48+
}
4249
}import "sst/node/api";
4350
declare module "sst/node/api" {
4451
export interface ApiResources {

apps/extract-stack/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
"type-check": "tsc --noEmit && echo \"✔ No TypeScript warnings or errors\"",
88
"test": "echo \"Warning: no test specified\"",
99
"lint": "eslint . && echo \"✔ No ESLint warnings or errors\"",
10-
"dev": "sst dev",
10+
"dev": "npm run with-env sst dev",
1111
"build": "sst build",
1212
"deploy": "sst deploy",
1313
"remove": "sst remove",
14-
"console": "sst console"
14+
"console": "sst console",
15+
"with-env": "dotenv -e ../../.env --"
1516
},
1617
"author": "",
1718
"license": "ISC",
1819
"dependencies": {
1920
"@acme/extract-functions": "^1.0.0",
2021
"@acme/extract-schema": "^1.0.0",
2122
"@acme/source-control": "^1.0.0",
23+
"@clerk/clerk-sdk-node": "^4.12.2",
2224
"@libsql/client": "^0.3.1",
2325
"@tsconfig/node16": "^16.1.0",
2426
"aws-cdk-lib": "2.84.0",
@@ -29,6 +31,7 @@
2931
"zod": "^3.21.4"
3032
},
3133
"devDependencies": {
32-
"@types/aws-lambda": "^8.10.119"
34+
"@types/aws-lambda": "^8.10.119",
35+
"dotenv-cli": "^7.2.1"
3336
}
3437
}

apps/extract-stack/src/events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const eventBuilder = createEventBuilder({
99
version: z.number(),
1010
timestamp: z.number(),
1111
caller: z.string(),
12+
sourceControl: z.literal("github").or(z.literal("gitlab")),
13+
userId: z.string(),
1214
}).shape,
1315
});
1416

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,116 @@
11
import { extractRepositoryEvent, defineEvent } from "./events";
22
import { getRepository } from "@acme/extract-functions";
33
import type { Context, GetRepositorySourceControl, GetRepositoryEntities } from "@acme/extract-functions";
4-
import { GitlabSourceControl } from "@acme/source-control";
4+
import { GitlabSourceControl, GitHubSourceControl } from "@acme/source-control";
55
import { repositories, namespaces } from "@acme/extract-schema";
66
import { createClient } from '@libsql/client';
77
import { drizzle } from 'drizzle-orm/libsql';
8-
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
98
import { z } from "zod";
109
import { Config } from "sst/node/config";
10+
import { Clerk } from "@clerk/clerk-sdk-node";
11+
import { ApiHandler, useJsonBody } from 'sst/node/api';
1112

13+
const clerkClient = Clerk({ secretKey: Config.CLERK_SECRET_KEY });
1214
const client = createClient({ url: Config.DATABASE_URL, authToken: Config.DATABASE_AUTH_TOKEN });
1315

1416
const db = drizzle(client);
1517

1618
const event = defineEvent(extractRepositoryEvent);
1719

20+
const fetchSourceControlAccessToken = async (userId: string, forgeryIdProvider: 'oauth_github' | 'oauth_gitlab') => {
21+
const [userOauthAccessTokenPayload, ...rest] = await clerkClient.users.getUserOauthAccessToken(userId, forgeryIdProvider);
22+
if (!userOauthAccessTokenPayload) throw new Error("Failed to get token");
23+
if (rest.length !== 0) throw new Error("wtf ?");
24+
25+
return userOauthAccessTokenPayload.token;
26+
}
1827

1928
const context: Context<GetRepositorySourceControl, GetRepositoryEntities> = {
2029
entities: {
2130
repositories,
2231
namespaces,
2332
},
2433
integrations: {
25-
sourceControl: new GitlabSourceControl(Config.GITLAB_TOKEN),
34+
sourceControl: null,
2635
},
2736
db,
2837
};
2938

39+
const contextSchema = z.object({
40+
authorizer: z.object({
41+
jwt: z.object({
42+
claims: z.object({
43+
sub: z.string(),
44+
}),
45+
}),
46+
}),
47+
});
48+
49+
type CTX = z.infer<typeof contextSchema>;
50+
3051
const inputSchema = z.object({
3152
repositoryId: z.number(),
3253
repositoryName: z.string(),
3354
namespaceName: z.string(),
55+
sourceControl: z.literal("gitlab").or(z.literal("github")),
3456
});
3557

3658
type Input = z.infer<typeof inputSchema>;
3759

38-
export const handler: APIGatewayProxyHandlerV2 = async (apiGatewayEvent) => {
60+
export const handler = ApiHandler(async (ev) => {
61+
62+
const body = useJsonBody() as unknown;
63+
64+
let lambdaContext: CTX;
65+
66+
try {
67+
lambdaContext = contextSchema.parse(ev.requestContext);
68+
} catch (error) {
69+
return {
70+
statusCode: 401,
71+
body: JSON.stringify({ error: (error as Error).message }),
72+
};
73+
}
3974

4075
let input: Input;
76+
let sourceControlAccessToken: string;
4177

4278
try {
43-
input = inputSchema.parse(apiGatewayEvent);
79+
input = inputSchema.parse(body);
80+
4481
} catch (error) {
4582
return {
4683
statusCode: 400,
4784
body: JSON.stringify({ error: (error as Error).message }),
4885
};
4986
}
5087

51-
const { repositoryId, repositoryName, namespaceName } = input;
88+
const { sub } = lambdaContext.authorizer.jwt.claims;
89+
90+
91+
const { repositoryId, repositoryName, namespaceName, sourceControl } = input;
92+
93+
try {
94+
sourceControlAccessToken = await fetchSourceControlAccessToken(sub, `oauth_${sourceControl}`);
95+
} catch (error) {
96+
return {
97+
statusCode: 500,
98+
body: JSON.stringify({ error: (error as Error).message }),
99+
}
100+
}
101+
102+
if (sourceControl === "gitlab") {
103+
context.integrations.sourceControl = new GitlabSourceControl(sourceControlAccessToken);
104+
} else if (sourceControl === "github") {
105+
context.integrations.sourceControl = new GitHubSourceControl(sourceControlAccessToken);
106+
}
52107

53108
const { repository, namespace } = await getRepository({ externalRepositoryId: repositoryId, repositoryName, namespaceName }, context);
54109

55-
await event.publish({ repository, namespace }, { caller: 'extract-repository', timestamp: new Date().getTime(), version: 1 });
110+
await event.publish({ repository, namespace }, { caller: 'extract-repository', timestamp: new Date().getTime(), version: 1, sourceControl, userId: sub });
56111

57112
return {
58113
statusCode: 200,
59114
body: JSON.stringify({})
60115
};
61-
}
116+
});

apps/extract-stack/src/stack.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Api, EventBus, Queue } from "sst/constructs";
22
import type { StackContext } from "sst/constructs";
33
import { Config } from "sst/constructs";
4+
import { z } from "zod";
45

56
export function ExtractStack({ stack }: StackContext) {
67
const bus = new EventBus(stack, "ExtractBus", {
@@ -16,15 +17,34 @@ export function ExtractStack({ stack }: StackContext) {
1617
const DATABASE_URL = new Config.Secret(stack, "DATABASE_URL");
1718
const DATABASE_AUTH_TOKEN = new Config.Secret(stack, "DATABASE_AUTH_TOKEN");
1819
const GITLAB_TOKEN = new Config.Secret(stack, "GITLAB_TOKEN");
20+
const CLERK_SECRET_KEY = new Config.Secret(stack, "CLERK_SECRET_KEY");
21+
22+
const ENVSchema = z.object({
23+
CLERK_JWT_ISSUER: z.string(),
24+
CLERK_JWT_AUDIENCE: z.string(),
25+
});
26+
27+
const ENV = ENVSchema.parse(process.env);
1928

2029
const api = new Api(stack, "ExtractApi", {
2130
defaults: {
31+
authorizer: 'JwtAuthorizer',
2232
function: {
23-
bind: [bus, DATABASE_URL, DATABASE_AUTH_TOKEN, GITLAB_TOKEN, queue],
33+
bind: [bus, DATABASE_URL, DATABASE_AUTH_TOKEN, GITLAB_TOKEN, CLERK_SECRET_KEY, queue],
34+
},
35+
},
36+
authorizers: {
37+
JwtAuthorizer: {
38+
type: "jwt",
39+
identitySource: ["$request.header.Authorization"],
40+
jwt: {
41+
issuer: ENV.CLERK_JWT_ISSUER,
42+
audience: [ENV.CLERK_JWT_AUDIENCE],
43+
},
2444
},
2545
},
2646
routes: {
27-
"POST /gitlab": "src/extract-repository.handler",
47+
"POST /start": "src/extract-repository.handler",
2848
},
2949
});
3050

0 commit comments

Comments
 (0)