Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/app/languine.lock
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,14 @@ files:
auth.email.error: ed239af1d84d25fd9bb99251114b488c
auth.terms: bb73614638d9a468c878278692a71a5e
onboarding.title: deecac09e6560d6f0f98401d9852d514
onboarding.setup: ad2376beebecdcf7846ba973fa1a005b
onboarding.description: 7cd2a7692cfacbbea2045f2e6416b805
onboarding.submit: 85a18a0474d135c1f4c6da6c383a2d81
onboarding.setup: 54efd9605180a9a74e6d1a53529f858e
onboarding.description: fcfd2a71d9095421e290f1b1229d1c6e
onboarding.trigger.title: c0a683b95c32208c79a08309225647ee
onboarding.trigger.creating: 5c1f94a8b72c6f33012732e6b8ba44e5
onboarding.trigger.completed: 435e0545f0bbfbbbe640e9e12e54c663
onboarding.trigger.continue: 01739cdc86e75ee2fea0aabcdeb2e557
onboarding.trigger.error: 9e78ff392dee43a6c78a8c4e29ff4dc2
onboarding.fields.fullName.label: 614cffa523202658a898e34a5d94d05e
onboarding.fields.fullName.placeholder: df1fc96e5401396fe01e24363a9ec40d
onboarding.fields.name.label: c1ca926603dc454ba981aa514db8402b
Expand All @@ -214,6 +220,7 @@ files:
onboarding.check_availability: 118740af1c4521b917e29791d661db82
onboarding.available: 78945de8de090e90045d299651a68a9b
onboarding.unavailable: 453e6aa38d87b28ccae545967c53004f
onboarding.creating: 3ab00f2a7303dc9e1aed05d535677b7e
overview.title: 3b878279a04dc47d60932cb294d96259
overview.framework_chart.title: 9c53d5a3379ee2c9bc7c1c55f54724b9
overview.requirement_chart.title: 69d9304854b767112e32573d7eeddc24
Expand Down
3 changes: 2 additions & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next dev --turbopack --turbo -p 3001",
"dev": "npx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev --turbopack --turbo -p 3001\" \"npm run trigger:dev\"",
"trigger:dev": "npx trigger.dev@latest dev",
"build": "next build",
"start": "next start",
"lint": "biome lint ./src",
Expand Down
40 changes: 0 additions & 40 deletions apps/app/src/actions/organization/check-subdomain-availability.ts

This file was deleted.

174 changes: 56 additions & 118 deletions apps/app/src/actions/organization/create-organization-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,133 +3,71 @@
"use server";

import { createOrganizationAndConnectUser } from "@/auth/org";
import type { createDefaultPoliciesTask } from "@/jobs/tasks/organization/create-default-policies";
import {
addDomainToVercel,
removeDomainFromVercelProject,
} from "@/lib/domains";
import { db } from "@bubba/db";
import { tasks } from "@trigger.dev/sdk/v3";
import { revalidateTag } from "next/cache";
import { authActionClient } from "../safe-action";
import { organizationSchema } from "../schema";
import { tasks } from "@trigger.dev/sdk/v3";
import type { createOrganizationTask } from "@/jobs/tasks/organization/create-organization";

