showpane 0.4.27 → 0.4.28
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/bundle/meta/scaffold-manifest.json +5 -5
- package/bundle/scaffold/src/__tests__/branding.test.ts +6 -0
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +5 -18
- package/bundle/scaffold/src/components/portal-login.tsx +21 -4
- package/bundle/scaffold/src/lib/branding.ts +18 -2
- package/bundle/toolchain/CLI_VERSION +1 -1
- package/bundle/toolchain/bin/create-portal.ts +16 -5
- package/bundle/toolchain/bin/export-runtime-state.ts +51 -15
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
3
|
+
"generatedAt": "2026-04-13T23:18:58.439Z",
|
|
4
4
|
"scaffoldVersion": "0.2.7",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"scripts/prisma-db-push.mjs": "76ac85fe65b5dc3d9cc7432e44618fcc84b7443574c8d88198d01f13ac23c040",
|
|
20
20
|
"scripts/prisma-generate.mjs": "d371e63388fa39f963b7c3c7cb8f87e0d9cd43cbf69d254b999108e29b8738c8",
|
|
21
21
|
"scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
|
|
22
|
-
"src/__tests__/branding.test.ts": "
|
|
22
|
+
"src/__tests__/branding.test.ts": "ed6246013ca9465e3ee73de72791b781556a75b65935ed61e116d3dcc7885b53",
|
|
23
23
|
"src/__tests__/client-portals.test.ts": "9c3236bf0f7190b7d5ba9082287dcb29bc00d28dd63782a89505125ead06c624",
|
|
24
24
|
"src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
|
|
25
25
|
"src/__tests__/portal-contracts.test.ts": "a3ecf29c460a14f22ef48449fc9de5b945ce5f03423f12380d05e01159d1e3b4",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"src/app/(portal)/client/example/example-client.tsx": "ed32b111acea861f448d865338f8841d47c6ca7c2f87ed30d85bb0804940d4ec",
|
|
29
29
|
"src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
|
|
30
30
|
"src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
|
|
31
|
-
"src/app/(portal)/client/page.tsx": "
|
|
31
|
+
"src/app/(portal)/client/page.tsx": "bb22886df765690f2ce0c2ddfcac3dcfe4fa5845142d358c596d813d86c3e849",
|
|
32
32
|
"src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
|
|
33
33
|
"src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
|
|
34
34
|
"src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
|
|
42
42
|
"src/app/page.tsx": "1f71205c3ae30bf6929d37947b0c94ae53aed33b6689b7d6b13066ad51c1bc14",
|
|
43
43
|
"src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
|
|
44
|
-
"src/components/portal-login.tsx": "
|
|
44
|
+
"src/components/portal-login.tsx": "1056ae210dfa16b69b69ca63be988025d11cc543daf253f17ffb97cb446be30f",
|
|
45
45
|
"src/components/portal-shell.tsx": "f46a0f753a4a0318f06c8b4e46295febb84b03ea082c95057a6da50c737b4e21",
|
|
46
46
|
"src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
|
|
47
|
-
"src/lib/branding.ts": "
|
|
47
|
+
"src/lib/branding.ts": "c4faf299209aa7e07ec4ff24237a1c9f7ed4e87f0353cf9393d9c484d987cf04",
|
|
48
48
|
"src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
|
|
49
49
|
"src/lib/client-portals.ts": "9b531f9a9ea459b4ab85257b9dd282874fa1422838fe89d511940e417114216a",
|
|
50
50
|
"src/lib/control-plane.ts": "e0cf39f28ec7de715fd5cfbb5f4240773fcd3d775cd1677588dd749fff740a0e",
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getDomainFromWebsite,
|
|
6
6
|
getLogoUrl,
|
|
7
7
|
normalizeWebsiteUrl,
|
|
8
|
+
resolvePortalLabel,
|
|
8
9
|
} from "@/lib/branding";
|
|
9
10
|
|
|
10
11
|
describe("branding helpers", () => {
|
|
@@ -40,4 +41,9 @@ describe("branding helpers", () => {
|
|
|
40
41
|
}),
|
|
41
42
|
).toBe(getLogoUrl("", "Bidgen"));
|
|
42
43
|
});
|
|
44
|
+
|
|
45
|
+
it("uses the org name when the stored portal label is still generic", () => {
|
|
46
|
+
expect(resolvePortalLabel("Bidgen-Test", "Client Portal")).toBe("Bidgen-Test Portal");
|
|
47
|
+
expect(resolvePortalLabel("Bidgen-Test", "Bidgen-Test Workspace")).toBe("Bidgen-Test Workspace");
|
|
48
|
+
});
|
|
43
49
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PortalLogin } from "@/components/portal-login";
|
|
2
|
-
import { getBrandLogoUrl } from "@/lib/branding";
|
|
2
|
+
import { getBrandLogoUrl, resolvePortalLabel } from "@/lib/branding";
|
|
3
3
|
import { prisma } from "@/lib/db";
|
|
4
4
|
import { resolveDefaultOrganizationId } from "@/lib/client-portals";
|
|
5
5
|
import { getRuntimePortalBySlug, getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
|
|
@@ -29,7 +29,7 @@ export default async function ClientLogin({
|
|
|
29
29
|
const orgName = state?.organization?.name || companyName;
|
|
30
30
|
companyName = orgName;
|
|
31
31
|
companyLogoAlt = orgName;
|
|
32
|
-
portalLabel = state?.organization?.portalLabel
|
|
32
|
+
portalLabel = resolvePortalLabel(orgName, state?.organization?.portalLabel);
|
|
33
33
|
description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
|
|
34
34
|
companyLogoSrc = getBrandLogoUrl({
|
|
35
35
|
logoUrl: state?.organization?.logoUrl,
|
|
@@ -71,7 +71,7 @@ export default async function ClientLogin({
|
|
|
71
71
|
const orgName = organization?.name || companyName;
|
|
72
72
|
companyName = orgName;
|
|
73
73
|
companyLogoAlt = orgName;
|
|
74
|
-
portalLabel = organization?.portalLabel
|
|
74
|
+
portalLabel = resolvePortalLabel(orgName, organization?.portalLabel);
|
|
75
75
|
description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
|
|
76
76
|
companyLogoSrc = getBrandLogoUrl({
|
|
77
77
|
logoUrl: organization?.logoUrl,
|
|
@@ -95,21 +95,8 @@ export default async function ClientLogin({
|
|
|
95
95
|
return (
|
|
96
96
|
<PortalLogin
|
|
97
97
|
companyName={companyName}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<img
|
|
101
|
-
src={companyLogoSrc}
|
|
102
|
-
alt={companyLogoAlt}
|
|
103
|
-
className="h-7 w-7 rounded-lg object-cover"
|
|
104
|
-
/>
|
|
105
|
-
) : (
|
|
106
|
-
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
|
|
107
|
-
<span className="text-xs font-bold text-white">
|
|
108
|
-
{companyName[0]?.toUpperCase() || "S"}
|
|
109
|
-
</span>
|
|
110
|
-
</div>
|
|
111
|
-
)
|
|
112
|
-
}
|
|
98
|
+
companyLogoSrc={companyLogoSrc}
|
|
99
|
+
companyLogoAlt={companyLogoAlt}
|
|
113
100
|
companyUrl={companyUrl}
|
|
114
101
|
supportEmail={supportEmail}
|
|
115
102
|
portalLabel={portalLabel}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, FormEvent
|
|
3
|
+
import { useState, FormEvent } from "react";
|
|
4
4
|
import { ArrowRight, Eye, EyeOff } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
export type PortalLoginProps = {
|
|
7
7
|
companyName: string;
|
|
8
|
-
|
|
8
|
+
companyLogoSrc?: string | null;
|
|
9
|
+
companyLogoAlt?: string;
|
|
9
10
|
companyUrl: string;
|
|
10
11
|
portalLabel?: string;
|
|
11
12
|
description?: string;
|
|
@@ -16,7 +17,8 @@ export type PortalLoginProps = {
|
|
|
16
17
|
|
|
17
18
|
export function PortalLogin({
|
|
18
19
|
companyName,
|
|
19
|
-
|
|
20
|
+
companyLogoSrc,
|
|
21
|
+
companyLogoAlt,
|
|
20
22
|
companyUrl,
|
|
21
23
|
portalLabel,
|
|
22
24
|
description,
|
|
@@ -29,11 +31,13 @@ export function PortalLogin({
|
|
|
29
31
|
const [error, setError] = useState<{ message: string; hint?: string } | null>(null);
|
|
30
32
|
const [loading, setLoading] = useState(false);
|
|
31
33
|
const [showPassword, setShowPassword] = useState(false);
|
|
34
|
+
const [logoFailed, setLogoFailed] = useState(false);
|
|
32
35
|
|
|
33
36
|
const resolvedPortalLabel = portalLabel ?? "Client Portal";
|
|
34
37
|
const resolvedDescription = description ?? `Private portal for ${companyName} clients. Sign in with the credentials we sent you.`;
|
|
35
38
|
const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
|
|
36
39
|
const resolvedRedirectBasePath = redirectBasePath ?? "/client";
|
|
40
|
+
const resolvedCompanyLogoAlt = companyLogoAlt ?? companyName;
|
|
37
41
|
|
|
38
42
|
let displayDomain: string;
|
|
39
43
|
try {
|
|
@@ -86,7 +90,20 @@ export function PortalLogin({
|
|
|
86
90
|
|
|
87
91
|
<div className="relative z-10 w-full max-w-sm">
|
|
88
92
|
<a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
|
|
89
|
-
{
|
|
93
|
+
{companyLogoSrc && !logoFailed ? (
|
|
94
|
+
<img
|
|
95
|
+
src={companyLogoSrc}
|
|
96
|
+
alt={resolvedCompanyLogoAlt}
|
|
97
|
+
className="h-7 w-7 rounded-lg object-cover"
|
|
98
|
+
onError={() => setLogoFailed(true)}
|
|
99
|
+
/>
|
|
100
|
+
) : (
|
|
101
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
|
|
102
|
+
<span className="text-xs font-bold text-white">
|
|
103
|
+
{companyName[0]?.toUpperCase() || "S"}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
90
107
|
<span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
|
|
91
108
|
</a>
|
|
92
109
|
|
|
@@ -41,8 +41,13 @@ export function getDomainFromWebsite(value?: string | null): string | null {
|
|
|
41
41
|
*/
|
|
42
42
|
export function getLogoUrl(domain: string, fallbackName?: string): string {
|
|
43
43
|
if (domain) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const params = new URLSearchParams({
|
|
45
|
+
size: "128",
|
|
46
|
+
format: "webp",
|
|
47
|
+
fallback: "monogram",
|
|
48
|
+
retina: "true",
|
|
49
|
+
});
|
|
50
|
+
return `https://img.logo.dev/${domain}?${params.toString()}`;
|
|
46
51
|
}
|
|
47
52
|
// Fallback: initial-based avatar via ui-avatars.com
|
|
48
53
|
const initial = (fallbackName || "?")[0].toUpperCase();
|
|
@@ -66,6 +71,17 @@ export function getBrandLogoUrl(options: {
|
|
|
66
71
|
return getLogoUrl(domain ?? "", options.fallbackName);
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
export function resolvePortalLabel(
|
|
75
|
+
organizationName: string,
|
|
76
|
+
portalLabel?: string | null,
|
|
77
|
+
): string {
|
|
78
|
+
if (portalLabel && portalLabel.trim() && portalLabel !== "Client Portal") {
|
|
79
|
+
return portalLabel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `${organizationName} Portal`;
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
/**
|
|
70
86
|
* Fetch a Gravatar URL for an email address.
|
|
71
87
|
* Falls back to UI Avatars if no Gravatar exists.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.4.
|
|
1
|
+
0.4.28
|
|
@@ -13,6 +13,21 @@ function fail(message: string): never {
|
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function shouldRetryWithoutWebsiteUrl(message: string): boolean {
|
|
17
|
+
const normalized = message.toLowerCase();
|
|
18
|
+
return (
|
|
19
|
+
normalized.includes("websiteurl") &&
|
|
20
|
+
(
|
|
21
|
+
normalized.includes("unknown arg") ||
|
|
22
|
+
normalized.includes("unknown argument") ||
|
|
23
|
+
normalized.includes("unknown field") ||
|
|
24
|
+
normalized.includes("no column named") ||
|
|
25
|
+
normalized.includes("has no column") ||
|
|
26
|
+
normalized.includes("does not exist")
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
async function main() {
|
|
17
32
|
const args = process.argv.slice(2);
|
|
18
33
|
|
|
@@ -74,11 +89,7 @@ async function main() {
|
|
|
74
89
|
});
|
|
75
90
|
} catch (error) {
|
|
76
91
|
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
-
if (
|
|
78
|
-
websiteUrl &&
|
|
79
|
-
(message.includes("Unknown arg `websiteUrl`") ||
|
|
80
|
-
message.includes("Unknown argument `websiteUrl`"))
|
|
81
|
-
) {
|
|
92
|
+
if (websiteUrl && shouldRetryWithoutWebsiteUrl(message)) {
|
|
82
93
|
delete createData.websiteUrl;
|
|
83
94
|
portal = await prisma.clientPortal.create({
|
|
84
95
|
data: createData,
|
|
@@ -45,21 +45,57 @@ async function main() {
|
|
|
45
45
|
fail("No organization found");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
let portals: Array<{
|
|
49
|
+
slug: string;
|
|
50
|
+
companyName: string;
|
|
51
|
+
websiteUrl?: string | null;
|
|
52
|
+
logoUrl?: string | null;
|
|
53
|
+
username: string;
|
|
54
|
+
passwordHash: string;
|
|
55
|
+
credentialVersion: string;
|
|
56
|
+
isActive: boolean;
|
|
57
|
+
lastUpdated?: string | null;
|
|
58
|
+
}> = [];
|
|
59
|
+
try {
|
|
60
|
+
portals = await prisma.clientPortal.findMany({
|
|
61
|
+
where: { organizationId: organization.id },
|
|
62
|
+
orderBy: { createdAt: "asc" },
|
|
63
|
+
select: {
|
|
64
|
+
slug: true,
|
|
65
|
+
companyName: true,
|
|
66
|
+
websiteUrl: true,
|
|
67
|
+
logoUrl: true,
|
|
68
|
+
username: true,
|
|
69
|
+
passwordHash: true,
|
|
70
|
+
credentialVersion: true,
|
|
71
|
+
isActive: true,
|
|
72
|
+
lastUpdated: true,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
if (!message.toLowerCase().includes("websiteurl")) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
const legacyPortals = await prisma.clientPortal.findMany({
|
|
81
|
+
where: { organizationId: organization.id },
|
|
82
|
+
orderBy: { createdAt: "asc" },
|
|
83
|
+
select: {
|
|
84
|
+
slug: true,
|
|
85
|
+
companyName: true,
|
|
86
|
+
logoUrl: true,
|
|
87
|
+
username: true,
|
|
88
|
+
passwordHash: true,
|
|
89
|
+
credentialVersion: true,
|
|
90
|
+
isActive: true,
|
|
91
|
+
lastUpdated: true,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
portals = legacyPortals.map((portal) => ({
|
|
95
|
+
...portal,
|
|
96
|
+
websiteUrl: null,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
63
99
|
|
|
64
100
|
console.log(JSON.stringify({
|
|
65
101
|
ok: true,
|