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.
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/bw.mjs +1 -1
- package/src/constants.mjs +14 -9
- package/src/generator.mjs +3 -6
- package/src/migrations.mjs +43 -5
- package/src/upgrade.mjs +54 -2
- package/template/base/app/(shell)/dashboard/dashboard-page-mount.tsx +7 -0
- package/template/base/app/(shell)/dashboard/page.tsx +1 -1
- package/template/base/app/(shell)/shell-layout-client.tsx +28 -7
- package/template/modules/crm/app/(shell)/crm/organizations/[id]/page.tsx +1 -1
- package/template/modules/crm/app/(shell)/crm/organizations/page.tsx +1 -3
- package/template/modules/crm/app/(shell)/crm/page.tsx +1 -5
- package/template/modules/crm/app/(shell)/crm/report/page.tsx +1 -5
- package/template/modules/projects/app/(shell)/account/page.tsx +5 -1
- package/template/modules/projects/app/(shell)/account/perfil/page.tsx +1 -0
- package/template/modules/projects/app/(shell)/projetos/projetos-server-mounts.tsx +3 -2
- package/template/modules/projects/app/api/account/projects/[id]/route.ts +2 -0
- package/template/modules/projects/app/api/account/projects/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/client-access/route.ts +2 -0
- package/template/modules/projects/app/api/projects/[id]/organizations/route.ts +2 -0
- package/template/modules/projects/app/api/projects/_handlers.ts +24 -0
- package/template/modules/projects/app/api/projects/setup-options/route.ts +2 -0
- package/template/supabase/modules/orgs/migrations/20260812120000_staff_only_organization_access_management.sql +7 -0
- package/template/supabase/modules/projects/migrations/20260810120000_project_client_access.sql +328 -0
- package/template/supabase/modules/projects/migrations/20260811120000_project_client_access_expand.sql +1061 -0
- package/template/supabase/modules/projects/migrations/20260811120500_project_member_sync_hardening.sql +156 -0
- package/template/supabase/modules/projects/migrations/20260811121000_project_client_access_enforcement.sql +651 -0
- package/template/supabase/modules/projects/migrations/20260811121500_project_client_organization_memberships.sql +29 -0
- package/template/supabase/modules/projects/migrations/20260811121700_project_client_meta_preview.sql +193 -0
- package/template/supabase/modules/projects/migrations/20260811122000_project_client_access_identity_cleanup.sql +201 -0
- 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
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.
|
|
180
|
-
"@brightweblabs/core-auth": "^0.
|
|
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.
|
|
183
|
-
"@brightweblabs/module-crm": "^0.17.
|
|
184
|
-
"@brightweblabs/module-marketing": "^0.4.
|
|
185
|
-
"@brightweblabs/module-orgs": "^0.6.
|
|
186
|
-
"@brightweblabs/module-projects": "^0.
|
|
187
|
-
"@brightweblabs/theme": "^0.8.
|
|
188
|
-
"@brightweblabs/ui": "^1.5.
|
|
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,",
|
package/src/migrations.mjs
CHANGED
|
@@ -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)
|
|
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({
|
|
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
|
|
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 (
|
|
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>
|
|
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
|
|
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 {
|
|
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:
|
|
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/
|
|
1
|
+
export { CrmOrganizationRoutePage as default } from "@brightweblabs/module-crm/routes";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ClientProfilePage as default } from "@brightweblabs/module-projects";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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),
|
|
@@ -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,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;
|