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/schemas/node_modules \
packages/schemas/dist \
packages/crypto/node_modules \
packages/crypto/dist \
.sourcebot
.PHONY: bin

View file

@ -1,10 +1,6 @@
import dotenv from 'dotenv';
export const getEnv = (env: string | undefined, defaultValue?: string, required?: boolean) => {
if (required && !env && !defaultValue) {
throw new Error(`Missing required environment variable`);
}
export const getEnv = (env: string | undefined, defaultValue?: string) => {
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
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 } {
if (!SOURCEBOT_ENCRYPTION_KEY) {
throw new Error('Encryption key is not set');
}
const encryptionKey = Buffer.from(SOURCEBOT_ENCRYPTION_KEY, 'ascii');
const iv = generateIV();
@ -21,6 +25,10 @@ export function encrypt(text: string): { iv: string; encryptedData: 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 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 SCHEMAS: string[] = ["github.json", "shared.json"];
(async () => {
const cwd = process.cwd();
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) => {
const name = path.parse(schemaPath).name;
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 });
// Generate schema

View file

@ -47,9 +47,7 @@ export default async function Layout({
if (isServiceError(subscription) || (subscription.status !== "active" && subscription.status !== "trialing")) {
return (
<div className="flex flex-col items-center overflow-hidden min-h-screen">
<NavigationMenu
domain={domain}
/>
<NavigationMenu domain={domain} />
<PaywallCard domain={domain} />
<Footer />
</div>

View file

@ -20,8 +20,9 @@ const onboardingFormSchema = z.object({
domain: z.string()
.min(2, { message: "Organization domain must be at least 3 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(/^[^-].*[^-]$/, { message: "Organization domain must not start or end with a hyphen." }),
.regex(/^[a-z-]+$/, {
message: "Domain can only contain lowercase letters and dashes.",
}),
})
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 (
<div className="space-y-6">
<div className="flex justify-center">
@ -80,7 +87,14 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
<FormItem>
<FormLabel>Organization Name</FormLabel>
<FormControl>
<Input placeholder="Aperture Laboratories Inc." {...field} />
<Input
placeholder="Aperture Labs"
{...field}
onChange={(e) => {
field.onChange(e)
handleNameChange(e)
}}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -94,7 +108,7 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
<FormLabel>Organization Domain</FormLabel>
<FormControl>
<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>
</div>
</FormControl>

View file

@ -6,9 +6,7 @@ import { PrismaAdapter } from "@auth/prisma-adapter"
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 { User } from '@sourcebot/db';
import { notAuthenticated, notFound, unexpectedError } from "@/lib/serviceError";
import { getUser } from "./data/user";
import { LuToggleRight } from 'react-icons/lu';
import 'next-auth/jwt';
import type { Provider } from "next-auth/providers";
declare module 'next-auth' {
@ -49,8 +47,8 @@ export const providerMap = providers
.filter((provider) => provider.id !== "credentials");
const useSecureCookies = AUTH_URL.startsWith("https://");
const hostName = new URL(AUTH_URL).hostname;
const useSecureCookies = AUTH_URL?.startsWith("https://") ?? false;
const hostName = AUTH_URL ? new URL(AUTH_URL).hostname : "localhost";
export const { handlers, signIn, signOut, auth } = NextAuth({
secret: AUTH_SECRET,
@ -115,43 +113,3 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
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 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 { 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) => {
const host = request.headers.get("host")!;