feature(auth): replace clerk.com with custom auth (#103)
* feature(auth): replace clerk.com with custom auth * minor fixes * remove notification preferences * decrease live events interval fix(api): cookies.. # Conflicts: # .gitignore # apps/api/src/index.ts # apps/dashboard/src/app/providers.tsx # packages/trpc/src/trpc.ts
This commit is contained in:
committed by
Carl-Gerhard Lindesvärd
parent
f28802b1c2
commit
d31d9924a5
@@ -1,4 +1,3 @@
|
||||
import { validateClerkJwt } from '@/utils/auth';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import superjson from 'superjson';
|
||||
import type * as WebSocket from 'ws';
|
||||
@@ -124,18 +123,24 @@ export async function wsProjectEvents(
|
||||
}>,
|
||||
) {
|
||||
const { params, query } = req;
|
||||
const { token } = query;
|
||||
const type = query.type || 'saved';
|
||||
const subscribeToEvent = `event:${type}`;
|
||||
|
||||
if (!['saved', 'received'].includes(type)) {
|
||||
connection.socket.send('Invalid type');
|
||||
connection.socket.close();
|
||||
return;
|
||||
}
|
||||
const subscribeToEvent = `event:${type}`;
|
||||
const decoded = validateClerkJwt(token);
|
||||
const userId = decoded?.sub;
|
||||
|
||||
const userId = req.session?.userId;
|
||||
if (!userId) {
|
||||
connection.socket.send('No active session');
|
||||
connection.socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const access = await getProjectAccess({
|
||||
userId: userId!,
|
||||
userId,
|
||||
projectId: params.projectId,
|
||||
});
|
||||
|
||||
@@ -185,18 +190,18 @@ export async function wsProjectNotifications(
|
||||
}>,
|
||||
) {
|
||||
const { params, query } = req;
|
||||
const userId = req.session?.userId;
|
||||
|
||||
if (!query.token) {
|
||||
connection.socket.send('No token provided');
|
||||
if (!userId) {
|
||||
connection.socket.send('No active session');
|
||||
connection.socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const subscribeToEvent = 'notification';
|
||||
const decoded = validateClerkJwt(query.token);
|
||||
const userId = decoded?.sub;
|
||||
|
||||
const access = await getProjectAccess({
|
||||
userId: userId!,
|
||||
userId,
|
||||
projectId: params.projectId,
|
||||
});
|
||||
|
||||
|
||||
359
apps/api/src/controllers/oauth-callback.controller.tsx
Normal file
359
apps/api/src/controllers/oauth-callback.controller.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import {
|
||||
Arctic,
|
||||
type OAuth2Tokens,
|
||||
createSession,
|
||||
generateSessionToken,
|
||||
github,
|
||||
google,
|
||||
setSessionTokenCookie,
|
||||
} from '@openpanel/auth';
|
||||
import { type User, connectUserToOrganization, db } from '@openpanel/db';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
|
||||
async function getGithubEmail(githubAccessToken: string) {
|
||||
const emailListRequest = new Request('https://api.github.com/user/emails');
|
||||
emailListRequest.headers.set('Authorization', `Bearer ${githubAccessToken}`);
|
||||
const emailListResponse = await fetch(emailListRequest);
|
||||
const emailListResult: unknown = await emailListResponse.json();
|
||||
if (!Array.isArray(emailListResult) || emailListResult.length < 1) {
|
||||
return null;
|
||||
}
|
||||
let email: string | null = null;
|
||||
for (const emailRecord of emailListResult) {
|
||||
const emailParser = z.object({
|
||||
primary: z.boolean(),
|
||||
verified: z.boolean(),
|
||||
email: z.string(),
|
||||
});
|
||||
const emailResult = emailParser.safeParse(emailRecord);
|
||||
if (!emailResult.success) {
|
||||
continue;
|
||||
}
|
||||
if (emailResult.data.primary && emailResult.data.verified) {
|
||||
email = emailResult.data.email;
|
||||
}
|
||||
}
|
||||
return email;
|
||||
}
|
||||
|
||||
export async function githubCallback(
|
||||
req: FastifyRequest<{
|
||||
Querystring: {
|
||||
code: string;
|
||||
state: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const schema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
inviteId: z.string().nullish(),
|
||||
});
|
||||
|
||||
const query = schema.safeParse(req.query);
|
||||
if (!query.success) {
|
||||
return reply.status(400).send(query.error.message);
|
||||
}
|
||||
const { code, state, inviteId } = query.data;
|
||||
const storedState = req.cookies.github_oauth_state ?? null;
|
||||
|
||||
if (code === null || state === null || storedState === null) {
|
||||
return new Response('Please restart the process.', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (state !== storedState) {
|
||||
return new Response('Please restart the process.', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
let tokens: OAuth2Tokens;
|
||||
try {
|
||||
tokens = await github.validateAuthorizationCode(code);
|
||||
} catch {
|
||||
// Invalid code or client credentials
|
||||
return new Response('Please restart the process.', {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const githubAccessToken = tokens.accessToken();
|
||||
|
||||
const userRequest = new Request('https://api.github.com/user');
|
||||
userRequest.headers.set('Authorization', `Bearer ${githubAccessToken}`);
|
||||
const userResponse = await fetch(userRequest);
|
||||
|
||||
const userSchema = z.object({
|
||||
id: z.number(),
|
||||
login: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
const userJson = await userResponse.json();
|
||||
|
||||
const userResult = userSchema.safeParse(userJson);
|
||||
if (!userResult.success) {
|
||||
return reply.status(400).send(userResult.error.message);
|
||||
}
|
||||
const githubUserId = userResult.data.id;
|
||||
const email = await getGithubEmail(githubAccessToken);
|
||||
|
||||
const existingUser = await db.account.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
provider: 'github',
|
||||
providerId: String(githubUserId),
|
||||
},
|
||||
{
|
||||
provider: 'oauth',
|
||||
user: {
|
||||
email: email ?? '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser !== null) {
|
||||
const sessionToken = generateSessionToken();
|
||||
const session = await createSession(sessionToken, existingUser.userId);
|
||||
|
||||
if (existingUser.provider === 'oauth') {
|
||||
await db.account.update({
|
||||
where: {
|
||||
id: existingUser.id,
|
||||
},
|
||||
data: {
|
||||
provider: 'github',
|
||||
providerId: String(githubUserId),
|
||||
},
|
||||
});
|
||||
} else if (existingUser.provider !== 'github') {
|
||||
await db.account.create({
|
||||
data: {
|
||||
provider: 'github',
|
||||
providerId: String(githubUserId),
|
||||
user: {
|
||||
connect: {
|
||||
id: existingUser.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setSessionTokenCookie(
|
||||
(...args) => reply.setCookie(...args),
|
||||
sessionToken,
|
||||
session.expiresAt,
|
||||
);
|
||||
return reply.status(302).redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!);
|
||||
}
|
||||
|
||||
if (email === null) {
|
||||
return reply.status(400).send('Please verify your GitHub email address.');
|
||||
}
|
||||
|
||||
// (githubUserId, email, username);
|
||||
const user = await await db.user.create({
|
||||
data: {
|
||||
email,
|
||||
firstName: userResult.data.name,
|
||||
accounts: {
|
||||
create: {
|
||||
provider: 'github',
|
||||
providerId: String(githubUserId),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (inviteId) {
|
||||
try {
|
||||
await connectUserToOrganization({ user, inviteId });
|
||||
} catch (error) {
|
||||
req.log.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error connecting user to projects',
|
||||
{
|
||||
inviteId,
|
||||
email: user.email,
|
||||
error,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
const session = await createSession(sessionToken, user.id);
|
||||
setSessionTokenCookie(
|
||||
(...args) => reply.setCookie(...args),
|
||||
sessionToken,
|
||||
session.expiresAt,
|
||||
);
|
||||
return reply.status(302).redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!);
|
||||
}
|
||||
|
||||
export async function googleCallback(
|
||||
req: FastifyRequest<{
|
||||
Querystring: {
|
||||
code: string;
|
||||
state: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const schema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
inviteId: z.string().nullish(),
|
||||
});
|
||||
|
||||
const query = schema.safeParse(req.query);
|
||||
if (!query.success) {
|
||||
return reply.status(400).send(query.error.message);
|
||||
}
|
||||
|
||||
const { code, state, inviteId } = query.data;
|
||||
const storedState = req.cookies.google_oauth_state ?? null;
|
||||
const codeVerifier = req.cookies.google_code_verifier ?? null;
|
||||
|
||||
if (
|
||||
code === null ||
|
||||
state === null ||
|
||||
storedState === null ||
|
||||
codeVerifier === null
|
||||
) {
|
||||
return reply.status(400).send('Please restart the process.');
|
||||
}
|
||||
if (state !== storedState) {
|
||||
return reply.status(400).send('Please restart the process.');
|
||||
}
|
||||
|
||||
let tokens: OAuth2Tokens;
|
||||
try {
|
||||
tokens = await google.validateAuthorizationCode(code, codeVerifier);
|
||||
} catch {
|
||||
return reply.status(400).send('Please restart the process.');
|
||||
}
|
||||
|
||||
const claims = Arctic.decodeIdToken(tokens.idToken());
|
||||
|
||||
const claimsParser = z.object({
|
||||
sub: z.string(),
|
||||
given_name: z.string(),
|
||||
family_name: z.string(),
|
||||
picture: z.string(),
|
||||
email: z.string(),
|
||||
});
|
||||
|
||||
const claimsResult = claimsParser.safeParse(claims);
|
||||
if (!claimsResult.success) {
|
||||
return reply.status(400).send(claimsResult.error.message);
|
||||
}
|
||||
|
||||
const { sub: googleId, given_name, family_name, email } = claimsResult.data;
|
||||
|
||||
const existingAccount = await db.account.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
provider: 'google',
|
||||
providerId: googleId,
|
||||
},
|
||||
{
|
||||
provider: 'oauth',
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAccount !== null) {
|
||||
const sessionToken = generateSessionToken();
|
||||
const session = await createSession(sessionToken, existingAccount.userId);
|
||||
|
||||
if (existingAccount.provider === 'oauth') {
|
||||
await db.account.update({
|
||||
where: {
|
||||
id: existingAccount.id,
|
||||
},
|
||||
data: {
|
||||
provider: 'google',
|
||||
providerId: googleId,
|
||||
},
|
||||
});
|
||||
} else if (existingAccount.provider !== 'google') {
|
||||
await db.account.create({
|
||||
data: {
|
||||
provider: 'google',
|
||||
providerId: googleId,
|
||||
user: {
|
||||
connect: {
|
||||
id: existingAccount.userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setSessionTokenCookie(
|
||||
(...args) => reply.setCookie(...args),
|
||||
sessionToken,
|
||||
session.expiresAt,
|
||||
);
|
||||
return reply.status(302).redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!);
|
||||
}
|
||||
|
||||
const user = await db.user.upsert({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
update: {
|
||||
firstName: given_name,
|
||||
lastName: family_name,
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
firstName: given_name,
|
||||
lastName: family_name,
|
||||
accounts: {
|
||||
create: {
|
||||
provider: 'google',
|
||||
providerId: googleId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (inviteId) {
|
||||
try {
|
||||
await connectUserToOrganization({ user, inviteId });
|
||||
} catch (error) {
|
||||
req.log.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error connecting user to projects',
|
||||
{
|
||||
inviteId,
|
||||
email: user.email,
|
||||
error,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
const session = await createSession(sessionToken, user.id);
|
||||
setSessionTokenCookie(
|
||||
(...args) => reply.setCookie(...args),
|
||||
sessionToken,
|
||||
session.expiresAt,
|
||||
);
|
||||
return reply.status(302).redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!);
|
||||
}
|
||||
@@ -1,164 +1,14 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import type { WebhookEvent } from '@clerk/fastify';
|
||||
import { AccessLevel, db } from '@openpanel/db';
|
||||
import { db } from '@openpanel/db';
|
||||
import {
|
||||
sendSlackNotification,
|
||||
slackInstaller,
|
||||
} from '@openpanel/integrations/src/slack';
|
||||
import { getRedisPub } from '@openpanel/redis';
|
||||
import { zSlackAuthResponse } from '@openpanel/validation';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { pathOr } from 'ramda';
|
||||
import { Webhook } from 'svix';
|
||||
import { z } from 'zod';
|
||||
|
||||
if (!process.env.CLERK_SIGNING_SECRET) {
|
||||
throw new Error('CLERK_SIGNING_SECRET is required');
|
||||
}
|
||||
|
||||
const wh = new Webhook(process.env.CLERK_SIGNING_SECRET);
|
||||
|
||||
function verify(body: any, headers: FastifyRequest['headers']) {
|
||||
try {
|
||||
const svix_id = headers['svix-id'] as string;
|
||||
const svix_timestamp = headers['svix-timestamp'] as string;
|
||||
const svix_signature = headers['svix-signature'] as string;
|
||||
|
||||
wh.verify(JSON.stringify(body), {
|
||||
'svix-id': svix_id,
|
||||
'svix-timestamp': svix_timestamp,
|
||||
'svix-signature': svix_signature,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function clerkWebhook(
|
||||
request: FastifyRequest<{
|
||||
Body: WebhookEvent;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const payload = request.body;
|
||||
const verified = verify(payload, request.headers);
|
||||
|
||||
if (!verified) {
|
||||
return reply.send({ message: 'Invalid signature' });
|
||||
}
|
||||
|
||||
if (payload.type === 'user.created') {
|
||||
const email = payload.data.email_addresses[0]?.email_address;
|
||||
const emails = payload.data.email_addresses.map((e) => e.email_address);
|
||||
|
||||
if (!email) {
|
||||
return Response.json(
|
||||
{ message: 'No email address found' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const user = await db.user.create({
|
||||
data: {
|
||||
id: payload.data.id,
|
||||
email,
|
||||
firstName: payload.data.first_name,
|
||||
lastName: payload.data.last_name,
|
||||
},
|
||||
});
|
||||
|
||||
const memberships = await db.member.findMany({
|
||||
where: {
|
||||
email: {
|
||||
in: emails,
|
||||
},
|
||||
userId: null,
|
||||
},
|
||||
});
|
||||
|
||||
for (const membership of memberships) {
|
||||
const access = pathOr<string[]>([], ['meta', 'access'], membership);
|
||||
await db.$transaction([
|
||||
// Update the member to link it to the user
|
||||
// This will remove the item from invitations
|
||||
db.member.update({
|
||||
where: {
|
||||
id: membership.id,
|
||||
},
|
||||
data: {
|
||||
userId: user.id,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.createMany({
|
||||
data: access
|
||||
.filter((a) => typeof a === 'string')
|
||||
.map((projectId) => ({
|
||||
organizationId: membership.organizationId,
|
||||
projectId: projectId,
|
||||
userId: user.id,
|
||||
level: AccessLevel.read,
|
||||
})),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'organizationMembership.created') {
|
||||
const access = payload.data.public_metadata.access;
|
||||
if (Array.isArray(access)) {
|
||||
await db.projectAccess.createMany({
|
||||
data: access
|
||||
.filter((a): a is string => typeof a === 'string')
|
||||
.map((projectId) => ({
|
||||
organizationId: payload.data.organization.slug,
|
||||
projectId: projectId,
|
||||
userId: payload.data.public_user_data.user_id,
|
||||
level: AccessLevel.read,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'user.deleted') {
|
||||
await db.$transaction([
|
||||
db.user.update({
|
||||
where: {
|
||||
id: payload.data.id,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
},
|
||||
}),
|
||||
db.projectAccess.deleteMany({
|
||||
where: {
|
||||
userId: payload.data.id,
|
||||
},
|
||||
}),
|
||||
db.member.deleteMany({
|
||||
where: {
|
||||
userId: payload.data.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
if (payload.type === 'organizationMembership.deleted') {
|
||||
await db.projectAccess.deleteMany({
|
||||
where: {
|
||||
organizationId: payload.data.organization.slug,
|
||||
userId: payload.data.public_user_data.user_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
reply.send({ success: true });
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
@@ -172,7 +22,7 @@ const metadataSchema = z.object({
|
||||
|
||||
export async function slackWebhook(
|
||||
request: FastifyRequest<{
|
||||
Querystring: WebhookEvent;
|
||||
Querystring: unknown;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import zlib from 'node:zlib';
|
||||
import { clerkPlugin } from '@clerk/fastify';
|
||||
import compress from '@fastify/compress';
|
||||
import cookie from '@fastify/cookie';
|
||||
import cors, { type FastifyCorsOptions } from '@fastify/cors';
|
||||
@@ -8,14 +7,18 @@ import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';
|
||||
import type { FastifyBaseLogger, FastifyRequest } from 'fastify';
|
||||
import Fastify from 'fastify';
|
||||
import metricsPlugin from 'fastify-metrics';
|
||||
import { path, pick } from 'ramda';
|
||||
|
||||
import { generateId } from '@openpanel/common';
|
||||
import type { IServiceClient, IServiceClientWithProject } from '@openpanel/db';
|
||||
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||
import { getRedisPub } from '@openpanel/redis';
|
||||
import type { AppRouter } from '@openpanel/trpc';
|
||||
import { appRouter, createContext } from '@openpanel/trpc';
|
||||
|
||||
import {
|
||||
EMPTY_SESSION,
|
||||
type SessionValidationResult,
|
||||
validateSessionToken,
|
||||
} from '@openpanel/auth';
|
||||
import sourceMapSupport from 'source-map-support';
|
||||
import {
|
||||
healthcheck,
|
||||
@@ -30,6 +33,7 @@ import exportRouter from './routes/export.router';
|
||||
import importRouter from './routes/import.router';
|
||||
import liveRouter from './routes/live.router';
|
||||
import miscRouter from './routes/misc.router';
|
||||
import oauthRouter from './routes/oauth-callback.router';
|
||||
import profileRouter from './routes/profile.router';
|
||||
import trackRouter from './routes/track.router';
|
||||
import webhookRouter from './routes/webhook.router';
|
||||
@@ -42,6 +46,7 @@ declare module 'fastify' {
|
||||
client: IServiceClientWithProject | null;
|
||||
clientIp?: string;
|
||||
timestamp?: number;
|
||||
session: SessionValidationResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +66,31 @@ const startServer = async () => {
|
||||
: generateId(),
|
||||
});
|
||||
|
||||
fastify.register(cors, () => {
|
||||
return (
|
||||
req: FastifyRequest,
|
||||
callback: (error: Error | null, options: FastifyCorsOptions) => void,
|
||||
) => {
|
||||
// TODO: set prefix on dashboard routes
|
||||
const corsPaths = ['/trpc', '/live', '/webhook', '/oauth', '/misc'];
|
||||
|
||||
const isPrivatePath = corsPaths.some((path) =>
|
||||
req.url.startsWith(path),
|
||||
);
|
||||
|
||||
if (isPrivatePath) {
|
||||
return callback(null, {
|
||||
origin: process.env.NEXT_PUBLIC_DASHBOARD_URL,
|
||||
credentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
origin: '*',
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
fastify.addHook('preHandler', ipHook);
|
||||
fastify.addHook('preHandler', timestampHook);
|
||||
fastify.addHook('onRequest', requestIdHook);
|
||||
@@ -105,40 +135,34 @@ const startServer = async () => {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.register(cors, () => {
|
||||
return (
|
||||
req: FastifyRequest,
|
||||
callback: (error: Error | null, options: FastifyCorsOptions) => void,
|
||||
) => {
|
||||
// TODO: set prefix on dashboard routes
|
||||
const corsPaths = ['/trpc', '/live', '/webhook', '/oauth', '/misc'];
|
||||
|
||||
const isPrivatePath = corsPaths.some((path) =>
|
||||
req.url.startsWith(path),
|
||||
);
|
||||
|
||||
if (isPrivatePath) {
|
||||
return callback(null, {
|
||||
origin: process.env.NEXT_PUBLIC_DASHBOARD_URL,
|
||||
credentials: true,
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
origin: '*',
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Dashboard API
|
||||
fastify.register((instance, opts, done) => {
|
||||
fastify.register(cookie, {
|
||||
secret: 'random', // for cookies signature
|
||||
instance.register(cookie, {
|
||||
secret: process.env.COOKIE_SECRET ?? '',
|
||||
hook: 'onRequest',
|
||||
parseOptions: {},
|
||||
});
|
||||
instance.register(clerkPlugin, {
|
||||
publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
secretKey: process.env.CLERK_SECRET_KEY,
|
||||
|
||||
instance.addHook('onRequest', (req, reply, done) => {
|
||||
if (req.cookies?.session) {
|
||||
validateSessionToken(req.cookies.session)
|
||||
.then((session) => {
|
||||
if (session.session) {
|
||||
req.session = session;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
req.session = EMPTY_SESSION;
|
||||
})
|
||||
.finally(() => {
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
req.session = EMPTY_SESSION;
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
instance.register(fastifyTRPCPlugin, {
|
||||
prefix: '/trpc',
|
||||
trpcOptions: {
|
||||
@@ -155,22 +179,27 @@ const startServer = async () => {
|
||||
} satisfies FastifyTRPCPluginOptions<AppRouter>['trpcOptions'],
|
||||
});
|
||||
instance.register(liveRouter, { prefix: '/live' });
|
||||
instance.register(webhookRouter, { prefix: '/webhook' });
|
||||
instance.register(oauthRouter, { prefix: '/oauth' });
|
||||
instance.register(miscRouter, { prefix: '/misc' });
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register(metricsPlugin, { endpoint: '/metrics' });
|
||||
fastify.register(eventRouter, { prefix: '/event' });
|
||||
fastify.register(profileRouter, { prefix: '/profile' });
|
||||
fastify.register(miscRouter, { prefix: '/misc' });
|
||||
fastify.register(exportRouter, { prefix: '/export' });
|
||||
fastify.register(webhookRouter, { prefix: '/webhook' });
|
||||
fastify.register(importRouter, { prefix: '/import' });
|
||||
fastify.register(trackRouter, { prefix: '/track' });
|
||||
fastify.get('/', (_request, reply) =>
|
||||
reply.send({ name: 'openpanel sdk api' }),
|
||||
);
|
||||
fastify.get('/healthcheck', healthcheck);
|
||||
fastify.get('/healthcheck/queue', healthcheckQueue);
|
||||
// Public API
|
||||
fastify.register((instance, opts, done) => {
|
||||
instance.register(metricsPlugin, { endpoint: '/metrics' });
|
||||
instance.register(eventRouter, { prefix: '/event' });
|
||||
instance.register(profileRouter, { prefix: '/profile' });
|
||||
instance.register(exportRouter, { prefix: '/export' });
|
||||
instance.register(importRouter, { prefix: '/import' });
|
||||
instance.register(trackRouter, { prefix: '/track' });
|
||||
instance.get('/healthcheck', healthcheck);
|
||||
instance.get('/healthcheck/queue', healthcheckQueue);
|
||||
instance.get('/', (_request, reply) =>
|
||||
reply.send({ name: 'openpanel sdk api' }),
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.setErrorHandler((error, request, reply) => {
|
||||
if (error.statusCode === 429) {
|
||||
|
||||
18
apps/api/src/routes/oauth-callback.router.ts
Normal file
18
apps/api/src/routes/oauth-callback.router.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as controller from '@/controllers/oauth-callback.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
const router: FastifyPluginCallback = (fastify, opts, done) => {
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/github/callback',
|
||||
handler: controller.githubCallback,
|
||||
});
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/google/callback',
|
||||
handler: controller.googleCallback,
|
||||
});
|
||||
done();
|
||||
};
|
||||
|
||||
export default router;
|
||||
@@ -2,11 +2,6 @@ import * as controller from '@/controllers/webhook.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
const webhookRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/clerk',
|
||||
handler: controller.clerkWebhook,
|
||||
});
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/slack',
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { verifyPassword } from '@openpanel/common/server';
|
||||
import type {
|
||||
Client,
|
||||
IServiceClient,
|
||||
IServiceClientWithProject,
|
||||
} from '@openpanel/db';
|
||||
import { ClientType, db, getClientByIdCached } from '@openpanel/db';
|
||||
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||
import { ClientType, getClientByIdCached } from '@openpanel/db';
|
||||
import type { PostEventPayload, TrackHandlerPayload } from '@openpanel/sdk';
|
||||
import type {
|
||||
IProjectFilterIp,
|
||||
@@ -187,23 +182,3 @@ export async function validateImportRequest(
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export function validateClerkJwt(token?: string) {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
process.env.CLERK_PUBLIC_PEM_KEY!.replace(/\\n/g, '\n'),
|
||||
);
|
||||
|
||||
if (typeof decoded === 'object') {
|
||||
return decoded;
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user