Skip to content

Commit 909ddb0

Browse files
committed
feat: removed packages for now + new auto org creation
1 parent 5148b1d commit 909ddb0

37 files changed

Lines changed: 372 additions & 699 deletions

apps/api/src/db/queries/organization.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
type OrganizationSelect,
77
type OrganizationInsert,
88
type Database,
9+
auth,
910
} from "@repo/database";
11+
import { generateSlugFromEmailDomain } from "@api/utils/organisation";
1012

1113
// Create organization
1214
export async function createOrganization(
@@ -105,7 +107,46 @@ export async function getOrganizationsForUser(
105107
.from(organization)
106108
.innerJoin(member, eq(member.organizationId, organization.id))
107109
.where(eq(member.userId, params.userId))
108-
.orderBy(desc(member.createdAt));
110+
.orderBy(desc(member.createdAt))
111+
.$withCache();
112+
113+
return organizations;
114+
}
115+
116+
export async function getOrganizationsForUserOrCreateDefault(
117+
db: Database,
118+
params: {
119+
userId: string;
120+
userEmail: string;
121+
userName: string;
122+
}
123+
) {
124+
const organizations = await getOrganizationsForUser(db, {
125+
userId: params.userId,
126+
});
127+
128+
// If the user has no organizations, create a default one
129+
if (organizations.length === 0) {
130+
const { slug, organizationName } = await generateSlugFromEmailDomain(db, {
131+
email: params.userEmail,
132+
});
133+
134+
const newOrganization = await auth.api.createOrganization({
135+
body: {
136+
name: organizationName,
137+
slug,
138+
userId: params.userId,
139+
},
140+
});
141+
142+
return [
143+
{
144+
organization: newOrganization,
145+
role: "owner",
146+
joinedAt: new Date(),
147+
},
148+
];
149+
}
109150

110151
return organizations;
111152
}

apps/api/src/utils/organisation.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { Database, organization } from "@repo/database";
2+
import { eq } from "drizzle-orm";
3+
import { customAlphabet } from "nanoid";
4+
5+
// list of forbidden words for organization names
6+
const FORBIDDEN_SLUGS = [
7+
"origami",
8+
"admin",
9+
"api",
10+
"origami-chat",
11+
"org",
12+
"select",
13+
"website",
14+
"app",
15+
"web",
16+
"www",
17+
"blog",
18+
"docs",
19+
"help",
20+
"support",
21+
"contact",
22+
"about",
23+
"terms",
24+
"privacy",
25+
"security",
26+
"login",
27+
"register",
28+
"dashboard",
29+
"settings",
30+
"profile",
31+
"account",
32+
"billing",
33+
"feature",
34+
"features",
35+
"page",
36+
"pages",
37+
];
38+
39+
// Most popular email domains that B2B customers might use
40+
// These are considered "popular" domains where we should generate random slugs
41+
const POPULAR_EMAIL_DOMAINS = [
42+
// Consumer email providers
43+
"gmail.com",
44+
"yahoo.com",
45+
"hotmail.com",
46+
"outlook.com",
47+
"icloud.com",
48+
"me.com",
49+
"mac.com",
50+
"protonmail.com",
51+
"proton.me",
52+
"tutanota.com",
53+
"zoho.com",
54+
"yandex.com",
55+
"yandex.ru",
56+
"aol.com",
57+
"gmx.com",
58+
"gmx.net",
59+
"gmx.de",
60+
"t-online.de",
61+
"web.de",
62+
"mail.com",
63+
"mail.ru",
64+
"qq.com",
65+
"163.com",
66+
"126.com",
67+
"sina.com",
68+
"naver.com",
69+
"daum.net",
70+
"rediffmail.com",
71+
72+
// Popular business email providers (where companies often use generic domains)
73+
"google.com", // Google Workspace but on google.com domain
74+
"microsoft.com",
75+
"office365.com",
76+
"live.com",
77+
"msn.com",
78+
"windowslive.com",
79+
80+
// Other popular free providers
81+
"fastmail.com",
82+
"hey.com",
83+
"superhuman.com",
84+
];
85+
86+
// URL-friendly alphabet for slug generation (lowercase letters and numbers)
87+
const SLUG_ALPHABET = "ORIGAMIorigami0123456789";
88+
const SLUG_LENGTH = 8;
89+
90+
// Create nanoId generator for slugs
91+
const generateNanoSlugId = customAlphabet(SLUG_ALPHABET, SLUG_LENGTH);
92+
93+
export function isForbiddenSlug(slug: string): boolean {
94+
return FORBIDDEN_SLUGS.includes(slug.toLowerCase());
95+
}
96+
97+
export function isPopularEmailDomain(email: string): boolean {
98+
const domain = email.split("@")[1]?.toLowerCase();
99+
return domain ? POPULAR_EMAIL_DOMAINS.includes(domain) : false;
100+
}
101+
102+
export function generateSlugFromName(name: string): string {
103+
// remove all non-alphanumeric characters and convert to lowercase
104+
let slug = name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
105+
106+
// if empty after cleaning, generate random slug
107+
if (!slug) {
108+
return generateNanoSlugId();
109+
}
110+
111+
// if the slug is in the forbidden words list, add a random number to the end
112+
if (isForbiddenSlug(slug)) {
113+
slug = `${slug}-${Math.floor(Math.random() * 1000000)}`;
114+
}
115+
116+
return slug;
117+
}
118+
119+
export async function generateSlugFromEmailDomain(
120+
db: Database,
121+
params: {
122+
email: string;
123+
}
124+
): Promise<{ slug: string; organizationName: string }> {
125+
const domain = params.email.split("@")[1]?.toLowerCase();
126+
127+
if (!domain) {
128+
const slug = generateNanoSlugId();
129+
130+
return { slug, organizationName: slug.replace(/-/g, " ") };
131+
}
132+
133+
// If this is a popular email domain, generate a random slug instead
134+
if (isPopularEmailDomain(params.email)) {
135+
const slug = generateNanoSlugId();
136+
137+
return { slug, organizationName: slug.replace(/-/g, " ") };
138+
}
139+
140+
// Remove everything after the first dot and clean the domain
141+
let slug = domain
142+
.split(".")[0]
143+
.replace(/[^a-zA-Z0-9]/g, "")
144+
.toLowerCase();
145+
146+
// if empty after cleaning, generate random slug
147+
if (!slug) {
148+
const slug = generateNanoSlugId();
149+
150+
return { slug, organizationName: slug.replace(/-/g, " ") };
151+
}
152+
153+
// if the slug is in the forbidden words list, add a random number to the end
154+
if (isForbiddenSlug(slug)) {
155+
slug = `${slug}-${generateNanoSlugId()}`;
156+
}
157+
158+
// check if the slug is already taken
159+
const existingOrganization = await db.query.organization.findFirst({
160+
where: eq(organization.slug, slug),
161+
});
162+
163+
if (existingOrganization) {
164+
return {
165+
slug: generateNanoSlugId(),
166+
organizationName: existingOrganization.name,
167+
};
168+
}
169+
170+
return { slug, organizationName: slug.replace(/-/g, " ") };
171+
}

apps/origami-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"better-auth": "^1.2.9",
2525
"class-variance-authority": "^0.7.1",
2626
"clsx": "^2.1.1",
27+
"drizzle-orm": "0.44.0",
2728
"fumadocs-core": "^15.5.1",
2829
"fumadocs-mdx": "^11.6.7",
2930
"fumadocs-ui": "^15.5.1",

apps/origami-web/src/app/(withAuth)/auth/TestTRPC.tsx renamed to apps/origami-web/src/app/(withAuth)/[organisationSlug]/TestTRPC.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export default function TestTRPC() {
88

99
const { data, isLoading } = useQuery(trpc.user.me.queryOptions());
1010

11-
return <div>{data?.email}</div>;
11+
return <div>{data?.email} houla</div>;
1212
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ensurePageAuth } from "@/lib/auth/server";
2+
import TestTRPC from "./TestTRPC";
3+
import { db } from "@database/database";
4+
import { getOrganizationsForUserOrCreateDefault } from "@api/db/queries/organization";
5+
6+
export default async function Auth() {
7+
const { user } = await ensurePageAuth();
8+
9+
// If the user lands on this page and is not a member of any organization, we create a default one for them
10+
const orgs = await getOrganizationsForUserOrCreateDefault(db, {
11+
userId: user?.id,
12+
userEmail: user?.email,
13+
userName: user?.name,
14+
});
15+
16+
// Redirect to the active organization
17+
18+
return (
19+
<div className="flex flex-col items-center justify-center h-screen">
20+
<h1 className="text-4xl font-bold">Origami</h1>
21+
<p>You're signed-in as {user?.email}</p>
22+
23+
<TestTRPC />
24+
</div>
25+
);
26+
}

apps/origami-web/src/app/(withAuth)/auth/page.tsx

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ensurePageAuth } from "@/lib/auth/server";
2+
import { db } from "@database/database";
3+
import { getOrganizationsForUserOrCreateDefault } from "@api/db/queries/organization";
4+
import { notFound, redirect } from "next/navigation";
5+
6+
export default async function Auth() {
7+
const { user } = await ensurePageAuth();
8+
9+
// If the user lands on this page and is not a member of any organization, we create a default one for them
10+
const orgs = await getOrganizationsForUserOrCreateDefault(db, {
11+
userId: user?.id,
12+
userEmail: user?.email,
13+
userName: user?.name,
14+
});
15+
16+
const orgToRedirectTo = orgs?.[0]?.organization;
17+
18+
// This should never happen, but just in case
19+
if (!orgToRedirectTo) {
20+
console.error(`ERROR: User ${user?.id} has no organizations found`);
21+
22+
notFound();
23+
}
24+
25+
// Redirect to the active organization
26+
redirect(`/${orgToRedirectTo.slug}`);
27+
}

0 commit comments

Comments
 (0)