create-bw-app 0.13.0 → 0.13.2
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/package.json +2 -2
- package/src/constants.mjs +17 -13
- package/src/generator.mjs +2 -0
- package/template/base/app/fonts.ts +2 -0
- package/template/base/app/layout.tsx +5 -2
- package/template/modules/crm/app/api/crm/contacts/route.ts +1 -0
- package/template/modules/crm/app/api/crm/report/route.ts +1 -0
- package/template/modules/crm/app/crm/report/page.tsx +5 -0
- package/template/site/base/app/fonts.ts +1 -0
- package/template/site/base/app/globals.css +11 -8
- package/template/site/base/app/layout.tsx +2 -1
- package/template/supabase/modules/admin/migrations/20260724121000_admin_user_invitations.sql +81 -0
- package/template/supabase/modules/crm/migrations/20260724120000_crm_status_authorization.sql +59 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bw-app",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"create-bw-app": "bin/create-bw-app.mjs",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"node": ">=20"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@inquirer/prompts": "^7.
|
|
28
|
+
"@inquirer/prompts": "^7.10.1"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/constants.mjs
CHANGED
|
@@ -60,9 +60,11 @@ export const MODULE_STARTER_FILES = {
|
|
|
60
60
|
crm: [
|
|
61
61
|
"app/crm/layout.tsx",
|
|
62
62
|
"app/crm/page.tsx",
|
|
63
|
+
"app/crm/report/page.tsx",
|
|
63
64
|
"app/api/crm/contacts/route.ts",
|
|
64
65
|
"app/api/crm/organizations/route.ts",
|
|
65
66
|
"app/api/crm/owners/route.ts",
|
|
67
|
+
"app/api/crm/report/route.ts",
|
|
66
68
|
"app/api/crm/stats/route.ts",
|
|
67
69
|
"app/api/crm/timeline/route.ts",
|
|
68
70
|
],
|
|
@@ -75,25 +77,27 @@ export const PLATFORM_STARTER_FILES = [
|
|
|
75
77
|
];
|
|
76
78
|
|
|
77
79
|
export const APP_DEPENDENCY_DEFAULTS = {
|
|
78
|
-
"@brightweblabs/app-shell": "^0.
|
|
79
|
-
"@brightweblabs/core-auth": "^0.
|
|
80
|
-
"@brightweblabs/infra": "^0.3.
|
|
81
|
-
"@brightweblabs/module-admin": "^0.
|
|
82
|
-
"@brightweblabs/module-crm": "^0.
|
|
83
|
-
"@brightweblabs/module-orgs": "^0.
|
|
84
|
-
"@brightweblabs/module-projects": "^0.
|
|
85
|
-
"@brightweblabs/theme": "^0.
|
|
86
|
-
"@brightweblabs/ui": "^1.0
|
|
80
|
+
"@brightweblabs/app-shell": "^0.6.0",
|
|
81
|
+
"@brightweblabs/core-auth": "^0.4.0",
|
|
82
|
+
"@brightweblabs/infra": "^0.3.2",
|
|
83
|
+
"@brightweblabs/module-admin": "^0.5.0",
|
|
84
|
+
"@brightweblabs/module-crm": "^0.8.0",
|
|
85
|
+
"@brightweblabs/module-orgs": "^0.3.0",
|
|
86
|
+
"@brightweblabs/module-projects": "^0.5.0",
|
|
87
|
+
"@brightweblabs/theme": "^0.4.0",
|
|
88
|
+
"@brightweblabs/ui": "^1.1.0",
|
|
89
|
+
"geist": "1.7.2",
|
|
87
90
|
"lucide-react": "^1.8.0",
|
|
88
91
|
"next": "16.1.6",
|
|
89
|
-
"react": "19.2.
|
|
90
|
-
"react-dom": "19.2.
|
|
92
|
+
"react": "19.2.4",
|
|
93
|
+
"react-dom": "19.2.4",
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
export const SITE_DEPENDENCY_DEFAULTS = {
|
|
97
|
+
"geist": "1.7.2",
|
|
94
98
|
"next": "16.1.6",
|
|
95
|
-
"react": "19.2.
|
|
96
|
-
"react-dom": "19.2.
|
|
99
|
+
"react": "19.2.4",
|
|
100
|
+
"react-dom": "19.2.4",
|
|
97
101
|
};
|
|
98
102
|
|
|
99
103
|
export const APP_DEV_DEPENDENCY_DEFAULTS = {
|
package/src/generator.mjs
CHANGED
|
@@ -711,6 +711,7 @@ export function createPackageJson({
|
|
|
711
711
|
lint: "tsc --noEmit",
|
|
712
712
|
},
|
|
713
713
|
dependencies: sortObjectKeys({
|
|
714
|
+
"geist": versionMap.geist,
|
|
714
715
|
"next": versionMap.next,
|
|
715
716
|
"react": versionMap.react,
|
|
716
717
|
"react-dom": versionMap["react-dom"],
|
|
@@ -735,6 +736,7 @@ export function createPackageJson({
|
|
|
735
736
|
"@brightweblabs/infra": internalDependencyVersion("@brightweblabs/infra"),
|
|
736
737
|
"@brightweblabs/theme": internalDependencyVersion("@brightweblabs/theme"),
|
|
737
738
|
"@brightweblabs/ui": internalDependencyVersion("@brightweblabs/ui"),
|
|
739
|
+
"geist": versionMap.geist,
|
|
738
740
|
"lucide-react": versionMap["lucide-react"],
|
|
739
741
|
"next": versionMap.next,
|
|
740
742
|
"react": versionMap.react,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { starterBrandConfig } from "../config/brand";
|
|
3
|
+
import { geistMono, geistSans } from "./fonts";
|
|
3
4
|
import "./globals.css";
|
|
4
5
|
|
|
5
6
|
export const metadata = {
|
|
@@ -9,8 +10,10 @@ export const metadata = {
|
|
|
9
10
|
|
|
10
11
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
11
12
|
return (
|
|
12
|
-
<html lang="en">
|
|
13
|
-
<body>
|
|
13
|
+
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
|
|
14
|
+
<body>
|
|
15
|
+
{children}
|
|
16
|
+
</body>
|
|
14
17
|
</html>
|
|
15
18
|
);
|
|
16
19
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { handleCrmContactsGetRequest as GET } from "@brightweblabs/module-crm";
|
|
2
|
+
export { handleCrmContactsDeleteRequest as DELETE } from "@brightweblabs/module-crm";
|
|
2
3
|
export { handleCrmContactsPatchRequest as PATCH } from "@brightweblabs/module-crm";
|
|
3
4
|
export { handleCrmContactsPostRequest as POST } from "@brightweblabs/module-crm";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { handleCrmReportGetRequest as GET } from "@brightweblabs/module-crm";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GeistSans as geistSans } from "geist/font/sans";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
|
|
3
3
|
@theme inline {
|
|
4
|
-
--font-sans: var(--font-sans);
|
|
5
|
-
--font-display: var(--font-
|
|
4
|
+
--font-sans: var(--font-body, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
|
|
5
|
+
--font-display: var(--font-heading, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
|
|
6
6
|
--color-background: var(--background);
|
|
7
7
|
--color-foreground: var(--foreground);
|
|
8
8
|
--color-muted-foreground: var(--muted-foreground);
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
:root {
|
|
20
20
|
color-scheme: light;
|
|
21
|
-
--font-
|
|
22
|
-
--font-
|
|
21
|
+
--font-body: var(--font-geist-sans);
|
|
22
|
+
--font-sans: var(--font-body);
|
|
23
|
+
--font-display: var(--font-body);
|
|
23
24
|
--background: #f3efe5;
|
|
24
25
|
--foreground: #18241d;
|
|
25
26
|
--muted-foreground: #5a665c;
|
|
@@ -39,7 +40,8 @@
|
|
|
39
40
|
--line: var(--border);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
@layer base {
|
|
44
|
+
* {
|
|
43
45
|
box-sizing: border-box;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -55,7 +57,7 @@ body {
|
|
|
55
57
|
radial-gradient(circle at 85% 12%, rgba(189, 140, 89, 0.12), transparent 18rem),
|
|
56
58
|
linear-gradient(180deg, #faf8f1 0%, var(--background) 48%, #e8e0cf 100%);
|
|
57
59
|
color: var(--foreground);
|
|
58
|
-
font-family: var(--font-sans)
|
|
60
|
+
font-family: var(--font-sans);
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
body::before {
|
|
@@ -76,6 +78,7 @@ a {
|
|
|
76
78
|
text-decoration: none;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
::selection {
|
|
80
|
-
|
|
81
|
+
::selection {
|
|
82
|
+
background: rgba(38, 105, 70, 0.18);
|
|
83
|
+
}
|
|
81
84
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { siteConfig } from "@/config/site";
|
|
4
|
+
import { geistSans } from "./fonts";
|
|
4
5
|
import "./globals.css";
|
|
5
6
|
|
|
6
7
|
export const metadata: Metadata = {
|
|
@@ -11,7 +12,7 @@ export const metadata: Metadata = {
|
|
|
11
12
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
12
13
|
return (
|
|
13
14
|
<html lang="en">
|
|
14
|
-
<body>{children}</body>
|
|
15
|
+
<body className={geistSans.variable}>{children}</body>
|
|
15
16
|
</html>
|
|
16
17
|
);
|
|
17
18
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
-- Admin-managed app user invitations, ported from MQ.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS public.admin_user_invitations (
|
|
4
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
5
|
+
invited_email text NOT NULL,
|
|
6
|
+
role_code text NOT NULL REFERENCES public.roles(code) CHECK (role_code IN ('staff', 'admin')),
|
|
7
|
+
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')),
|
|
8
|
+
invited_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
9
|
+
accepted_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
10
|
+
accepted_at timestamptz,
|
|
11
|
+
revoked_at timestamptz,
|
|
12
|
+
expires_at timestamptz NOT NULL DEFAULT (now() + interval '14 days'),
|
|
13
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
14
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE UNIQUE INDEX IF NOT EXISTS admin_user_invitations_unique_pending_email
|
|
18
|
+
ON public.admin_user_invitations (lower(invited_email))
|
|
19
|
+
WHERE status = 'pending';
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_admin_user_invitations_status
|
|
22
|
+
ON public.admin_user_invitations (status, created_at DESC);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_admin_user_invitations_email_status
|
|
25
|
+
ON public.admin_user_invitations (lower(invited_email), status);
|
|
26
|
+
|
|
27
|
+
CREATE OR REPLACE FUNCTION public.touch_admin_user_invitations_updated_at()
|
|
28
|
+
RETURNS trigger
|
|
29
|
+
LANGUAGE plpgsql
|
|
30
|
+
SECURITY DEFINER
|
|
31
|
+
SET search_path = public
|
|
32
|
+
AS $$
|
|
33
|
+
BEGIN
|
|
34
|
+
NEW.updated_at := now();
|
|
35
|
+
RETURN NEW;
|
|
36
|
+
END;
|
|
37
|
+
$$;
|
|
38
|
+
|
|
39
|
+
DROP TRIGGER IF EXISTS trg_touch_admin_user_invitations_updated_at ON public.admin_user_invitations;
|
|
40
|
+
CREATE TRIGGER trg_touch_admin_user_invitations_updated_at
|
|
41
|
+
BEFORE UPDATE ON public.admin_user_invitations
|
|
42
|
+
FOR EACH ROW
|
|
43
|
+
EXECUTE FUNCTION public.touch_admin_user_invitations_updated_at();
|
|
44
|
+
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.normalize_admin_user_invitation_email()
|
|
46
|
+
RETURNS trigger
|
|
47
|
+
LANGUAGE plpgsql
|
|
48
|
+
SECURITY DEFINER
|
|
49
|
+
SET search_path = public
|
|
50
|
+
AS $$
|
|
51
|
+
BEGIN
|
|
52
|
+
NEW.invited_email := lower(trim(NEW.invited_email));
|
|
53
|
+
RETURN NEW;
|
|
54
|
+
END;
|
|
55
|
+
$$;
|
|
56
|
+
|
|
57
|
+
DROP TRIGGER IF EXISTS trg_normalize_admin_user_invitation_email ON public.admin_user_invitations;
|
|
58
|
+
CREATE TRIGGER trg_normalize_admin_user_invitation_email
|
|
59
|
+
BEFORE INSERT OR UPDATE OF invited_email ON public.admin_user_invitations
|
|
60
|
+
FOR EACH ROW
|
|
61
|
+
EXECUTE FUNCTION public.normalize_admin_user_invitation_email();
|
|
62
|
+
|
|
63
|
+
ALTER TABLE public.admin_user_invitations ENABLE ROW LEVEL SECURITY;
|
|
64
|
+
|
|
65
|
+
DROP POLICY IF EXISTS "Admins manage app user invitations" ON public.admin_user_invitations;
|
|
66
|
+
CREATE POLICY "Admins manage app user invitations"
|
|
67
|
+
ON public.admin_user_invitations
|
|
68
|
+
FOR ALL
|
|
69
|
+
TO authenticated
|
|
70
|
+
USING (public.is_admin())
|
|
71
|
+
WITH CHECK (public.is_admin());
|
|
72
|
+
|
|
73
|
+
DROP POLICY IF EXISTS "Invited app users view own invitations" ON public.admin_user_invitations;
|
|
74
|
+
CREATE POLICY "Invited app users view own invitations"
|
|
75
|
+
ON public.admin_user_invitations
|
|
76
|
+
FOR SELECT
|
|
77
|
+
TO authenticated
|
|
78
|
+
USING (
|
|
79
|
+
status = 'pending'
|
|
80
|
+
AND lower(invited_email) = lower(coalesce(auth.jwt() ->> 'email', ''))
|
|
81
|
+
);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- Restrict the CRM status transition RPC to staff and trusted service calls.
|
|
2
|
+
|
|
3
|
+
CREATE OR REPLACE FUNCTION public.set_crm_status(
|
|
4
|
+
p_contact_id uuid,
|
|
5
|
+
p_new_status text,
|
|
6
|
+
p_reason text DEFAULT NULL
|
|
7
|
+
)
|
|
8
|
+
RETURNS void
|
|
9
|
+
LANGUAGE plpgsql
|
|
10
|
+
SECURITY DEFINER
|
|
11
|
+
SET search_path = public
|
|
12
|
+
AS $$
|
|
13
|
+
DECLARE
|
|
14
|
+
v_current text;
|
|
15
|
+
BEGIN
|
|
16
|
+
IF COALESCE(auth.role(), '') <> 'service_role'
|
|
17
|
+
AND NOT COALESCE(public.is_staff(), false)
|
|
18
|
+
THEN
|
|
19
|
+
RAISE EXCEPTION 'Staff access required'
|
|
20
|
+
USING ERRCODE = '42501';
|
|
21
|
+
END IF;
|
|
22
|
+
|
|
23
|
+
SELECT status INTO v_current
|
|
24
|
+
FROM public.crm_contacts
|
|
25
|
+
WHERE id = p_contact_id;
|
|
26
|
+
|
|
27
|
+
IF v_current IS NULL THEN
|
|
28
|
+
v_current := 'none';
|
|
29
|
+
END IF;
|
|
30
|
+
|
|
31
|
+
IF v_current = p_new_status THEN
|
|
32
|
+
RETURN;
|
|
33
|
+
END IF;
|
|
34
|
+
|
|
35
|
+
UPDATE public.crm_contacts
|
|
36
|
+
SET status = p_new_status,
|
|
37
|
+
updated_at = now()
|
|
38
|
+
WHERE id = p_contact_id;
|
|
39
|
+
|
|
40
|
+
INSERT INTO public.crm_status_log (
|
|
41
|
+
contact_id,
|
|
42
|
+
previous_status,
|
|
43
|
+
new_status,
|
|
44
|
+
reason,
|
|
45
|
+
changed_by_user_id
|
|
46
|
+
) VALUES (
|
|
47
|
+
p_contact_id,
|
|
48
|
+
v_current,
|
|
49
|
+
p_new_status,
|
|
50
|
+
p_reason,
|
|
51
|
+
auth.uid()
|
|
52
|
+
);
|
|
53
|
+
END;
|
|
54
|
+
$$;
|
|
55
|
+
|
|
56
|
+
REVOKE EXECUTE ON FUNCTION public.set_crm_status(uuid, text, text) FROM PUBLIC;
|
|
57
|
+
REVOKE EXECUTE ON FUNCTION public.set_crm_status(uuid, text, text) FROM anon;
|
|
58
|
+
GRANT EXECUTE ON FUNCTION public.set_crm_status(uuid, text, text) TO authenticated;
|
|
59
|
+
GRANT EXECUTE ON FUNCTION public.set_crm_status(uuid, text, text) TO service_role;
|