create-cronus-stack 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scaffold.js +425 -23
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/scaffold.js
CHANGED
|
@@ -148,7 +148,7 @@ function packageJson(projectName, config) {
|
|
|
148
148
|
add(devDeps, "drizzle-kit", "^0.31.10");
|
|
149
149
|
switch (single(config, "database")) {
|
|
150
150
|
case "db-sqlite":
|
|
151
|
-
add(deps, "better-sqlite3", "^
|
|
151
|
+
add(deps, "better-sqlite3", "^12.0.0");
|
|
152
152
|
add(devDeps, "@types/better-sqlite3", "^9.6.0");
|
|
153
153
|
break;
|
|
154
154
|
case "db-postgres":
|
|
@@ -311,19 +311,95 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|
|
311
311
|
}
|
|
312
312
|
`;
|
|
313
313
|
}
|
|
314
|
-
function
|
|
314
|
+
function fromAppImport(config, target) {
|
|
315
|
+
return usesImportAlias(config) ? `@/${target}` : `../${target}`;
|
|
316
|
+
}
|
|
317
|
+
function pageStatusCopy(config) {
|
|
318
|
+
if (emitsBetterAuth(config) && emitsDrizzle(config)) {
|
|
319
|
+
return ` {session ? (
|
|
320
|
+
<p className="max-w-prose text-fg-secondary">
|
|
321
|
+
Signed in as {session.user?.email}. {itemCount} items in the active workspace.
|
|
322
|
+
</p>
|
|
323
|
+
) : (
|
|
324
|
+
<p className="max-w-prose text-fg-secondary">
|
|
325
|
+
Sign in required on this stack. Middleware redirects unsigned-in visitors to /login.
|
|
326
|
+
</p>
|
|
327
|
+
)}`;
|
|
328
|
+
}
|
|
329
|
+
if (emitsDrizzle(config)) {
|
|
330
|
+
return ` <p className="max-w-prose text-fg-secondary">
|
|
331
|
+
{itemCount} items in the database. Read KICKOFF.md before changing frameworks,
|
|
332
|
+
databases, auth, payments, or design-system rules.
|
|
333
|
+
</p>`;
|
|
334
|
+
}
|
|
335
|
+
return ` <p className="max-w-prose text-fg-secondary">
|
|
336
|
+
This app was scaffolded from the Cronus Stack Builder. Read KICKOFF.md before changing
|
|
337
|
+
frameworks, databases, auth, payments, or design-system rules.
|
|
338
|
+
</p>`;
|
|
339
|
+
}
|
|
340
|
+
function pageTsx(config) {
|
|
341
|
+
const withDrizzle = emitsDrizzle(config);
|
|
342
|
+
const withAuth = emitsBetterAuth(config);
|
|
343
|
+
const extraImports = [
|
|
344
|
+
withDrizzle && withAuth
|
|
345
|
+
? 'import { count, eq } from "drizzle-orm";'
|
|
346
|
+
: withDrizzle
|
|
347
|
+
? 'import { count } from "drizzle-orm";'
|
|
348
|
+
: undefined,
|
|
349
|
+
withAuth ? 'import { headers } from "next/headers";' : undefined,
|
|
350
|
+
withDrizzle ? `import { db } from "${fromAppImport(config, "db")}";` : undefined,
|
|
351
|
+
withDrizzle && withAuth
|
|
352
|
+
? `import { items, member } from "${fromAppImport(config, "db/schema")}";`
|
|
353
|
+
: withDrizzle
|
|
354
|
+
? `import { items } from "${fromAppImport(config, "db/schema")}";`
|
|
355
|
+
: undefined,
|
|
356
|
+
withAuth ? `import { auth } from "${fromAppImport(config, "lib/auth")}";` : undefined,
|
|
357
|
+
]
|
|
358
|
+
.filter((line) => line !== undefined)
|
|
359
|
+
.join("\n");
|
|
360
|
+
const asyncKw = withDrizzle || withAuth ? "async " : "";
|
|
361
|
+
let loader = "";
|
|
362
|
+
if (withDrizzle && withAuth) {
|
|
363
|
+
loader = ` const session = await auth.api.getSession({ headers: await headers() });
|
|
364
|
+
let orgId = session?.session?.activeOrganizationId ?? null;
|
|
365
|
+
if (!orgId && session?.user?.id) {
|
|
366
|
+
const [row] = await db
|
|
367
|
+
.select({ organizationId: member.organizationId })
|
|
368
|
+
.from(member)
|
|
369
|
+
.where(eq(member.userId, session.user.id))
|
|
370
|
+
.limit(1);
|
|
371
|
+
orgId = row?.organizationId ?? null;
|
|
372
|
+
}
|
|
373
|
+
const [itemRow] = orgId
|
|
374
|
+
? await db.select({ value: count() }).from(items).where(eq(items.workspaceId, orgId))
|
|
375
|
+
: [{ value: 0 }];
|
|
376
|
+
const itemCount = new Intl.NumberFormat("en-US").format(Number(itemRow?.value ?? 0));
|
|
377
|
+
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
else if (withDrizzle) {
|
|
381
|
+
loader = ` const [itemRow] = await db.select({ value: count() }).from(items);
|
|
382
|
+
const itemCount = new Intl.NumberFormat("en-US").format(Number(itemRow?.value ?? 0));
|
|
383
|
+
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
else if (withAuth) {
|
|
387
|
+
loader = ` const session = await auth.api.getSession({ headers: await headers() });
|
|
388
|
+
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
315
391
|
return `import { Badge } from "@cronus-ui/ui/badge";
|
|
316
392
|
import { Button } from "@cronus-ui/ui/button";
|
|
317
393
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@cronus-ui/ui/card";
|
|
318
|
-
|
|
394
|
+
${extraImports ? `${extraImports}\n` : ""}
|
|
319
395
|
const metrics = [
|
|
320
396
|
{ label: "Revenue", value: "R$ 48.2k" },
|
|
321
397
|
{ label: "Active users", value: "2,318" },
|
|
322
398
|
{ label: "NPS", value: "72" },
|
|
323
399
|
];
|
|
324
400
|
|
|
325
|
-
export default function Page() {
|
|
326
|
-
return (
|
|
401
|
+
export default ${asyncKw}function Page() {
|
|
402
|
+
${loader} return (
|
|
327
403
|
<main className="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-8 px-6 py-12">
|
|
328
404
|
<header className="flex flex-col gap-4">
|
|
329
405
|
<Badge variant="primary" className="w-fit">
|
|
@@ -331,10 +407,7 @@ export default function Page() {
|
|
|
331
407
|
</Badge>
|
|
332
408
|
<div className="flex flex-col gap-3">
|
|
333
409
|
<h1 className="text-4xl font-semibold tracking-tight text-fg">Your stack is ready</h1>
|
|
334
|
-
|
|
335
|
-
This app was scaffolded from the Cronus Stack Builder. Read KICKOFF.md before changing
|
|
336
|
-
frameworks, databases, auth, payments, or design-system rules.
|
|
337
|
-
</p>
|
|
410
|
+
${pageStatusCopy(config)}
|
|
338
411
|
</div>
|
|
339
412
|
<div className="flex flex-wrap gap-3">
|
|
340
413
|
<Button variant="primary">Start building</Button>
|
|
@@ -505,6 +578,23 @@ function kitSkillsFromConfig(config) {
|
|
|
505
578
|
function drizzleConfig(config) {
|
|
506
579
|
const dialect = sqlDialect(config) ?? "sqlite";
|
|
507
580
|
const fallback = defaultDatabaseUrl(config) ?? "file:./data/app.db";
|
|
581
|
+
if (dialect === "sqlite") {
|
|
582
|
+
return `import { mkdirSync } from "node:fs";
|
|
583
|
+
import { dirname } from "node:path";
|
|
584
|
+
import { defineConfig } from "drizzle-kit";
|
|
585
|
+
|
|
586
|
+
const url = process.env.DATABASE_URL ?? "${fallback}";
|
|
587
|
+
const fileFromUrl = url.startsWith("file:") ? url.slice("file:".length) : url;
|
|
588
|
+
mkdirSync(dirname(fileFromUrl) || ".", { recursive: true });
|
|
589
|
+
|
|
590
|
+
export default defineConfig({
|
|
591
|
+
dialect: "sqlite",
|
|
592
|
+
schema: "./${dbDir(config)}/schema.ts",
|
|
593
|
+
out: "./drizzle",
|
|
594
|
+
dbCredentials: { url },
|
|
595
|
+
});
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
508
598
|
return `import { defineConfig } from "drizzle-kit";
|
|
509
599
|
|
|
510
600
|
export default defineConfig({
|
|
@@ -535,6 +625,7 @@ export const session = sqliteTable("session", {
|
|
|
535
625
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
536
626
|
ipAddress: text("ip_address"),
|
|
537
627
|
userAgent: text("user_agent"),
|
|
628
|
+
activeOrganizationId: text("active_organization_id"),
|
|
538
629
|
userId: text("user_id")
|
|
539
630
|
.notNull()
|
|
540
631
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
@@ -567,6 +658,42 @@ export const verification = sqliteTable("verification", {
|
|
|
567
658
|
createdAt: integer("created_at", { mode: "timestamp" }),
|
|
568
659
|
updatedAt: integer("updated_at", { mode: "timestamp" }),
|
|
569
660
|
});
|
|
661
|
+
|
|
662
|
+
export const organization = sqliteTable("organization", {
|
|
663
|
+
id: text("id").primaryKey(),
|
|
664
|
+
name: text("name").notNull(),
|
|
665
|
+
slug: text("slug").notNull().unique(),
|
|
666
|
+
logo: text("logo"),
|
|
667
|
+
metadata: text("metadata"),
|
|
668
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
export const member = sqliteTable("member", {
|
|
672
|
+
id: text("id").primaryKey(),
|
|
673
|
+
organizationId: text("organization_id")
|
|
674
|
+
.notNull()
|
|
675
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
676
|
+
userId: text("user_id")
|
|
677
|
+
.notNull()
|
|
678
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
679
|
+
role: text("role").notNull(),
|
|
680
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
export const invitation = sqliteTable("invitation", {
|
|
684
|
+
id: text("id").primaryKey(),
|
|
685
|
+
organizationId: text("organization_id")
|
|
686
|
+
.notNull()
|
|
687
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
688
|
+
email: text("email").notNull(),
|
|
689
|
+
role: text("role"),
|
|
690
|
+
status: text("status").notNull(),
|
|
691
|
+
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
692
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
693
|
+
inviterId: text("inviter_id")
|
|
694
|
+
.notNull()
|
|
695
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
696
|
+
});
|
|
570
697
|
`;
|
|
571
698
|
}
|
|
572
699
|
function pgAuthTables() {
|
|
@@ -589,6 +716,7 @@ export const session = pgTable("session", {
|
|
|
589
716
|
updatedAt: timestamp("updated_at").notNull(),
|
|
590
717
|
ipAddress: text("ip_address"),
|
|
591
718
|
userAgent: text("user_agent"),
|
|
719
|
+
activeOrganizationId: text("active_organization_id"),
|
|
592
720
|
userId: text("user_id")
|
|
593
721
|
.notNull()
|
|
594
722
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
@@ -621,6 +749,42 @@ export const verification = pgTable("verification", {
|
|
|
621
749
|
createdAt: timestamp("created_at"),
|
|
622
750
|
updatedAt: timestamp("updated_at"),
|
|
623
751
|
});
|
|
752
|
+
|
|
753
|
+
export const organization = pgTable("organization", {
|
|
754
|
+
id: text("id").primaryKey(),
|
|
755
|
+
name: text("name").notNull(),
|
|
756
|
+
slug: text("slug").notNull().unique(),
|
|
757
|
+
logo: text("logo"),
|
|
758
|
+
metadata: text("metadata"),
|
|
759
|
+
createdAt: timestamp("created_at").notNull(),
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
export const member = pgTable("member", {
|
|
763
|
+
id: text("id").primaryKey(),
|
|
764
|
+
organizationId: text("organization_id")
|
|
765
|
+
.notNull()
|
|
766
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
767
|
+
userId: text("user_id")
|
|
768
|
+
.notNull()
|
|
769
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
770
|
+
role: text("role").notNull(),
|
|
771
|
+
createdAt: timestamp("created_at").notNull(),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
export const invitation = pgTable("invitation", {
|
|
775
|
+
id: text("id").primaryKey(),
|
|
776
|
+
organizationId: text("organization_id")
|
|
777
|
+
.notNull()
|
|
778
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
779
|
+
email: text("email").notNull(),
|
|
780
|
+
role: text("role"),
|
|
781
|
+
status: text("status").notNull(),
|
|
782
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
783
|
+
createdAt: timestamp("created_at").notNull(),
|
|
784
|
+
inviterId: text("inviter_id")
|
|
785
|
+
.notNull()
|
|
786
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
787
|
+
});
|
|
624
788
|
`;
|
|
625
789
|
}
|
|
626
790
|
function mysqlAuthTables() {
|
|
@@ -643,6 +807,7 @@ export const session = mysqlTable("session", {
|
|
|
643
807
|
updatedAt: timestamp("updated_at").notNull(),
|
|
644
808
|
ipAddress: text("ip_address"),
|
|
645
809
|
userAgent: text("user_agent"),
|
|
810
|
+
activeOrganizationId: varchar("active_organization_id", { length: 36 }),
|
|
646
811
|
userId: varchar("user_id", { length: 36 })
|
|
647
812
|
.notNull()
|
|
648
813
|
.references(() => user.id, { onDelete: "cascade" }),
|
|
@@ -675,6 +840,42 @@ export const verification = mysqlTable("verification", {
|
|
|
675
840
|
createdAt: timestamp("created_at"),
|
|
676
841
|
updatedAt: timestamp("updated_at"),
|
|
677
842
|
});
|
|
843
|
+
|
|
844
|
+
export const organization = mysqlTable("organization", {
|
|
845
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
846
|
+
name: varchar("name", { length: 255 }).notNull(),
|
|
847
|
+
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
|
848
|
+
logo: text("logo"),
|
|
849
|
+
metadata: text("metadata"),
|
|
850
|
+
createdAt: timestamp("created_at").notNull(),
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
export const member = mysqlTable("member", {
|
|
854
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
855
|
+
organizationId: varchar("organization_id", { length: 36 })
|
|
856
|
+
.notNull()
|
|
857
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
858
|
+
userId: varchar("user_id", { length: 36 })
|
|
859
|
+
.notNull()
|
|
860
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
861
|
+
role: varchar("role", { length: 255 }).notNull(),
|
|
862
|
+
createdAt: timestamp("created_at").notNull(),
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
export const invitation = mysqlTable("invitation", {
|
|
866
|
+
id: varchar("id", { length: 36 }).primaryKey(),
|
|
867
|
+
organizationId: varchar("organization_id", { length: 36 })
|
|
868
|
+
.notNull()
|
|
869
|
+
.references(() => organization.id, { onDelete: "cascade" }),
|
|
870
|
+
email: varchar("email", { length: 255 }).notNull(),
|
|
871
|
+
role: varchar("role", { length: 255 }),
|
|
872
|
+
status: varchar("status", { length: 255 }).notNull(),
|
|
873
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
874
|
+
createdAt: timestamp("created_at").notNull(),
|
|
875
|
+
inviterId: varchar("inviter_id", { length: 36 })
|
|
876
|
+
.notNull()
|
|
877
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
878
|
+
});
|
|
678
879
|
`;
|
|
679
880
|
}
|
|
680
881
|
function drizzleSchema(config) {
|
|
@@ -684,34 +885,44 @@ function drizzleSchema(config) {
|
|
|
684
885
|
const imports = withAuth
|
|
685
886
|
? 'import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";'
|
|
686
887
|
: 'import { integer, pgTable, text } from "drizzle-orm/pg-core";';
|
|
888
|
+
const workspaceCol = withAuth
|
|
889
|
+
? `\n workspaceId: text("workspace_id").references(() => organization.id, { onDelete: "cascade" }),`
|
|
890
|
+
: "";
|
|
687
891
|
return `${imports}
|
|
688
|
-
|
|
892
|
+
${withAuth ? pgAuthTables() : ""}
|
|
689
893
|
export const items = pgTable("items", {
|
|
690
894
|
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
691
|
-
title: text("title").notNull()
|
|
895
|
+
title: text("title").notNull(),${workspaceCol}
|
|
692
896
|
});
|
|
693
|
-
|
|
897
|
+
`;
|
|
694
898
|
}
|
|
695
899
|
case "mysql": {
|
|
696
900
|
const imports = withAuth
|
|
697
901
|
? 'import { boolean, int, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";'
|
|
698
902
|
: 'import { int, mysqlTable, varchar } from "drizzle-orm/mysql-core";';
|
|
903
|
+
const workspaceCol = withAuth
|
|
904
|
+
? `\n workspaceId: varchar("workspace_id", { length: 36 }).references(() => organization.id, { onDelete: "cascade" }),`
|
|
905
|
+
: "";
|
|
699
906
|
return `${imports}
|
|
700
|
-
|
|
907
|
+
${withAuth ? mysqlAuthTables() : ""}
|
|
701
908
|
export const items = mysqlTable("items", {
|
|
702
909
|
id: int("id").autoincrement().primaryKey(),
|
|
703
|
-
title: varchar("title", { length: 255 }).notNull()
|
|
910
|
+
title: varchar("title", { length: 255 }).notNull(),${workspaceCol}
|
|
704
911
|
});
|
|
705
|
-
|
|
912
|
+
`;
|
|
706
913
|
}
|
|
707
|
-
default:
|
|
914
|
+
default: {
|
|
915
|
+
const workspaceCol = withAuth
|
|
916
|
+
? `\n workspaceId: text("workspace_id").references(() => organization.id, { onDelete: "cascade" }),`
|
|
917
|
+
: "";
|
|
708
918
|
return `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
709
|
-
|
|
919
|
+
${withAuth ? sqliteAuthTables() : ""}
|
|
710
920
|
export const items = sqliteTable("items", {
|
|
711
921
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
712
|
-
title: text("title").notNull()
|
|
922
|
+
title: text("title").notNull(),${workspaceCol}
|
|
713
923
|
});
|
|
714
|
-
|
|
924
|
+
`;
|
|
925
|
+
}
|
|
715
926
|
}
|
|
716
927
|
}
|
|
717
928
|
function drizzleClient(config) {
|
|
@@ -759,21 +970,210 @@ function betterAuthServer(config) {
|
|
|
759
970
|
const schemaImport = dbModuleImport(config, "schema");
|
|
760
971
|
return `import { betterAuth } from "better-auth";
|
|
761
972
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
973
|
+
import { nextCookies } from "better-auth/next-js";
|
|
974
|
+
import { organization } from "better-auth/plugins";
|
|
975
|
+
import { and, eq } from "drizzle-orm";
|
|
762
976
|
import { db } from "${dbImport}";
|
|
763
977
|
import * as schema from "${schemaImport}";
|
|
978
|
+
import {
|
|
979
|
+
invitation as invitationTable,
|
|
980
|
+
member,
|
|
981
|
+
organization as organizationTable,
|
|
982
|
+
user as userTable,
|
|
983
|
+
} from "${schemaImport}";
|
|
984
|
+
|
|
985
|
+
function newId(): string {
|
|
986
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
987
|
+
}
|
|
764
988
|
|
|
765
989
|
export const auth = betterAuth({
|
|
766
990
|
database: drizzleAdapter(db, { provider: "${provider}", schema }),
|
|
767
|
-
emailAndPassword: {
|
|
991
|
+
emailAndPassword: {
|
|
992
|
+
enabled: true,
|
|
993
|
+
sendResetPassword: async ({ url }) => {
|
|
994
|
+
console.info("Password reset URL:", url);
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
databaseHooks: {
|
|
998
|
+
session: {
|
|
999
|
+
create: {
|
|
1000
|
+
before: async (session) => {
|
|
1001
|
+
const [existing] = await db
|
|
1002
|
+
.select({ organizationId: member.organizationId })
|
|
1003
|
+
.from(member)
|
|
1004
|
+
.where(eq(member.userId, session.userId))
|
|
1005
|
+
.limit(1);
|
|
1006
|
+
if (existing?.organizationId) {
|
|
1007
|
+
return { data: { ...session, activeOrganizationId: existing.organizationId } };
|
|
1008
|
+
}
|
|
1009
|
+
const [owner] = await db
|
|
1010
|
+
.select({ name: userTable.name, email: userTable.email })
|
|
1011
|
+
.from(userTable)
|
|
1012
|
+
.where(eq(userTable.id, session.userId))
|
|
1013
|
+
.limit(1);
|
|
1014
|
+
if (owner?.email) {
|
|
1015
|
+
const [pending] = await db
|
|
1016
|
+
.select({ id: invitationTable.id })
|
|
1017
|
+
.from(invitationTable)
|
|
1018
|
+
.where(
|
|
1019
|
+
and(eq(invitationTable.email, owner.email), eq(invitationTable.status, "pending")),
|
|
1020
|
+
)
|
|
1021
|
+
.limit(1);
|
|
1022
|
+
if (pending) return;
|
|
1023
|
+
}
|
|
1024
|
+
const orgId = newId();
|
|
1025
|
+
const now = new Date();
|
|
1026
|
+
await db.insert(organizationTable).values({
|
|
1027
|
+
id: orgId,
|
|
1028
|
+
name: owner?.name.trim() || "Workspace",
|
|
1029
|
+
slug: \`ws-\${session.userId.slice(0, 16)}\`,
|
|
1030
|
+
createdAt: now,
|
|
1031
|
+
});
|
|
1032
|
+
await db.insert(member).values({
|
|
1033
|
+
id: newId(),
|
|
1034
|
+
organizationId: orgId,
|
|
1035
|
+
userId: session.userId,
|
|
1036
|
+
role: "owner",
|
|
1037
|
+
createdAt: now,
|
|
1038
|
+
});
|
|
1039
|
+
return { data: { ...session, activeOrganizationId: orgId } };
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
plugins: [
|
|
1045
|
+
organization({
|
|
1046
|
+
sendInvitationEmail: async (data) => {
|
|
1047
|
+
const base = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
|
|
1048
|
+
console.info(\`Invite \${data.email}: \${base}/accept-invitation?id=\${data.id}\`);
|
|
1049
|
+
},
|
|
1050
|
+
}),
|
|
1051
|
+
nextCookies(),
|
|
1052
|
+
],
|
|
768
1053
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
769
1054
|
baseURL: process.env.BETTER_AUTH_URL,
|
|
770
1055
|
});
|
|
771
1056
|
`;
|
|
772
1057
|
}
|
|
1058
|
+
function betterAuthMiddleware() {
|
|
1059
|
+
return `import { NextRequest, NextResponse } from "next/server";
|
|
1060
|
+
import { getSessionCookie } from "better-auth/cookies";
|
|
1061
|
+
|
|
1062
|
+
const AUTH_PAGES = ["/login", "/signup", "/forgot-password"];
|
|
1063
|
+
|
|
1064
|
+
function invitationOf(request: NextRequest): string | null {
|
|
1065
|
+
const { pathname, searchParams } = request.nextUrl;
|
|
1066
|
+
return (
|
|
1067
|
+
searchParams.get("invitation") ??
|
|
1068
|
+
(pathname === "/accept-invitation" ? searchParams.get("id") : null)
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export function middleware(request: NextRequest) {
|
|
1073
|
+
const sessionCookie = getSessionCookie(request);
|
|
1074
|
+
const path = request.nextUrl.pathname;
|
|
1075
|
+
const isAuthPage = AUTH_PAGES.includes(path);
|
|
1076
|
+
const invitation = invitationOf(request);
|
|
1077
|
+
if (!sessionCookie && path === "/accept-invitation") {
|
|
1078
|
+
const url = new URL("/signup", request.url);
|
|
1079
|
+
if (invitation) url.searchParams.set("invitation", invitation);
|
|
1080
|
+
return NextResponse.redirect(url);
|
|
1081
|
+
}
|
|
1082
|
+
if (!sessionCookie && !isAuthPage) {
|
|
1083
|
+
const url = new URL("/login", request.url);
|
|
1084
|
+
if (invitation) url.searchParams.set("invitation", invitation);
|
|
1085
|
+
return NextResponse.redirect(url);
|
|
1086
|
+
}
|
|
1087
|
+
if (sessionCookie && isAuthPage) {
|
|
1088
|
+
if (invitation) {
|
|
1089
|
+
return NextResponse.redirect(
|
|
1090
|
+
new URL(\`/accept-invitation?id=\${encodeURIComponent(invitation)}\`, request.url),
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
1094
|
+
}
|
|
1095
|
+
return NextResponse.next();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export const config = {
|
|
1099
|
+
matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico|.*\\\\..*).*)"],
|
|
1100
|
+
};
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1103
|
+
function acceptInvitationPage(config) {
|
|
1104
|
+
const authClientImport = fromAppImport(config, "lib/auth-client");
|
|
1105
|
+
return `"use client";
|
|
1106
|
+
|
|
1107
|
+
import { Suspense, useEffect, useState } from "react";
|
|
1108
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
1109
|
+
import { authClient } from "${authClientImport}";
|
|
1110
|
+
|
|
1111
|
+
function AcceptInvitation() {
|
|
1112
|
+
const router = useRouter();
|
|
1113
|
+
const params = useSearchParams();
|
|
1114
|
+
const id = params.get("id") ?? params.get("invitation");
|
|
1115
|
+
const { data: session, isPending } = authClient.useSession();
|
|
1116
|
+
const [error, setError] = useState<string | null>(null);
|
|
1117
|
+
|
|
1118
|
+
useEffect(() => {
|
|
1119
|
+
if (isPending) return;
|
|
1120
|
+
if (!id) {
|
|
1121
|
+
setError("Invitation is missing.");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!session) {
|
|
1125
|
+
router.replace(\`/signup?invitation=\${encodeURIComponent(id)}\`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
let cancelled = false;
|
|
1129
|
+
void (async () => {
|
|
1130
|
+
const { data, error: acceptError } = await authClient.organization.acceptInvitation({
|
|
1131
|
+
invitationId: id,
|
|
1132
|
+
});
|
|
1133
|
+
if (cancelled) return;
|
|
1134
|
+
if (acceptError) {
|
|
1135
|
+
setError(acceptError.message || "Could not accept invitation.");
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const orgId = data?.invitation?.organizationId ?? data?.member?.organizationId;
|
|
1139
|
+
if (orgId) {
|
|
1140
|
+
await authClient.organization.setActive({ organizationId: orgId });
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
sessionStorage.removeItem("cronus-invitation");
|
|
1144
|
+
} catch {
|
|
1145
|
+
// ignore
|
|
1146
|
+
}
|
|
1147
|
+
window.location.assign("/");
|
|
1148
|
+
})();
|
|
1149
|
+
return () => {
|
|
1150
|
+
cancelled = true;
|
|
1151
|
+
};
|
|
1152
|
+
}, [id, isPending, router, session]);
|
|
1153
|
+
|
|
1154
|
+
return (
|
|
1155
|
+
<main>
|
|
1156
|
+
{error ? <p role="alert">{error}</p> : <p>Accepting invitation…</p>}
|
|
1157
|
+
</main>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export default function AcceptInvitationPage() {
|
|
1162
|
+
return (
|
|
1163
|
+
<Suspense fallback={<main><p>Accepting invitation…</p></main>}>
|
|
1164
|
+
<AcceptInvitation />
|
|
1165
|
+
</Suspense>
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
773
1170
|
function betterAuthClient() {
|
|
774
|
-
return `import {
|
|
1171
|
+
return `import { organizationClient } from "better-auth/client/plugins";
|
|
1172
|
+
import { createAuthClient } from "better-auth/react";
|
|
775
1173
|
|
|
776
|
-
export const authClient = createAuthClient(
|
|
1174
|
+
export const authClient = createAuthClient({
|
|
1175
|
+
plugins: [organizationClient()],
|
|
1176
|
+
});
|
|
777
1177
|
`;
|
|
778
1178
|
}
|
|
779
1179
|
function betterAuthRoute(config) {
|
|
@@ -866,7 +1266,7 @@ export function scaffoldStack(options) {
|
|
|
866
1266
|
emit("postcss.config.mjs", 'export default { plugins: { "@tailwindcss/postcss": {} } };\n');
|
|
867
1267
|
emit(`${app}/globals.css`, globalsCss(config));
|
|
868
1268
|
emit(`${app}/layout.tsx`, layoutTsx(projectName));
|
|
869
|
-
emit(`${app}/page.tsx`, pageTsx());
|
|
1269
|
+
emit(`${app}/page.tsx`, pageTsx(config));
|
|
870
1270
|
emit("cronus-ui.json", `${JSON.stringify({
|
|
871
1271
|
aliases: { ui: "@/components/ui", lib: "@/lib", blocks: "@/components/blocks" },
|
|
872
1272
|
paths: cronusUiPaths(config),
|
|
@@ -888,6 +1288,8 @@ export function scaffoldStack(options) {
|
|
|
888
1288
|
emit(`${libDir(config)}/auth.ts`, betterAuthServer(config));
|
|
889
1289
|
emit(`${libDir(config)}/auth-client.ts`, betterAuthClient());
|
|
890
1290
|
emit(`${app}/api/auth/[...all]/route.ts`, betterAuthRoute(config));
|
|
1291
|
+
emit(single(config, "structure") === "structure-root" ? "middleware.ts" : "src/middleware.ts", betterAuthMiddleware());
|
|
1292
|
+
emit(`${app}/accept-invitation/page.tsx`, acceptInvitationPage(config));
|
|
891
1293
|
}
|
|
892
1294
|
}
|
|
893
1295
|
else {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CREATE_STACK_VERSION = "0.6.
|
|
1
|
+
export declare const CREATE_STACK_VERSION = "0.6.3";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const CREATE_STACK_VERSION = "0.6.
|
|
1
|
+
export const CREATE_STACK_VERSION = "0.6.3";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cronus-stack",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Scaffold a Cronus stack from the Cronus Stack Builder contract: app files, stack.json, KICKOFF.md, and optional AI Kit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"prepublishOnly": "tsc -p tsconfig.json"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@cronus-ui/ai-kit": "0.6.
|
|
48
|
-
"@cronus-ui/stack": "0.6.
|
|
47
|
+
"@cronus-ui/ai-kit": "0.6.3",
|
|
48
|
+
"@cronus-ui/stack": "0.6.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.10.0",
|