diff --git a/apps/dashboard/src/app/api/integrations/source-control/github/route.ts b/apps/dashboard/src/app/api/integrations/source-control/github/route.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/dashboard/src/app/api/integrations/source-control/gitlab/route.ts b/apps/dashboard/src/app/api/integrations/source-control/gitlab/route.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/dashboard/src/app/page.tsx b/apps/dashboard/src/app/page.tsx
index d9633ff5c..d1b8a860f 100644
--- a/apps/dashboard/src/app/page.tsx
+++ b/apps/dashboard/src/app/page.tsx
@@ -1,5 +1,6 @@
import { UserButton, OrganizationSwitcher } from "@clerk/nextjs";
import { MainNav } from "~/components/ui/main-nav";
+import { GenerateToken } from "~/components/generate-token";
export default function Page() {
return (
@@ -15,6 +16,7 @@ export default function Page() {
+
>
);
}
diff --git a/apps/dashboard/src/components/generate-token.tsx b/apps/dashboard/src/components/generate-token.tsx
new file mode 100644
index 000000000..e49fcfb80
--- /dev/null
+++ b/apps/dashboard/src/components/generate-token.tsx
@@ -0,0 +1,19 @@
+"use client"
+import { useAuth } from '@clerk/nextjs';
+
+
+export function GenerateToken() {
+ const { getToken } = useAuth();
+ const handleClick = async () => {
+ const token = await getToken({ template: 'dashboard' });
+ console.log(token);
+ };
+
+
+ return (
+
+ );
+
+}
diff --git a/apps/dashboard/src/components/user-test.tsx b/apps/dashboard/src/components/user-test.tsx
new file mode 100644
index 000000000..3ed20d426
--- /dev/null
+++ b/apps/dashboard/src/components/user-test.tsx
@@ -0,0 +1,18 @@
+"use client";
+import { useUser } from "@clerk/clerk-react";
+
+export default function Home() {
+ const { isSignedIn, user, isLoaded } = useUser();
+
+ if (!isLoaded) {
+ return null;
+ }
+
+ if (isSignedIn) {
+ return
{JSON.stringify(user, null, 2)};
+ }
+
+ return Not signed in
;
+}
+
+
diff --git a/apps/extract-stack/.sst/types/index.ts b/apps/extract-stack/.sst/types/index.ts
index 1d0e6abf6..ba7a76a42 100644
--- a/apps/extract-stack/.sst/types/index.ts
+++ b/apps/extract-stack/.sst/types/index.ts
@@ -39,6 +39,13 @@ declare module "sst/node/config" {
value: string;
}
}
+}import "sst/node/config";
+declare module "sst/node/config" {
+ export interface SecretResources {
+ "CLERK_SECRET_KEY": {
+ value: string;
+ }
+ }
}import "sst/node/api";
declare module "sst/node/api" {
export interface ApiResources {
diff --git a/apps/extract-stack/package.json b/apps/extract-stack/package.json
index 90e6bff6c..34944d644 100644
--- a/apps/extract-stack/package.json
+++ b/apps/extract-stack/package.json
@@ -7,11 +7,12 @@
"type-check": "tsc --noEmit && echo \"✔ No TypeScript warnings or errors\"",
"test": "echo \"Warning: no test specified\"",
"lint": "eslint . && echo \"✔ No ESLint warnings or errors\"",
- "dev": "sst dev",
+ "dev": "npm run with-env sst dev",
"build": "sst build",
"deploy": "sst deploy",
"remove": "sst remove",
- "console": "sst console"
+ "console": "sst console",
+ "with-env": "dotenv -e ../../.env --"
},
"author": "",
"license": "ISC",
@@ -19,6 +20,7 @@
"@acme/extract-functions": "^1.0.0",
"@acme/extract-schema": "^1.0.0",
"@acme/source-control": "^1.0.0",
+ "@clerk/clerk-sdk-node": "^4.12.2",
"@libsql/client": "^0.3.1",
"@tsconfig/node16": "^16.1.0",
"aws-cdk-lib": "2.84.0",
@@ -29,6 +31,7 @@
"zod": "^3.21.4"
},
"devDependencies": {
- "@types/aws-lambda": "^8.10.119"
+ "@types/aws-lambda": "^8.10.119",
+ "dotenv-cli": "^7.2.1"
}
}
diff --git a/apps/extract-stack/src/events.ts b/apps/extract-stack/src/events.ts
index 5a79c2ec8..960c34dc9 100644
--- a/apps/extract-stack/src/events.ts
+++ b/apps/extract-stack/src/events.ts
@@ -9,6 +9,8 @@ const eventBuilder = createEventBuilder({
version: z.number(),
timestamp: z.number(),
caller: z.string(),
+ sourceControl: z.literal("github").or(z.literal("gitlab")),
+ userId: z.string(),
}).shape,
});
diff --git a/apps/extract-stack/src/extract-repository.ts b/apps/extract-stack/src/extract-repository.ts
index d0e5fb90e..b1ad5ffb1 100644
--- a/apps/extract-stack/src/extract-repository.ts
+++ b/apps/extract-stack/src/extract-repository.ts
@@ -1,20 +1,29 @@
import { extractRepositoryEvent, defineEvent } from "./events";
import { getRepository } from "@acme/extract-functions";
import type { Context, GetRepositorySourceControl, GetRepositoryEntities } from "@acme/extract-functions";
-import { GitlabSourceControl } from "@acme/source-control";
+import { GitlabSourceControl, GitHubSourceControl } from "@acme/source-control";
import { repositories, namespaces } from "@acme/extract-schema";
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
-import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
import { z } from "zod";
import { Config } from "sst/node/config";
+import { Clerk } from "@clerk/clerk-sdk-node";
+import { ApiHandler, useJsonBody } from 'sst/node/api';
+const clerkClient = Clerk({ secretKey: Config.CLERK_SECRET_KEY });
const client = createClient({ url: Config.DATABASE_URL, authToken: Config.DATABASE_AUTH_TOKEN });
const db = drizzle(client);
const event = defineEvent(extractRepositoryEvent);
+const fetchSourceControlAccessToken = async (userId: string, forgeryIdProvider: 'oauth_github' | 'oauth_gitlab') => {
+ const [userOauthAccessTokenPayload, ...rest] = await clerkClient.users.getUserOauthAccessToken(userId, forgeryIdProvider);
+ if (!userOauthAccessTokenPayload) throw new Error("Failed to get token");
+ if (rest.length !== 0) throw new Error("wtf ?");
+
+ return userOauthAccessTokenPayload.token;
+}
const context: Context = {
entities: {
@@ -22,25 +31,53 @@ const context: Context = {
namespaces,
},
integrations: {
- sourceControl: new GitlabSourceControl(Config.GITLAB_TOKEN),
+ sourceControl: null,
},
db,
};
+const contextSchema = z.object({
+ authorizer: z.object({
+ jwt: z.object({
+ claims: z.object({
+ sub: z.string(),
+ }),
+ }),
+ }),
+});
+
+type CTX = z.infer;
+
const inputSchema = z.object({
repositoryId: z.number(),
repositoryName: z.string(),
namespaceName: z.string(),
+ sourceControl: z.literal("gitlab").or(z.literal("github")),
});
type Input = z.infer;
-export const handler: APIGatewayProxyHandlerV2 = async (apiGatewayEvent) => {
+export const handler = ApiHandler(async (ev) => {
+
+ const body = useJsonBody() as unknown;
+
+ let lambdaContext: CTX;
+
+ try {
+ lambdaContext = contextSchema.parse(ev.requestContext);
+ } catch (error) {
+ return {
+ statusCode: 401,
+ body: JSON.stringify({ error: (error as Error).message }),
+ };
+ }
let input: Input;
+ let sourceControlAccessToken: string;
try {
- input = inputSchema.parse(apiGatewayEvent);
+ input = inputSchema.parse(body);
+
} catch (error) {
return {
statusCode: 400,
@@ -48,14 +85,32 @@ export const handler: APIGatewayProxyHandlerV2 = async (apiGatewayEvent) => {
};
}
- const { repositoryId, repositoryName, namespaceName } = input;
+ const { sub } = lambdaContext.authorizer.jwt.claims;
+
+
+ const { repositoryId, repositoryName, namespaceName, sourceControl } = input;
+
+ try {
+ sourceControlAccessToken = await fetchSourceControlAccessToken(sub, `oauth_${sourceControl}`);
+ } catch (error) {
+ return {
+ statusCode: 500,
+ body: JSON.stringify({ error: (error as Error).message }),
+ }
+ }
+
+ if (sourceControl === "gitlab") {
+ context.integrations.sourceControl = new GitlabSourceControl(sourceControlAccessToken);
+ } else if (sourceControl === "github") {
+ context.integrations.sourceControl = new GitHubSourceControl(sourceControlAccessToken);
+ }
const { repository, namespace } = await getRepository({ externalRepositoryId: repositoryId, repositoryName, namespaceName }, context);
- await event.publish({ repository, namespace }, { caller: 'extract-repository', timestamp: new Date().getTime(), version: 1 });
+ await event.publish({ repository, namespace }, { caller: 'extract-repository', timestamp: new Date().getTime(), version: 1, sourceControl, userId: sub });
return {
statusCode: 200,
body: JSON.stringify({})
};
-}
+});
diff --git a/apps/extract-stack/src/stack.ts b/apps/extract-stack/src/stack.ts
index a00e2cd27..fedd69f0d 100644
--- a/apps/extract-stack/src/stack.ts
+++ b/apps/extract-stack/src/stack.ts
@@ -1,6 +1,7 @@
import { Api, EventBus, Queue } from "sst/constructs";
import type { StackContext } from "sst/constructs";
import { Config } from "sst/constructs";
+import { z } from "zod";
export function ExtractStack({ stack }: StackContext) {
const bus = new EventBus(stack, "ExtractBus", {
@@ -16,15 +17,34 @@ export function ExtractStack({ stack }: StackContext) {
const DATABASE_URL = new Config.Secret(stack, "DATABASE_URL");
const DATABASE_AUTH_TOKEN = new Config.Secret(stack, "DATABASE_AUTH_TOKEN");
const GITLAB_TOKEN = new Config.Secret(stack, "GITLAB_TOKEN");
+ const CLERK_SECRET_KEY = new Config.Secret(stack, "CLERK_SECRET_KEY");
+
+ const ENVSchema = z.object({
+ CLERK_JWT_ISSUER: z.string(),
+ CLERK_JWT_AUDIENCE: z.string(),
+ });
+
+ const ENV = ENVSchema.parse(process.env);
const api = new Api(stack, "ExtractApi", {
defaults: {
+ authorizer: 'JwtAuthorizer',
function: {
- bind: [bus, DATABASE_URL, DATABASE_AUTH_TOKEN, GITLAB_TOKEN, queue],
+ bind: [bus, DATABASE_URL, DATABASE_AUTH_TOKEN, GITLAB_TOKEN, CLERK_SECRET_KEY, queue],
+ },
+ },
+ authorizers: {
+ JwtAuthorizer: {
+ type: "jwt",
+ identitySource: ["$request.header.Authorization"],
+ jwt: {
+ issuer: ENV.CLERK_JWT_ISSUER,
+ audience: [ENV.CLERK_JWT_AUDIENCE],
+ },
},
},
routes: {
- "POST /gitlab": "src/extract-repository.handler",
+ "POST /start": "src/extract-repository.handler",
},
});
diff --git a/apps/test-stack/package.json b/apps/test-stack/package.json
deleted file mode 100644
index b678fe4db..000000000
--- a/apps/test-stack/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "@acme/test-stack",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {},
- "keywords": [],
- "author": "",
- "license": "ISC"
-}
diff --git a/package-lock.json b/package-lock.json
index 6256e990b..5e705c221 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -71,6 +71,7 @@
"@acme/extract-functions": "^1.0.0",
"@acme/extract-schema": "^1.0.0",
"@acme/source-control": "^1.0.0",
+ "@clerk/clerk-sdk-node": "^4.12.2",
"@libsql/client": "^0.3.1",
"@tsconfig/node16": "^16.1.0",
"aws-cdk-lib": "2.84.0",
@@ -81,7 +82,8 @@
"zod": "^3.21.4"
},
"devDependencies": {
- "@types/aws-lambda": "^8.10.119"
+ "@types/aws-lambda": "^8.10.119",
+ "dotenv-cli": "^7.2.1"
}
},
"apps/extract-stack/node_modules/@tsconfig/node16": {
@@ -92,6 +94,7 @@
"apps/test-stack": {
"name": "@acme/test-stack",
"version": "1.0.0",
+ "extraneous": true,
"license": "ISC"
},
"node_modules/@acme/eslint-config": {
@@ -122,10 +125,6 @@
"resolved": "packages/config/tailwind",
"link": true
},
- "node_modules/@acme/test-stack": {
- "resolved": "apps/test-stack",
- "link": true
- },
"node_modules/@alcalzone/ansi-tokenize": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.1.tgz",
@@ -2576,17 +2575,16 @@
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@clerk/clerk-sdk-node": {
- "version": "4.10.15",
- "resolved": "https://registry.npmjs.org/@clerk/clerk-sdk-node/-/clerk-sdk-node-4.10.15.tgz",
- "integrity": "sha512-fif8iwedLDFgllPLx8xwxA66ICbp7KjsSULqb8mcKXHPnZ/xL17eZSx4puHg4CEGBmOxY6IL5nbRj1+s+TfgqQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@clerk/clerk-sdk-node/-/clerk-sdk-node-4.12.2.tgz",
+ "integrity": "sha512-7xYPsLSeGO5XoP0No/9m2dsCMezwtmiYGKOwWzt41ZzJNFlU0rfqYF3VOZEsbtQlc3ZXeU+67ItjoJYrf3kT6A==",
"dependencies": {
- "@clerk/backend": "^0.24.0",
- "@clerk/types": "^3.46.1",
+ "@clerk/backend": "^0.27.0",
+ "@clerk/types": "^3.49.0",
"@types/cookies": "0.7.7",
"@types/express": "4.17.14",
"@types/node-fetch": "2.6.2",
"camelcase-keys": "6.2.2",
- "cookie": "0.5.0",
"snakecase-keys": "3.2.1",
"tslib": "2.4.1"
},
@@ -2594,6 +2592,42 @@
"node": ">=14"
}
},
+ "node_modules/@clerk/clerk-sdk-node/node_modules/@clerk/backend": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-0.27.0.tgz",
+ "integrity": "sha512-Sj541JrpqAn1A/UwdyDBxFV3stq2A/Pe/8HdPTG3Cct6briPyavfi46O5s1+L3BSvUcKUY+UbM0+8VsoCNFi4w==",
+ "dependencies": {
+ "@clerk/types": "^3.49.0",
+ "@peculiar/webcrypto": "1.4.1",
+ "@types/node": "16.18.6",
+ "cookie": "0.5.0",
+ "deepmerge": "4.2.2",
+ "node-fetch-native": "1.0.1",
+ "snakecase-keys": "5.4.4",
+ "tslib": "2.4.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@clerk/clerk-sdk-node/node_modules/@clerk/backend/node_modules/snakecase-keys": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-5.4.4.tgz",
+ "integrity": "sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==",
+ "dependencies": {
+ "map-obj": "^4.1.0",
+ "snake-case": "^3.0.4",
+ "type-fest": "^2.5.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@clerk/clerk-sdk-node/node_modules/@types/node": {
+ "version": "16.18.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.6.tgz",
+ "integrity": "sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA=="
+ },
"node_modules/@clerk/clerk-sdk-node/node_modules/snakecase-keys": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-3.2.1.tgz",
@@ -2611,6 +2645,17 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
+ "node_modules/@clerk/clerk-sdk-node/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@clerk/nextjs": {
"version": "4.21.15",
"resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-4.21.15.tgz",
@@ -2651,9 +2696,9 @@
}
},
"node_modules/@clerk/types": {
- "version": "3.46.1",
- "resolved": "https://registry.npmjs.org/@clerk/types/-/types-3.46.1.tgz",
- "integrity": "sha512-IA/iSXJJZym4Z6f9OcT9OJayK2opVhxUVJky15aY4iVsJCPScgApBuuzC7N4vfXwA6RI/wGebFbvmsEQ3oI/UA==",
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/@clerk/types/-/types-3.49.0.tgz",
+ "integrity": "sha512-vAx5R/iYfsgIaIDMiDr6ZKQnAneAmRrUVYz6KCtPG6/hnEAnRYhwXpEUi89e5G0BFmuUfSxe/N/Anfc1PNteXQ==",
"dependencies": {
"csstype": "3.1.1"
},
@@ -10504,8 +10549,9 @@
},
"node_modules/dotenv-cli": {
"version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz",
+ "integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"dotenv": "^16.0.0",
diff --git a/packages/functions/extract/src/config.ts b/packages/functions/extract/src/config.ts
index 5cce5b3a7..638f8ae92 100644
--- a/packages/functions/extract/src/config.ts
+++ b/packages/functions/extract/src/config.ts
@@ -13,7 +13,7 @@ export type Entities = {
export type Context, E extends Partial> = {
integrations: {
- sourceControl: SC;
+ sourceControl: SC | null;
};
db: Database;
entities: E;
diff --git a/packages/functions/extract/src/get-merge-requests.ts b/packages/functions/extract/src/get-merge-requests.ts
index 11e4b9ae5..6de930897 100644
--- a/packages/functions/extract/src/get-merge-requests.ts
+++ b/packages/functions/extract/src/get-merge-requests.ts
@@ -25,6 +25,11 @@ export const getMergeRequests: GetMergeRequestsFunction = async (
{ externalRepositoryId, namespaceName, repositoryName, repositoryId },
{ integrations, db, entities }
) => {
+
+ if(!integrations.sourceControl) {
+ throw new Error("Source control integration not configured");
+ }
+
const { mergeRequests, pagination } = await integrations.sourceControl.fetchMergeRequests(externalRepositoryId, namespaceName, repositoryName, repositoryId);
const insertedMergeRequests = await db.insert(entities.mergeRequests).values(mergeRequests)
diff --git a/packages/functions/extract/src/get-repository.ts b/packages/functions/extract/src/get-repository.ts
index 2b2c22aff..7e9ba61f9 100644
--- a/packages/functions/extract/src/get-repository.ts
+++ b/packages/functions/extract/src/get-repository.ts
@@ -23,6 +23,10 @@ export const getRepository: GetRepositoryFunction = async (
{ integrations, db, entities }
) => {
+ if(!integrations.sourceControl) {
+ throw new Error("Source control integration not configured");
+ }
+
const { repository, namespace } = await integrations.sourceControl.fetchRepository(externalRepositoryId, namespaceName, repositoryName);
const insertedRepository = await db.insert(entities.repositories).values(repository)
diff --git a/packages/integrations/source-control/src/gitlab/index.ts b/packages/integrations/source-control/src/gitlab/index.ts
index d623369f8..a2e217d08 100644
--- a/packages/integrations/source-control/src/gitlab/index.ts
+++ b/packages/integrations/source-control/src/gitlab/index.ts
@@ -8,7 +8,7 @@ export class GitlabSourceControl implements SourceControl {
constructor(token: string) {
this.api = new Gitlab({
- token,
+ oauthToken: token,
// camelize: true
});
}