merge v3 changes into billing branch

This commit is contained in:
msukkari 2025-02-13 11:32:17 -08:00
commit a70c57715c
9 changed files with 38 additions and 76 deletions

View file

@ -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

View file

@ -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);

View file

@ -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');

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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);

View file

@ -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")!;