export const createOrganizationAction = authActionClient
.schema(organizationSchema)
.metadata({
name: "create-organization",
track: {
event: "create-organization",
channel: "server",
},
})
.action(async ({ parsedInput, ctx }) => {
const { name, website, subdomain } = parsedInput;
const { id: userId, organizationId } = ctx.user;

if (!name || !website) {
console.log("Invalid input detected:", { name, website });
throw new Error("Invalid user input");
}

const hasVercelConfig = Boolean(
process.env.NEXT_PUBLIC_VERCEL_URL &&
process.env.VERCEL_ACCESS_TOKEN &&
process.env.VERCEL_TEAM_ID &&
process.env.VERCEL_PROJECT_ID,
);

if (hasVercelConfig && subdomain) {
try {
await addDomainToVercel(
`${subdomain}.${process.env.NEXT_PUBLIC_VERCEL_URL}`,
);
} catch (error) {
console.error("Failed to add domain to Vercel:", error);
throw new Error("Failed to set up subdomain");
}
}

if (!organizationId) {
await createOrganizationAndConnectUser({
userId,
normalizedEmail: ctx.user.email!,
subdomain: hasVercelConfig ? subdomain || "" : "",
});
}

const organization = await db.organization.findFirst({
where: {
users: {
some: {
id: userId,
},
},
},
});
.schema(organizationSchema)
.metadata({
name: "create-organization",
track: {
event: "create-organization",
channel: "server",
},
})
.action(async ({ parsedInput, ctx }) => {
const { name, website, frameworks } = parsedInput;
const { id: userId, organizationId } = ctx.user;

if (!organization) {
throw new Error("Organization not found");
}
if (!name || !website) {
console.log("Invalid input detected:", { name, website });
throw new Error("Invalid user input");
}

try {
await db.$transaction(async () => {
await db.organization.upsert({
where: {
id: organization.id,
},
update: {
name,
website,
subdomain: hasVercelConfig ? subdomain || "" : "",
},
create: {
name,
website,
subdomain: hasVercelConfig ? subdomain || "" : "",
},
});
if (!organizationId) {
await createOrganizationAndConnectUser({
userId,
normalizedEmail: ctx.user.email!,
});
}

await db.user.update({
where: {
id: userId,
},
data: {
onboarded: true,
},
});
});
const organization = await db.organization.findFirst({
where: {
users: {
some: {
id: userId,
},
},
},
});

// await tasks.trigger<typeof createDefaultPoliciesTask>(
// "create-default-policies",
// {
// ownerId: userId,
// organizationId: organization.id,
// organizationName: name,
// }
// );
if (!organization) {
throw new Error("Organization not found");
}

revalidateTag(`user_${userId}`);
revalidateTag(`organization_${organizationId}`);
try {
const handle = await tasks.trigger<typeof createOrganizationTask>(
"create-organization",
{
userId,
fullName: name,
website,
frameworkIds: frameworks,
organizationId: organization.id,
},
);

return {
success: true,
};
} catch (error) {
if (hasVercelConfig && subdomain) {
try {
await removeDomainFromVercelProject(
`${subdomain}.${process.env.NEXT_PUBLIC_VERCEL_URL}`,
);
} catch (cleanupError) {
console.error(
"Failed to clean up subdomain after error:",
cleanupError,
);
}
}
return {
success: true,
runId: handle.id,
publicAccessToken: handle.publicAccessToken,
};
} catch (error) {
console.error("Error during organization update:", error);

console.error("Error during organization update:", error);
throw new Error("Failed to update organization");
}
});
throw new Error("Failed to update organization");
}
});
10 changes: 7 additions & 3 deletions apps/app/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import {
import { z } from "zod";

export const organizationSchema = z.object({
fullName: z.string().min(1, "Full name is required"),
name: z.string().min(1, "Name is required"),
website: z.string().url("Must be a valid URL"),
subdomain: z.string().min(1, "Subdomain is required").optional(),
fullName: z.string().min(1, "Full name is required"),
website: z.string().url("Must be a valid URL").optional().or(z.literal("")),
frameworks: z
.array(z.string())
.min(1, "Please select at least one framework to get started with"),
});

export type OrganizationSchema = z.infer<typeof organizationSchema>;

export const organizationNameSchema = z.object({
name: z
.string()
Expand Down
23 changes: 22 additions & 1 deletion apps/app/src/app/[locale]/(app)/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { auth } from "@/auth";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { db } from "@bubba/db";
import dynamic from "next/dynamic";
import { redirect } from "next/navigation";
import { cache } from "react";

const HotKeys = dynamic(
() => import("@/components/hot-keys").then((mod) => mod.HotKeys),
Expand All @@ -18,10 +20,16 @@ export default async function Layout({
}) {
const session = await auth();

if (!session?.user) {
if (!session?.user || !session.user.organizationId) {
redirect("/auth");
}

const isSetup = await isOrganizationSetup(session.user.organizationId);

if (!isSetup) {
redirect("/setup");
}

return (
<div className="relative">
<Sidebar />
Expand All @@ -35,3 +43,16 @@ export default async function Layout({
</div>
);
}

const isOrganizationSetup = cache(async (organizationId: string) => {
const organization = await db.organization.findUnique({
where: {
id: organizationId,
},
select: {
setup: true,
},
});

return organization?.setup;
});
37 changes: 20 additions & 17 deletions apps/app/src/app/[locale]/(app)/(dashboard)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { db } from "@bubba/db";
import type { Metadata } from "next";
import { setStaticParamsLocale } from "next-international/server";
import { redirect } from "next/navigation";
import { cache } from "react";

export default async function OrganizationSettings({
params,
Expand All @@ -18,28 +19,17 @@ export default async function OrganizationSettings({

const session = await auth();

const [organization] = await Promise.all([
db.organization.findUnique({
where: {
id: session?.user.organizationId,
},
select: {
name: true,
website: true,
id: true,
},
}),
]);

if (!organization) {
if (!session?.user.organizationId) {
return redirect("/");
}

const organization = await organizationDetails(session.user.organizationId);

return (
<div className="space-y-12">
<UpdateOrganizationName organizationName={organization.name} />
<UpdateOrganizationWebsite organizationWebsite={organization.website} />
<DeleteOrganization organizationId={organization.id} />
<UpdateOrganizationName organizationName={organization?.name ?? ""} />
<UpdateOrganizationWebsite organizationWebsite={organization?.website ?? ""} />
<DeleteOrganization organizationId={organization?.id ?? ""} />
</div>
);
}
Expand All @@ -57,3 +47,16 @@ export async function generateMetadata({
title: t("sidebar.settings"),
};
}

const organizationDetails = cache(async (organizationId: string) => {
const organization = await db.organization.findUnique({
where: { id: organizationId },
select: {
name: true,
website: true,
id: true,
},
});

return organization;
});
Loading