mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-13 04:45:19 +00:00
merge v3 changes into billing branch
This commit is contained in:
commit
a70c57715c
9 changed files with 38 additions and 76 deletions
2
Makefile
2
Makefile
|
|
@ -24,6 +24,8 @@ clean:
|
||||||
packages/db/dist \
|
packages/db/dist \
|
||||||
packages/schemas/node_modules \
|
packages/schemas/node_modules \
|
||||||
packages/schemas/dist \
|
packages/schemas/dist \
|
||||||
|
packages/crypto/node_modules \
|
||||||
|
packages/crypto/dist \
|
||||||
.sourcebot
|
.sourcebot
|
||||||
|
|
||||||
.PHONY: bin
|
.PHONY: bin
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
|
export const getEnv = (env: string | undefined, defaultValue?: string) => {
|
||||||
if (required && !env && !defaultValue) {
|
|
||||||
throw new Error(`Missing required environment variable`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return env ?? defaultValue;
|
return env ?? defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,4 +10,4 @@ dotenv.config({
|
||||||
});
|
});
|
||||||
|
|
||||||
// @note: You can use https://generate-random.org/encryption-key-generator to create a new 32 byte key
|
// @note: You can use https://generate-random.org/encryption-key-generator to create a new 32 byte key
|
||||||
export const SOURCEBOT_ENCRYPTION_KEY = getEnv(process.env.SOURCEBOT_ENCRYPTION_KEY, undefined, true)!;
|
export const SOURCEBOT_ENCRYPTION_KEY = getEnv(process.env.SOURCEBOT_ENCRYPTION_KEY);
|
||||||
|
|
@ -9,6 +9,10 @@ const generateIV = (): Buffer => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||||
|
if (!SOURCEBOT_ENCRYPTION_KEY) {
|
||||||
|
throw new Error('Encryption key is not set');
|
||||||
|
}
|
||||||
|
|
||||||
const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii');
|
const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii');
|
||||||
|
|
||||||
const iv = generateIV();
|
const iv = generateIV();
|
||||||
|
|
@ -21,6 +25,10 @@ export function encrypt(text: string): { iv: string; encryptedData: string } {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(iv: string, encryptedText: string): string {
|
export function decrypt(iv: string, encryptedText: string): string {
|
||||||
|
if (!SOURCEBOT_ENCRYPTION_KEY) {
|
||||||
|
throw new Error('Encryption key is not set');
|
||||||
|
}
|
||||||
|
|
||||||
const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii');
|
const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii');
|
||||||
|
|
||||||
const ivBuffer = Buffer.from(iv, 'hex');
|
const ivBuffer = Buffer.from(iv, 'hex');
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,18 @@ import { glob } from "glob";
|
||||||
|
|
||||||
|
|
||||||
const BANNER_COMMENT = '// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!\n';
|
const BANNER_COMMENT = '// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!\n';
|
||||||
// const SCHEMAS: string[] = ["github.json", "shared.json"];
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
const schemasBasePath = path.resolve(`${cwd}/../../schemas`);
|
const schemasBasePath = path.resolve(`${cwd}/../../schemas`);
|
||||||
const schemas = await glob(`${schemasBasePath}/**/*.json`)
|
const outDirRoot = path.resolve(`${cwd}/src`);
|
||||||
|
const schemas = await glob(`${schemasBasePath}/**/*.json`);
|
||||||
|
|
||||||
await Promise.all(schemas.map(async (schemaPath) => {
|
await Promise.all(schemas.map(async (schemaPath) => {
|
||||||
const name = path.parse(schemaPath).name;
|
const name = path.parse(schemaPath).name;
|
||||||
const version = path.basename(path.dirname(schemaPath));
|
const version = path.basename(path.dirname(schemaPath));
|
||||||
const outDir = path.join(cwd, `src/${version}`);
|
const outDir = path.join(outDirRoot, version);
|
||||||
|
|
||||||
// Clean output directory first
|
|
||||||
await rm(outDir, { recursive: true, force: true });
|
|
||||||
await mkdir(outDir, { recursive: true });
|
await mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
// Generate schema
|
// Generate schema
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,7 @@ export default async function Layout({
|
||||||
if (isServiceError(subscription) || (subscription.status !== "active" && subscription.status !== "trialing")) {
|
if (isServiceError(subscription) || (subscription.status !== "active" && subscription.status !== "trialing")) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
||||||
<NavigationMenu
|
<NavigationMenu domain={domain} />
|
||||||
domain={domain}
|
|
||||||
/>
|
|
||||||
<PaywallCard domain={domain} />
|
<PaywallCard domain={domain} />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,9 @@ const onboardingFormSchema = z.object({
|
||||||
domain: z.string()
|
domain: z.string()
|
||||||
.min(2, { message: "Organization domain must be at least 3 characters long." })
|
.min(2, { message: "Organization domain must be at least 3 characters long." })
|
||||||
.max(20, { message: "Organization domain must be at most 20 characters long." })
|
.max(20, { message: "Organization domain must be at most 20 characters long." })
|
||||||
.regex(/^[a-zA-Z-]+$/, { message: "Organization domain must contain only letters and hyphens." })
|
.regex(/^[a-z-]+$/, {
|
||||||
.regex(/^[^-].*[^-]$/, { message: "Organization domain must not start or end with a hyphen." }),
|
message: "Domain can only contain lowercase letters and dashes.",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type OnboardingFormValues = z.infer<typeof onboardingFormSchema>
|
export type OnboardingFormValues = z.infer<typeof onboardingFormSchema>
|
||||||
|
|
@ -54,6 +55,12 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const name = e.target.value
|
||||||
|
const domain = name.toLowerCase().replace(/\s+/g, "-")
|
||||||
|
form.setValue("domain", domain)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
|
@ -80,7 +87,14 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Organization Name</FormLabel>
|
<FormLabel>Organization Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Aperture Laboratories Inc." {...field} />
|
<Input
|
||||||
|
placeholder="Aperture Labs"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
field.onChange(e)
|
||||||
|
handleNameChange(e)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -94,7 +108,7 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
|
||||||
<FormLabel>Organization Domain</FormLabel>
|
<FormLabel>Organization Domain</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Input placeholder="aperature" {...field} className="w-1/2" />
|
<Input placeholder="aperature-labs" {...field} className="w-1/2" />
|
||||||
<span className="ml-2">.sourcebot.dev</span>
|
<span className="ml-2">.sourcebot.dev</span>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ import { PrismaAdapter } from "@auth/prisma-adapter"
|
||||||
import { prisma } from "@/prisma";
|
import { prisma } from "@/prisma";
|
||||||
import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, AUTH_SECRET, AUTH_URL } from "./lib/environment";
|
import { AUTH_GITHUB_CLIENT_ID, AUTH_GITHUB_CLIENT_SECRET, AUTH_GOOGLE_CLIENT_ID, AUTH_GOOGLE_CLIENT_SECRET, AUTH_SECRET, AUTH_URL } from "./lib/environment";
|
||||||
import { User } from '@sourcebot/db';
|
import { User } from '@sourcebot/db';
|
||||||
import { notAuthenticated, notFound, unexpectedError } from "@/lib/serviceError";
|
import 'next-auth/jwt';
|
||||||
import { getUser } from "./data/user";
|
|
||||||
import { LuToggleRight } from 'react-icons/lu';
|
|
||||||
import type { Provider } from "next-auth/providers";
|
import type { Provider } from "next-auth/providers";
|
||||||
|
|
||||||
declare module 'next-auth' {
|
declare module 'next-auth' {
|
||||||
|
|
@ -49,8 +47,8 @@ export const providerMap = providers
|
||||||
.filter((provider) => provider.id !== "credentials");
|
.filter((provider) => provider.id !== "credentials");
|
||||||
|
|
||||||
|
|
||||||
const useSecureCookies = AUTH_URL.startsWith("https://");
|
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false;
|
||||||
const hostName = new URL(AUTH_URL).hostname;
|
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost";
|
||||||
|
|
||||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
secret: AUTH_SECRET,
|
secret: AUTH_SECRET,
|
||||||
|
|
@ -115,43 +113,3 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
signIn: "/login"
|
signIn: "/login"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getCurrentUserOrg = async () => {
|
|
||||||
const session = await auth();
|
|
||||||
if (!session) {
|
|
||||||
return notAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
|
||||||
if (!user) {
|
|
||||||
return unexpectedError("User not found");
|
|
||||||
}
|
|
||||||
const orgId = user.activeOrgId;
|
|
||||||
if (!orgId) {
|
|
||||||
return unexpectedError("User has no active org");
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await prisma.userToOrg.findUnique({
|
|
||||||
where: {
|
|
||||||
orgId_userId: {
|
|
||||||
userId: session.user.id,
|
|
||||||
orgId,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!membership) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return orgId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const doesUserHaveOrg = async (userId: string) => {
|
|
||||||
const orgs = await prisma.userToOrg.findMany({
|
|
||||||
where: {
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return orgs.length > 0;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,4 @@ export const AUTH_GOOGLE_CLIENT_SECRET = getEnv(process.env.AUTH_GOOGLE_CLIENT_S
|
||||||
export const AUTH_URL = getEnv(process.env.AUTH_URL)!;
|
export const AUTH_URL = getEnv(process.env.AUTH_URL)!;
|
||||||
|
|
||||||
export const STRIPE_SECRET_KEY = getEnv(process.env.STRIPE_SECRET_KEY);
|
export const STRIPE_SECRET_KEY = getEnv(process.env.STRIPE_SECRET_KEY);
|
||||||
export const STRIPE_PRODUCT_ID = getEnv(process.env.STRIPE_PRODUCT_ID);
|
export const STRIPE_PRODUCT_ID = getEnv(process.env.STRIPE_PRODUCT_ID);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,6 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
|
|
||||||
/*
|
|
||||||
// We're not able to check if the user doesn't belong to any orgs in the middleware, since we cannot call prisma. As a result, we do this check
|
|
||||||
// in the root layout. However, there are certain endpoints (ex. login, redeem, onboard) that we want the user to be able to hit even if they don't
|
|
||||||
// belong to an org. It seems like the easiest way to do this is to check for these paths here and pass in a flag to the root layout using the headers
|
|
||||||
// https://github.com/vercel/next.js/discussions/43657#discussioncomment-5981981
|
|
||||||
const bypassOrgCheck = req.nextUrl.pathname === "/login" || req.nextUrl.pathname === "/redeem" || req.nextUrl.pathname.includes("onboard");
|
|
||||||
const bypassPaywall = req.nextUrl.pathname === "/login" || req.nextUrl.pathname === "/redeem" || req.nextUrl.pathname.includes("onboard") || req.nextUrl.pathname.includes("settings");
|
|
||||||
const requestheaders = new Headers(req.headers);
|
|
||||||
requestheaders.set("x-bypass-org-check", bypassOrgCheck.toString());
|
|
||||||
requestheaders.set("x-bypass-paywall", bypassPaywall.toString());
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default auth((request) => {
|
export default auth((request) => {
|
||||||
const host = request.headers.get("host")!;
|
const host = request.headers.get("host")!;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue