create-bw-app 0.24.9 → 0.25.0

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.
Files changed (32) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/src/bw.mjs +1 -1
  4. package/src/constants.mjs +14 -9
  5. package/src/generator.mjs +3 -6
  6. package/src/migrations.mjs +43 -5
  7. package/src/upgrade.mjs +54 -2
  8. package/template/base/app/(shell)/dashboard/dashboard-page-mount.tsx +7 -0
  9. package/template/base/app/(shell)/dashboard/page.tsx +1 -1
  10. package/template/base/app/(shell)/shell-layout-client.tsx +28 -7
  11. package/template/modules/crm/app/(shell)/crm/organizations/[id]/page.tsx +1 -1
  12. package/template/modules/crm/app/(shell)/crm/organizations/page.tsx +1 -3
  13. package/template/modules/crm/app/(shell)/crm/page.tsx +1 -5
  14. package/template/modules/crm/app/(shell)/crm/report/page.tsx +1 -5
  15. package/template/modules/projects/app/(shell)/account/page.tsx +5 -1
  16. package/template/modules/projects/app/(shell)/account/perfil/page.tsx +1 -0
  17. package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +3 -2
  18. package/template/modules/projects/app/api/account/projects/[id]/route.ts +2 -0
  19. package/template/modules/projects/app/api/account/projects/route.ts +2 -0
  20. package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +2 -0
  21. package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +2 -0
  22. package/template/modules/projects/app/api/projects/_handlers.ts +24 -0
  23. package/template/modules/projects/app/api/projects/setup-options/route.ts +2 -0
  24. package/template/supabase/modules/orgs/migrations/20260812120000_staff_only_organization_access_management.sql +7 -0
  25. package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql +328 -0
  26. package/template/supabase/modules/projects/migrations/20260811120000_project_client_access_expand.sql +1061 -0
  27. package/template/supabase/modules/projects/migrations/20260811120500_project_member_sync_hardening.sql +156 -0
  28. package/template/supabase/modules/projects/migrations/20260811121000_project_client_access_enforcement.sql +651 -0
  29. package/template/supabase/modules/projects/migrations/20260811121500_project_client_organization_memberships.sql +29 -0
  30. package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql +193 -0
  31. package/template/supabase/modules/projects/migrations/20260811122000_project_client_access_identity_cleanup.sql +201 -0
  32. package/template/supabase/modules/projects/migrations/20260811122500_remove_project_client_next_steps.sql +681 -0
package/README.md CHANGED
@@ -32,12 +32,14 @@ From a generated app, use `bw` to manage the machine-readable `.brightweb/app-ma
32
32
  ```bash
33
33
  bw add projects
34
34
  bw upgrade
35
+ bw upgrade projects --through-migration 20260811121700_project_client_meta_preview.sql
35
36
  bw doctor
36
37
  bw admin create --email owner@example.com
37
38
  ```
38
39
 
39
40
  - `bw add <moduleKey>` resolves requirements, installs thin package mounts and module wiring, and appends migrations.
40
- - `bw upgrade [moduleKey]` includes the existing managed update flow plus forward-only module migrations.
41
+ - `bw upgrade [moduleKey]` includes the existing managed update flow plus forward-only module migrations. For a staged database rollout, `bw upgrade <moduleKey> --through-migration <filename>` still updates packages and managed files but appends and records that module only through the named migration.
42
+ - Destructive migrations are held at the preceding safe boundary. After compatible code is deployed and affected data is backed up, apply a held migration with an explicit module-scoped `bw upgrade <moduleKey> --include-destructive-migrations`. A destructive `--through-migration` target also requires that opt-in.
41
43
  - `bw doctor` checks package, config, scaffold, environment-name, migration, configured function-region, and deployed function-region consistency. Pass `--deployment-url` to inspect the deployed `x-vercel-id`; add `--report` to stamp the result in the app manifest.
42
44
  - `bw admin create --email <email>` creates a passwordless Supabase Auth user, transactionally ensures its profile and `admin` assignment, then sends the Core Auth `/reset-password` flow. It always refuses when the project already has an admin (use the in-app admin role controls to add administrators) and never promotes an existing Auth user.
43
45
  - All mutating commands support `--dry-run`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-bw-app",
3
3
  "private": false,
4
- "version": "0.24.9",
4
+ "version": "0.25.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-bw-app": "bin/create-bw-app.mjs",
package/src/bw.mjs CHANGED
@@ -13,7 +13,7 @@ const HELP = `Usage: bw <command> [options]\n\nCommands:\n add <moduleKey>
13
13
  function parseOptions(argv) {
14
14
  const options = {};
15
15
  const positionals = [];
16
- const booleanFlags = new Set(["help", "dry-run", "strict", "report", "install", "refresh-starters", "allow-stale-fallback", "allow-uncursored", "force", "list", "yes"]);
16
+ const booleanFlags = new Set(["help", "dry-run", "strict", "report", "install", "refresh-starters", "include-destructive-migrations", "allow-stale-fallback", "allow-uncursored", "force", "list", "yes"]);
17
17
  const repeatableFlags = new Set(["cursor", "owned-surface", "own", "skip"]);
18
18
  for (let index = 0; index < argv.length; index += 1) {
19
19
  const token = argv[index];
package/src/constants.mjs CHANGED
@@ -123,13 +123,18 @@ export const MODULE_STARTER_FILES = {
123
123
  "app/(shell)/account/projetos/[projectId]/page.tsx",
124
124
  "app/(shell)/account/projetos/[projectId]/loading.tsx",
125
125
  "app/api/projects/_handlers.ts",
126
+ "app/api/account/projects/route.ts",
127
+ "app/api/account/projects/[id]/route.ts",
126
128
  "app/api/dashboard/projects/route.ts",
127
129
  "app/api/dashboard/tasks/route.ts",
128
130
  "app/api/projects/route.ts",
129
131
  "app/api/projects/stats/route.ts",
130
132
  "app/api/projects/organizations/route.ts",
133
+ "app/api/projects/setup-options/route.ts",
131
134
  "app/api/projects/[id]/route.ts",
132
135
  "app/api/projects/[id]/activity/route.ts",
136
+ "app/api/projects/[id]/client-access/route.ts",
137
+ "app/api/projects/[id]/organizations/route.ts",
133
138
  "app/api/projects/[id]/members/route.ts",
134
139
  "app/api/projects/[id]/links/route.ts",
135
140
  "app/api/projects/[id]/links/[itemId]/route.ts",
@@ -176,16 +181,16 @@ export const PLATFORM_STARTER_FILES = [
176
181
  ];
177
182
 
178
183
  export const APP_DEPENDENCY_DEFAULTS = {
179
- "@brightweblabs/app-shell": "^0.15.9",
180
- "@brightweblabs/core-auth": "^0.10.11",
184
+ "@brightweblabs/app-shell": "^0.16.0",
185
+ "@brightweblabs/core-auth": "^0.11.0",
181
186
  "@brightweblabs/infra": "^0.7.0",
182
- "@brightweblabs/module-admin": "^0.9.1",
183
- "@brightweblabs/module-crm": "^0.17.2",
184
- "@brightweblabs/module-marketing": "^0.4.22",
185
- "@brightweblabs/module-orgs": "^0.6.1",
186
- "@brightweblabs/module-projects": "^0.17.10",
187
- "@brightweblabs/theme": "^0.8.2",
188
- "@brightweblabs/ui": "^1.5.3",
187
+ "@brightweblabs/module-admin": "^0.9.2",
188
+ "@brightweblabs/module-crm": "^0.17.3",
189
+ "@brightweblabs/module-marketing": "^0.4.23",
190
+ "@brightweblabs/module-orgs": "^0.6.2",
191
+ "@brightweblabs/module-projects": "^0.18.0",
192
+ "@brightweblabs/theme": "^0.8.3",
193
+ "@brightweblabs/ui": "^1.5.4",
189
194
  "geist": "1.7.2",
190
195
  "lucide-react": "^1.8.0",
191
196
  "next": "^16.0.0",
package/src/generator.mjs CHANGED
@@ -976,7 +976,7 @@ export function createShellConfig(selectedModules) {
976
976
  "const dashboardModuleRegistration: ShellModuleRegistration<ShellContextualAction> = {",
977
977
  ' key: "dashboard",',
978
978
  ' placement: "primary",',
979
- ' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }],',
979
+ ' navItems: [{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard, visibility: "staff" }],',
980
980
  ' toolbarRoutes: [{ surface: "dashboard", match: { exact: ["/dashboard"] } }],',
981
981
  "};",
982
982
  "",
@@ -994,7 +994,7 @@ export function createShellConfig(selectedModules) {
994
994
  " return registrations;",
995
995
  "}",
996
996
  "",
997
- "export function getStarterShellConfig() {",
997
+ "export function getStarterShellConfig(viewer = { isAdmin: true, isStaff: true }) {",
998
998
  " const enabledModules = getEnabledStarterModules();",
999
999
  " const modules = applyShellRegistrationOverrides(",
1000
1000
  " getStarterModuleRegistrations(),",
@@ -1015,10 +1015,7 @@ export function createShellConfig(selectedModules) {
1015
1015
  " };",
1016
1016
  "",
1017
1017
  " const builtRegistration = buildClientAppShellRegistration(shellRegistration);",
1018
- " const shellPreview = resolveClientAppShellConfig(builtRegistration.shellConfig, {",
1019
- " isAdmin: true,",
1020
- " isStaff: true,",
1021
- " });",
1018
+ " const shellPreview = resolveClientAppShellConfig(builtRegistration.shellConfig, viewer);",
1022
1019
  "",
1023
1020
  " return {",
1024
1021
  " enabledModules,",
@@ -25,12 +25,28 @@ export async function getModuleMigrations(moduleKey, catalogEntry = {}) {
25
25
  for (const directory of candidates) {
26
26
  if (!(await pathExists(directory))) continue;
27
27
  const fileNames = (await fs.readdir(directory)).filter((fileName) => fileName.endsWith(".sql")).sort();
28
- if (fileNames.length > 0) return fileNames.map((fileName) => ({ fileName, sourcePath: path.join(directory, fileName) }));
28
+ if (fileNames.length > 0) {
29
+ return Promise.all(fileNames.map(async (fileName) => {
30
+ const sourcePath = path.join(directory, fileName);
31
+ const source = await fs.readFile(sourcePath, "utf8");
32
+ return {
33
+ fileName,
34
+ sourcePath,
35
+ destructive: /^\s*--\s*bw-migration-safety:\s*destructive\s*$/im.test(source),
36
+ };
37
+ }));
38
+ }
29
39
  }
30
40
  return [];
31
41
  }
32
42
 
33
- export async function planMigrationAppends({ targetDir, moduleKeys, catalog, migrationCursor = {} }) {
43
+ export async function planMigrationAppends({
44
+ targetDir,
45
+ moduleKeys,
46
+ catalog,
47
+ migrationCursor = {},
48
+ migrationUpperBounds = {},
49
+ }) {
34
50
  const migrationsDir = await findAppMigrationsDirectory(targetDir);
35
51
  const existing = (await pathExists(migrationsDir))
36
52
  ? (await fs.readdir(migrationsDir)).filter((fileName) => fileName.endsWith(".sql")).sort()
@@ -40,11 +56,33 @@ export async function planMigrationAppends({ targetDir, moduleKeys, catalog, mig
40
56
  return Math.max(maximum, Number(match?.[1] || 0));
41
57
  }, 0);
42
58
  const writes = [];
59
+ const deferred = [];
43
60
  const nextCursor = { ...migrationCursor };
44
61
  for (const moduleKey of moduleKeys) {
45
62
  const migrations = await getModuleMigrations(moduleKey, catalog[moduleKey]);
46
63
  const cursor = migrationCursor[moduleKey];
47
- const pending = cursor ? migrations.filter((entry) => entry.fileName > cursor) : migrations;
64
+ const upperBound = migrationUpperBounds[moduleKey];
65
+ let migrationsInScope = migrations;
66
+ if (upperBound) {
67
+ const upperBoundIndex = migrations.findIndex((entry) => entry.fileName === upperBound);
68
+ if (upperBoundIndex === -1) {
69
+ throw new Error(`Migration cutoff ${upperBound} does not exist for module ${moduleKey}.`);
70
+ }
71
+ if (cursor) {
72
+ const cursorIndex = migrations.findIndex((entry) => entry.fileName === cursor);
73
+ if (cursorIndex === -1) {
74
+ throw new Error(`Migration cutoff blocked: current cursor ${cursor} does not exist in the shipped ${moduleKey} migration history.`);
75
+ }
76
+ if (upperBoundIndex < cursorIndex) {
77
+ throw new Error(`Migration cutoff ${upperBound} is before the current ${moduleKey} cursor ${cursor}.`);
78
+ }
79
+ }
80
+ migrationsInScope = migrations.slice(0, upperBoundIndex + 1);
81
+ deferred.push(...migrations.slice(upperBoundIndex + 1).map((entry) => ({ moduleKey, ...entry })));
82
+ }
83
+ const pending = cursor
84
+ ? migrationsInScope.filter((entry) => entry.fileName > cursor)
85
+ : migrationsInScope;
48
86
  for (const entry of pending) {
49
87
  sequence += 1;
50
88
  const targetFileName = `${String(sequence).padStart(4, "0")}_${moduleKey}__${entry.fileName}`;
@@ -58,9 +96,9 @@ export async function planMigrationAppends({ targetDir, moduleKeys, catalog, mig
58
96
  content: `-- bw-module: ${moduleKey}@${version} ${entry.fileName}\n${source}`,
59
97
  });
60
98
  }
61
- if (migrations.length > 0) nextCursor[moduleKey] = migrations.at(-1).fileName;
99
+ if (migrationsInScope.length > 0) nextCursor[moduleKey] = migrationsInScope.at(-1).fileName;
62
100
  }
63
- return { writes, nextCursor };
101
+ return { writes, deferred, nextCursor };
64
102
  }
65
103
 
66
104
  export async function applyMigrationWrites(writes) {
package/src/upgrade.mjs CHANGED
@@ -7,10 +7,23 @@ import { applyMigrationWrites, getModuleMigrations, planMigrationAppends } from
7
7
  import { buildBrightwebAppUpdatePlan } from "./update.mjs";
8
8
  import { resolveSafeRelativePath } from "./safe-path.mjs";
9
9
 
10
- const HELP = `Usage: bw upgrade [moduleKey] [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --allow-stale-fallback Use baked-in versions if npm lookup fails\n --install Install changed dependencies\n --refresh-starters Refresh unchanged starter files\n --dry-run Print the upgrade plan without writing\n --help Show this help`;
10
+ const HELP = `Usage: bw upgrade [moduleKey] [options]\n\nOptions:\n --target-dir <path> App directory (defaults to cwd)\n --workspace-root <path> BrightWeb workspace root\n --through-migration <file> Advance the named module only through this migration\n --include-destructive-migrations Explicitly include held destructive migrations\n --allow-stale-fallback Use baked-in versions if npm lookup fails\n --install Install changed dependencies\n --refresh-starters Refresh unchanged starter files\n --dry-run Print the upgrade plan without writing\n --help Show this help`;
11
11
 
12
12
  export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOptions = {}) {
13
13
  if (argvOptions.help) { output.write(`${HELP}\n`); return { help: true }; }
14
+ const throughMigration = typeof argvOptions.throughMigration === "string"
15
+ ? argvOptions.throughMigration.trim()
16
+ : "";
17
+ if (argvOptions.throughMigration != null && !throughMigration) {
18
+ throw new Error("--through-migration requires a migration filename.");
19
+ }
20
+ if (throughMigration && !moduleKey) {
21
+ throw new Error("--through-migration requires an explicit module key, for example: bw upgrade projects --through-migration <filename>.");
22
+ }
23
+ const includeDestructiveMigrations = argvOptions.includeDestructiveMigrations === true;
24
+ if (includeDestructiveMigrations && !moduleKey) {
25
+ throw new Error("--include-destructive-migrations requires an explicit module key so destructive scope cannot expand implicitly.");
26
+ }
14
27
  const targetDir = path.resolve(runtimeOptions.targetDir || argvOptions.targetDir || process.cwd());
15
28
  const appManifest = await readAppManifest(targetDir);
16
29
  if (moduleKey && !appManifest.modules[moduleKey]) throw new Error(`Module ${moduleKey} is not installed according to ${path.join(".brightweb", "app-manifest.json")}.`);
@@ -50,8 +63,47 @@ export async function upgradeBrightwebApp(moduleKey, argvOptions = {}, runtimeOp
50
63
  if (appManifest.migrationCursor?.[key] == null && (await getModuleMigrations(key, catalog[key])).length > 0) uncursored.push(key);
51
64
  }
52
65
  if (uncursored.length > 0) throw new Error(`Migration upgrade blocked: ${uncursored.join(", ")} ${uncursored.length === 1 ? "has" : "have"} a null migration cursor. Set an explicit cursor with bw adopt --force --cursor before upgrading.`);
53
- const migrationPlan = await planMigrationAppends({ targetDir, moduleKeys, catalog, migrationCursor: appManifest.migrationCursor });
66
+ const migrationUpperBounds = {};
67
+ const safetyBoundaries = [];
68
+ for (const key of moduleKeys) {
69
+ const migrations = await getModuleMigrations(key, catalog[key]);
70
+ const destructiveIndex = migrations.findIndex((entry) => entry.destructive);
71
+ if (throughMigration && key === moduleKey) {
72
+ const cutoffIndex = migrations.findIndex((entry) => entry.fileName === throughMigration);
73
+ if (cutoffIndex >= destructiveIndex && destructiveIndex >= 0 && !includeDestructiveMigrations) {
74
+ throw new Error(`Migration cutoff ${throughMigration} includes destructive migration ${migrations[destructiveIndex].fileName}. Re-run with --include-destructive-migrations after reviewing and backing up affected data.`);
75
+ }
76
+ migrationUpperBounds[key] = throughMigration;
77
+ continue;
78
+ }
79
+ if (includeDestructiveMigrations || destructiveIndex < 0) continue;
80
+ const cursorIndex = migrations.findIndex((entry) => entry.fileName === appManifest.migrationCursor?.[key]);
81
+ if (cursorIndex >= destructiveIndex) continue;
82
+ if (destructiveIndex === 0) {
83
+ throw new Error(`Migration upgrade blocked: ${key} begins with destructive migration ${migrations[0].fileName}; use --include-destructive-migrations after review.`);
84
+ }
85
+ const boundary = migrations[destructiveIndex - 1].fileName;
86
+ migrationUpperBounds[key] = boundary;
87
+ safetyBoundaries.push({ moduleKey: key, boundary, destructiveMigration: migrations[destructiveIndex].fileName });
88
+ }
89
+ const migrationPlan = await planMigrationAppends({
90
+ targetDir,
91
+ moduleKeys,
92
+ catalog,
93
+ migrationCursor: appManifest.migrationCursor,
94
+ migrationUpperBounds,
95
+ });
54
96
  output.write(`bw upgrade\nPackages to update: ${plan.packageUpdates.length}\nManaged files to write: ${plan.fileWrites.length}\nMigrations to append: ${migrationPlan.writes.length}\n`);
97
+ if (throughMigration) output.write(`Migration cutoff: ${moduleKey} through ${throughMigration}\n`);
98
+ for (const boundary of safetyBoundaries) {
99
+ output.write(`Migration safety boundary: ${boundary.moduleKey} through ${boundary.boundary}; held ${boundary.destructiveMigration}\n`);
100
+ }
101
+ if (migrationPlan.deferred.length > 0) {
102
+ output.write(`Migrations deferred: ${migrationPlan.deferred.length}\n`);
103
+ for (const entry of migrationPlan.deferred) {
104
+ output.write(`- deferred migration: ${entry.moduleKey}/${entry.fileName}\n`);
105
+ }
106
+ }
55
107
  for (const relativePath of missing) output.write(`- missing: ${relativePath}\n`);
56
108
  for (const relativePath of drifted) output.write(`- drifted: ${relativePath}\n`);
57
109
  for (const relativePath of intentional) output.write(`- intent-protected: ${relativePath}\n`);
@@ -0,0 +1,7 @@
1
+ import { requireServerPageRoleAccess } from "@brightweblabs/core-auth/server";
2
+ import { DashboardLiveMount } from "./dashboard-live-mount";
3
+
4
+ export async function DashboardPageMount() {
5
+ await requireServerPageRoleAccess(["admin", "staff"]);
6
+ return <DashboardLiveMount />;
7
+ }
@@ -1 +1 @@
1
- export { DashboardLiveMount as default } from "./dashboard-live-mount";
1
+ export { DashboardPageMount as default } from "./dashboard-page-mount";
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation";
5
5
  import {
6
6
  AppHeader,
7
7
  AppShellFrame,
8
+ ClientPortalFrame,
8
9
  DesktopSidebar,
9
10
  MobileNav,
10
11
  ShellActionsProvider,
@@ -51,8 +52,8 @@ function ShellLayoutInner({
51
52
  const pathname = usePathname() ?? "";
52
53
  const router = useRouter();
53
54
  const { shellPreview: config, toolbarRoutes, toolbarActions } = useMemo(
54
- () => getStarterShellConfig(),
55
- [],
55
+ () => getStarterShellConfig({ isAdmin: viewer.isAdmin, isStaff: viewer.isStaff }),
56
+ [viewer.isAdmin, viewer.isStaff],
56
57
  );
57
58
  const shellGroups = useMemo<ShellNavStateGroup[]>(
58
59
  () => [
@@ -100,6 +101,30 @@ function ShellLayoutInner({
100
101
  if (item.action) dispatchShellAction(item.action);
101
102
  };
102
103
 
104
+ const handleSignOut = async () => {
105
+ await authClient.signOutLocal();
106
+ router.replace("/login");
107
+ router.refresh();
108
+ };
109
+
110
+ if (!viewer.isStaff && !viewer.isAdmin) {
111
+ return (
112
+ <ClientPortalFrame
113
+ brand={config.brand}
114
+ pathname={pathname}
115
+ displayName={displayName}
116
+ onSignOut={handleSignOut}
117
+ user={{
118
+ email: viewer.email,
119
+ user_metadata: { first_name: viewer.firstName, last_name: viewer.lastName },
120
+ }}
121
+ userInitials={computeInitials(viewer.email, viewer.firstName ?? undefined, viewer.lastName ?? undefined)}
122
+ >
123
+ {children}
124
+ </ClientPortalFrame>
125
+ );
126
+ }
127
+
103
128
  return (
104
129
  <AppShellFrame
105
130
  collapsed={isSidebarCollapsed}
@@ -125,11 +150,7 @@ function ShellLayoutInner({
125
150
  account={{
126
151
  displayName,
127
152
  isStaff: viewer.isStaff,
128
- onSignOut: async () => {
129
- await authClient.signOutLocal();
130
- router.replace("/login");
131
- router.refresh();
132
- },
153
+ onSignOut: handleSignOut,
133
154
  user: {
134
155
  email: viewer.email,
135
156
  user_metadata: {
@@ -1 +1 @@
1
- export { CrmOrganizationRoutePage as default } from "@brightweblabs/module-crm/ui";
1
+ export { CrmOrganizationRoutePage as default } from "@brightweblabs/module-crm/routes";
@@ -1,3 +1 @@
1
- import { CrmOrganizationsPage } from "@brightweblabs/module-crm/ui";
2
-
3
- export default function OrganizationsPage() { return <CrmOrganizationsPage />; }
1
+ export { CrmOrganizationsRoutePage as default } from "@brightweblabs/module-crm/routes";
@@ -1,5 +1 @@
1
- import { CrmDashboard } from "@brightweblabs/module-crm/ui";
2
-
3
- export default function CrmPage() {
4
- return <CrmDashboard />;
5
- }
1
+ export { CrmDashboardRoutePage as default } from "@brightweblabs/module-crm/routes";
@@ -1,5 +1 @@
1
- import { CrmReportPage } from "@brightweblabs/module-crm/ui";
2
-
3
- export default function ReportPage() {
4
- return <CrmReportPage />;
5
- }
1
+ export { CrmReportRoutePage as default } from "@brightweblabs/module-crm/routes";
@@ -1 +1,5 @@
1
- export { ClientAccountPage as default } from "@brightweblabs/module-projects";
1
+ import { ClientAccountPage } from "@brightweblabs/module-projects";
2
+
3
+ export default function Page() {
4
+ return <ClientAccountPage />;
5
+ }
@@ -0,0 +1 @@
1
+ export { ClientProfilePage as default } from "@brightweblabs/module-projects";
@@ -1,4 +1,4 @@
1
- import { requireServerPageAccess } from "@brightweblabs/core-auth/server";
1
+ import { requireServerPageRoleAccess } from "@brightweblabs/core-auth/server";
2
2
  import {
3
3
  getProjectDashboard,
4
4
  getProjectAccess,
@@ -18,6 +18,7 @@ const navigation = {
18
18
  };
19
19
 
20
20
  export async function ProjectsServerMount() {
21
+ await requireServerPageRoleAccess(["admin", "staff"]);
21
22
  const { organizationOptions, portfolioStats, result } = await getProjectsPortfolioPageData();
22
23
  const attentionSummary = {
23
24
  total: result.total,
@@ -38,7 +39,7 @@ export async function ProjectsServerMount() {
38
39
 
39
40
  async function getInitialProject(params: Promise<{ projectId: string }>) {
40
41
  const { projectId } = await params;
41
- const { supabase, profileId, role } = await requireServerPageAccess();
42
+ const { supabase, profileId, role } = await requireServerPageRoleAccess(["admin", "staff"]);
42
43
  const [initialData, access] = await Promise.all([
43
44
  getProjectDashboard(supabase, projectId),
44
45
  getProjectAccess(supabase, projectId, profileId, role),
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { clientProjectDetailGet as GET } from "../../../projects/_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { clientProjectsGet as GET } from "../../projects/_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectClientAccessGet as GET, projectClientAccessPatch as PATCH } from "../../_handlers";
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectOrganizationsPatch as PATCH } from "../../_handlers";
@@ -1,4 +1,9 @@
1
1
  import {
2
+ handleClientProjectDetailGetRequest,
3
+ handleClientProjectsGetRequest,
4
+ handleProjectClientAccessGetRequest,
5
+ handleProjectClientAccessPatchRequest,
6
+ handleProjectOrganizationsPatchRequest,
2
7
  handleProjectsActivityGetRequest,
3
8
  handleProjectsAssignableProfilesGetRequest,
4
9
  handleProjectsDashboardGetRequest,
@@ -16,6 +21,7 @@ import {
16
21
  handleProjectsPatchRequest,
17
22
  handleProjectsPostRequest,
18
23
  handleProjectsStatsGetRequest,
24
+ handleProjectsSetupOptionsGetRequest,
19
25
  handleProjectsTasksDeleteRequest,
20
26
  handleProjectsTasksPatchRequest,
21
27
  handleProjectsTasksPostRequest,
@@ -27,6 +33,24 @@ type ItemRouteContext = { params: Promise<{ id: string; itemId: string }> };
27
33
  export function projectsGet(request: Request) {
28
34
  return handleProjectsGetRequest(request);
29
35
  }
36
+ export function clientProjectsGet(request: Request) {
37
+ return handleClientProjectsGetRequest(request);
38
+ }
39
+ export function clientProjectDetailGet(request: Request, context: IdRouteContext) {
40
+ return handleClientProjectDetailGetRequest(request, context);
41
+ }
42
+ export function projectClientAccessGet(request: Request, context: IdRouteContext) {
43
+ return handleProjectClientAccessGetRequest(request, context);
44
+ }
45
+ export function projectClientAccessPatch(request: Request, context: IdRouteContext) {
46
+ return handleProjectClientAccessPatchRequest(request, context);
47
+ }
48
+ export function projectOrganizationsPatch(request: Request, context: IdRouteContext) {
49
+ return handleProjectOrganizationsPatchRequest(request, context);
50
+ }
51
+ export function projectsSetupOptionsGet(request: Request) {
52
+ return handleProjectsSetupOptionsGetRequest(request);
53
+ }
30
54
  export function projectsPost(request: Request) {
31
55
  return handleProjectsPostRequest(request);
32
56
  }
@@ -0,0 +1,2 @@
1
+ export const dynamic = "force-dynamic";
2
+ export { projectsSetupOptionsGet as GET } from "../_handlers";
@@ -0,0 +1,7 @@
1
+ -- Organization roles remain useful client-facing membership metadata, but
2
+ -- only global staff and administrators may manage members or invitations.
3
+ DROP POLICY IF EXISTS "Org admins manage org members"
4
+ ON public.organization_members;
5
+
6
+ DROP POLICY IF EXISTS "Org admins manage organization invitations"
7
+ ON public.organization_invitations;