@payez/next-mvp 3.2.3 → 3.4.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/dist/api-handlers/admin/analytics.js +3 -3
- package/dist/api-handlers/admin/audit.js +3 -3
- package/dist/api-handlers/admin/sessions.js +3 -3
- package/dist/api-handlers/admin/users.js +3 -3
- package/dist/api-handlers/admin/vibe-data.js +3 -3
- package/dist/components/account/UserAvatarMenu.js +6 -2
- package/dist/lib/idp-client-config.d.ts +1 -1
- package/dist/lib/idp-client-config.js +2 -2
- package/dist/lib/nextauth-secret.js +0 -3
- package/dist/lib/session-store.d.ts +4 -0
- package/dist/lib/session-store.js +8 -0
- package/dist/pages/coming-soon/page.d.ts +8 -0
- package/dist/pages/coming-soon/page.js +28 -0
- package/package.json +6 -1
- package/src/api-handlers/admin/analytics.ts +3 -3
- package/src/api-handlers/admin/audit.ts +3 -3
- package/src/api-handlers/admin/sessions.ts +3 -3
- package/src/api-handlers/admin/users.ts +3 -3
- package/src/api-handlers/admin/vibe-data.ts +3 -3
- package/src/components/account/UserAvatarMenu.tsx +21 -5
- package/src/lib/idp-client-config.ts +3 -3
- package/src/lib/nextauth-secret.ts +0 -4
- package/src/lib/session-store.ts +8 -0
- package/src/pages/coming-soon/page.tsx +83 -0
|
@@ -81,9 +81,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
81
81
|
.update(stringToSign)
|
|
82
82
|
.digest('base64');
|
|
83
83
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
84
|
-
// Get the
|
|
84
|
+
// Get the client slug from startup config for multi-client admin support
|
|
85
85
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
86
|
-
const
|
|
86
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
87
87
|
try {
|
|
88
88
|
const res = await fetch(proxyUrl, {
|
|
89
89
|
method: 'POST',
|
|
@@ -92,7 +92,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
92
92
|
'X-Vibe-Client-Id': clientId,
|
|
93
93
|
'X-Vibe-Timestamp': String(timestamp),
|
|
94
94
|
'X-Vibe-Signature': signature,
|
|
95
|
-
...(
|
|
95
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
96
96
|
},
|
|
97
97
|
body: JSON.stringify({
|
|
98
98
|
endpoint,
|
|
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
80
80
|
.update(stringToSign)
|
|
81
81
|
.digest('base64');
|
|
82
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
83
|
-
// Get the
|
|
83
|
+
// Get the client slug from startup config for multi-client admin support
|
|
84
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const
|
|
85
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
88
88
|
method: 'POST',
|
|
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
91
91
|
'X-Vibe-Client-Id': clientId,
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
|
-
...(
|
|
94
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
95
95
|
},
|
|
96
96
|
body: JSON.stringify({
|
|
97
97
|
endpoint,
|
|
@@ -87,9 +87,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
87
87
|
.update(stringToSign)
|
|
88
88
|
.digest('base64');
|
|
89
89
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
90
|
-
// Get the
|
|
90
|
+
// Get the client slug from startup config for multi-client admin support
|
|
91
91
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
92
|
-
const
|
|
92
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
93
93
|
try {
|
|
94
94
|
const res = await fetch(proxyUrl, {
|
|
95
95
|
method: 'POST',
|
|
@@ -98,7 +98,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
98
98
|
'X-Vibe-Client-Id': clientId,
|
|
99
99
|
'X-Vibe-Timestamp': String(timestamp),
|
|
100
100
|
'X-Vibe-Signature': signature,
|
|
101
|
-
...(
|
|
101
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
102
102
|
},
|
|
103
103
|
body: JSON.stringify({
|
|
104
104
|
endpoint,
|
|
@@ -80,9 +80,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
80
80
|
.update(stringToSign)
|
|
81
81
|
.digest('base64');
|
|
82
82
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
83
|
-
// Get the
|
|
83
|
+
// Get the client slug from startup config for multi-client admin support
|
|
84
84
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
85
|
-
const
|
|
85
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
88
88
|
method: 'POST',
|
|
@@ -91,7 +91,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
91
91
|
'X-Vibe-Client-Id': clientId,
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
|
-
...(
|
|
94
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
95
95
|
},
|
|
96
96
|
body: JSON.stringify({
|
|
97
97
|
endpoint,
|
|
@@ -95,9 +95,9 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
95
95
|
.update(stringToSign)
|
|
96
96
|
.digest('base64');
|
|
97
97
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
98
|
-
// Get the
|
|
98
|
+
// Get the client slug from startup config for multi-client admin support
|
|
99
99
|
const idpConfig = (0, startup_init_1.getStartupIDPConfig)();
|
|
100
|
-
const
|
|
100
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
101
101
|
try {
|
|
102
102
|
const res = await fetch(proxyUrl, {
|
|
103
103
|
method: 'POST',
|
|
@@ -107,7 +107,7 @@ async function vibeServiceRequest(endpoint, options) {
|
|
|
107
107
|
'X-Vibe-Timestamp': String(timestamp),
|
|
108
108
|
'X-Vibe-Signature': signature,
|
|
109
109
|
// For multi-client admins: specify which client context to use
|
|
110
|
-
...(
|
|
110
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
111
111
|
},
|
|
112
112
|
body: JSON.stringify({
|
|
113
113
|
endpoint,
|
|
@@ -44,7 +44,11 @@ function UserAvatarMenu({ basePath = '', showProfile = true, showSettings = true
|
|
|
44
44
|
if (!session?.user) {
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// Derive display initial from name or email — ignore anon/internal IDs
|
|
48
|
+
const userName = session.user?.name;
|
|
49
|
+
const userEmail = session.user.email;
|
|
50
|
+
const displaySource = userName || userEmail;
|
|
51
|
+
const userInitial = displaySource?.charAt(0).toUpperCase() || '?';
|
|
48
52
|
const handleNavigation = (path) => {
|
|
49
53
|
setIsOpen(false);
|
|
50
54
|
router.push(path);
|
|
@@ -69,7 +73,7 @@ function UserAvatarMenu({ basePath = '', showProfile = true, showSettings = true
|
|
|
69
73
|
router.push(item.href);
|
|
70
74
|
}
|
|
71
75
|
};
|
|
72
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "relative", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setIsOpen(!isOpen), className: "flex items-center justify-center h-10 w-10 rounded-full bg-[#349AD5] text-white font-semibold text-lg hover:bg-[#2980b9] transition-colors focus:outline-none focus:ring-2 focus:ring-[#349AD5] focus:ring-offset-2 dark:focus:ring-offset-slate-900", "aria-label": "User menu", "aria-expanded": isOpen, "aria-haspopup": "true", children: userInitial }), isOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute right-0 mt-2 w-56 rounded-md shadow-lg z-50\r\n bg-white dark:bg-slate-900\r\n border border-gray-200 dark:border-slate-700", role: "menu", "aria-orientation": "vertical", children: [(0, jsx_runtime_1.
|
|
76
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "relative", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setIsOpen(!isOpen), className: "flex items-center justify-center h-10 w-10 rounded-full bg-[#349AD5] text-white font-semibold text-lg hover:bg-[#2980b9] transition-colors focus:outline-none focus:ring-2 focus:ring-[#349AD5] focus:ring-offset-2 dark:focus:ring-offset-slate-900", "aria-label": "User menu", "aria-expanded": isOpen, "aria-haspopup": "true", children: userInitial }), isOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute right-0 mt-2 w-56 rounded-md shadow-lg z-50\r\n bg-white dark:bg-slate-900\r\n border border-gray-200 dark:border-slate-700", role: "menu", "aria-orientation": "vertical", children: [(0, jsx_runtime_1.jsxs)("div", { className: "px-4 py-3 border-b border-gray-200 dark:border-slate-700", children: [userName && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-700 dark:text-slate-200 truncate", children: userName })), userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400 truncate", children: userEmail })), !userName && !userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400", children: "Signed in" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "py-1", children: [showProfile && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.User, { className: "h-4 w-4" }), label: "Profile", onClick: () => handleNavigation(`${basePath}/profile`) })), showSettings && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Settings, { className: "h-4 w-4" }), label: "Settings", onClick: () => handleNavigation(`${basePath}/settings`) })), showSecurity && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Shield, { className: "h-4 w-4" }), label: "Security", onClick: () => handleNavigation(`${basePath}/security`) })), customItems?.map((item, index) => ((0, jsx_runtime_1.jsx)(MenuItem, { icon: item.icon, label: item.label, onClick: () => handleItemClick(item) }, index)))] }), (0, jsx_runtime_1.jsx)("div", { className: "border-t border-gray-200 dark:border-slate-700 py-1", children: (0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "h-4 w-4" }), label: "Sign Out", onClick: handleSignOut, variant: "danger" }) })] }))] }));
|
|
73
77
|
}
|
|
74
78
|
function MenuItem({ icon, label, onClick, variant = 'default' }) {
|
|
75
79
|
const baseClasses = "flex items-center w-full px-4 py-2 text-sm cursor-pointer transition-colors";
|
|
@@ -282,7 +282,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
|
282
282
|
}
|
|
283
283
|
// Map response to our interface (IDP always returns snake_case)
|
|
284
284
|
const config = {
|
|
285
|
-
clientId:
|
|
285
|
+
clientId: String(rawClientId),
|
|
286
286
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
287
287
|
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
288
288
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
@@ -330,7 +330,7 @@ async function fetchConfigFromIDP(idpUrl, clientIdStr) {
|
|
|
330
330
|
console.log(`[IDP_CONFIG] Parsed baseClientUrl:`, config.baseClientUrl, `| raw keys:`, Object.keys(configData).filter(k => k.toLowerCase().includes('client')));
|
|
331
331
|
// Validate we got what we need
|
|
332
332
|
if (!config.clientId) {
|
|
333
|
-
throw new Error('[IDP_CONFIG] FATAL: clientId is
|
|
333
|
+
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
334
334
|
}
|
|
335
335
|
if (!config.nextAuthSecret) {
|
|
336
336
|
throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
|
|
@@ -31,9 +31,6 @@ async function resolveNextAuthSecret() {
|
|
|
31
31
|
const clientIdStr = process.env.CLIENT_ID;
|
|
32
32
|
if (!clientIdStr || clientIdStr.trim() === '')
|
|
33
33
|
throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
|
-
// Determine if clientId is numeric or string
|
|
35
|
-
const isNumeric = /^[0-9]+$/.test(clientIdStr);
|
|
36
|
-
const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
|
|
37
34
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
38
35
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
|
39
36
|
// Client ID passed via X-Client-Id header, not query string
|
|
@@ -34,6 +34,10 @@ export declare function createSession(data: SessionData): Promise<string>;
|
|
|
34
34
|
* @returns The session data, or null if not found.
|
|
35
35
|
*/
|
|
36
36
|
export declare function getSession(sessionToken: string): Promise<SessionData | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Refresh session TTL without reading/writing data (sliding window expiry).
|
|
39
|
+
*/
|
|
40
|
+
export declare function touchSession(token: string): Promise<void>;
|
|
37
41
|
/**
|
|
38
42
|
* Retrieves a session along with a version identifier for optimistic locking.
|
|
39
43
|
* @param sessionToken The session token to look up.
|
|
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.generateSessionToken = generateSessionToken;
|
|
16
16
|
exports.createSession = createSession;
|
|
17
17
|
exports.getSession = getSession;
|
|
18
|
+
exports.touchSession = touchSession;
|
|
18
19
|
exports.getSessionWithVersion = getSessionWithVersion;
|
|
19
20
|
exports.isAccessTokenFresh = isAccessTokenFresh;
|
|
20
21
|
exports.deleteSession = deleteSession;
|
|
@@ -95,6 +96,13 @@ async function getSession(sessionToken) {
|
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Refresh session TTL without reading/writing data (sliding window expiry).
|
|
101
|
+
*/
|
|
102
|
+
async function touchSession(token) {
|
|
103
|
+
const key = getSessionKey(token);
|
|
104
|
+
await redis_1.default.expire(key, SESSION_TTL);
|
|
105
|
+
}
|
|
98
106
|
/**
|
|
99
107
|
* Retrieves a session along with a version identifier for optimistic locking.
|
|
100
108
|
* @param sessionToken The session token to look up.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ComingSoonPageProps {
|
|
3
|
+
homeUrl?: string;
|
|
4
|
+
/** Override logo — pass a React node (e.g. inline SVG or themed <img>) */
|
|
5
|
+
logo?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export default function ComingSoonPage(props: ComingSoonPageProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
'use client';
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.default = ComingSoonPage;
|
|
8
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
|
+
const react_1 = require("react");
|
|
10
|
+
const link_1 = __importDefault(require("next/link"));
|
|
11
|
+
const useTheme_1 = require("../../theme/useTheme");
|
|
12
|
+
function ComingSoonContent({ homeUrl = '/', logo }) {
|
|
13
|
+
const branding = (0, useTheme_1.useBranding)();
|
|
14
|
+
const colors = (0, useTheme_1.useColors)();
|
|
15
|
+
const fallbackLogo = branding.logo?.dark || branding.logo?.light;
|
|
16
|
+
const logoAlt = branding.logo?.alt || branding.appName || 'App Logo';
|
|
17
|
+
const logoHeight = branding.logo?.height || 48;
|
|
18
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full text-center rounded-xl p-8 shadow-lg border", style: {
|
|
19
|
+
backgroundColor: 'var(--bg-card, #ffffff)',
|
|
20
|
+
borderColor: 'var(--border-default, #e5e7eb)',
|
|
21
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-6 flex justify-center", children: logo || (fallbackLogo && ((0, jsx_runtime_1.jsx)("img", { src: fallbackLogo, alt: logoAlt, style: { height: logoHeight } }))) }), (0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold mb-2", style: { color: 'var(--text-primary, #111827)' }, children: branding.appName || 'Our App' }), (0, jsx_runtime_1.jsx)("span", { className: "inline-flex items-center px-3 py-1 text-xs font-medium rounded-full lowercase tracking-wide border mb-4", style: {
|
|
22
|
+
borderColor: colors.primary || '#3b82f6',
|
|
23
|
+
color: colors.primary || '#3b82f6',
|
|
24
|
+
}, children: "coming soon" }), (0, jsx_runtime_1.jsx)("p", { className: "mb-6", style: { color: 'var(--text-secondary, #6b7280)' }, children: "We're currently in beta and access is limited to approved users. Check back soon \u2014 we're working hard to open the doors!" }), (0, jsx_runtime_1.jsx)(link_1.default, { href: homeUrl, className: "inline-block w-full font-medium py-3 px-4 rounded-lg transition-colors text-white", style: { backgroundColor: colors.primary || '#3b82f6' }, children: "Go to Home" })] }) }));
|
|
25
|
+
}
|
|
26
|
+
function ComingSoonPage(props) {
|
|
27
|
+
return ((0, jsx_runtime_1.jsx)(react_1.Suspense, { children: (0, jsx_runtime_1.jsx)(ComingSoonContent, { ...props }) }));
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -816,6 +816,11 @@
|
|
|
816
816
|
"types": "./dist/pages/client-admin/index.d.ts",
|
|
817
817
|
"require": "./dist/pages/client-admin/index.js",
|
|
818
818
|
"default": "./dist/pages/client-admin/index.js"
|
|
819
|
+
},
|
|
820
|
+
"./pages/coming-soon": {
|
|
821
|
+
"types": "./dist/pages/coming-soon/page.d.ts",
|
|
822
|
+
"require": "./dist/pages/coming-soon/page.js",
|
|
823
|
+
"default": "./dist/pages/coming-soon/page.js"
|
|
819
824
|
}
|
|
820
825
|
},
|
|
821
826
|
"peerDependencies": {
|
|
@@ -65,9 +65,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
65
65
|
|
|
66
66
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
67
67
|
|
|
68
|
-
// Get the
|
|
68
|
+
// Get the client slug from startup config for multi-client admin support
|
|
69
69
|
const idpConfig = getStartupIDPConfig();
|
|
70
|
-
const
|
|
70
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
73
|
const res = await fetch(proxyUrl, {
|
|
@@ -77,7 +77,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
77
77
|
'X-Vibe-Client-Id': clientId,
|
|
78
78
|
'X-Vibe-Timestamp': String(timestamp),
|
|
79
79
|
'X-Vibe-Signature': signature,
|
|
80
|
-
...(
|
|
80
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
81
81
|
},
|
|
82
82
|
body: JSON.stringify({
|
|
83
83
|
endpoint,
|
|
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
64
64
|
|
|
65
65
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
66
66
|
|
|
67
|
-
// Get the
|
|
67
|
+
// Get the client slug from startup config for multi-client admin support
|
|
68
68
|
const idpConfig = getStartupIDPConfig();
|
|
69
|
-
const
|
|
69
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const res = await fetch(proxyUrl, {
|
|
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
76
76
|
'X-Vibe-Client-Id': clientId,
|
|
77
77
|
'X-Vibe-Timestamp': String(timestamp),
|
|
78
78
|
'X-Vibe-Signature': signature,
|
|
79
|
-
...(
|
|
79
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
80
80
|
},
|
|
81
81
|
body: JSON.stringify({
|
|
82
82
|
endpoint,
|
|
@@ -77,9 +77,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
77
77
|
|
|
78
78
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
79
79
|
|
|
80
|
-
// Get the
|
|
80
|
+
// Get the client slug from startup config for multi-client admin support
|
|
81
81
|
const idpConfig = getStartupIDPConfig();
|
|
82
|
-
const
|
|
82
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
85
|
const res = await fetch(proxyUrl, {
|
|
@@ -89,7 +89,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
89
89
|
'X-Vibe-Client-Id': clientId,
|
|
90
90
|
'X-Vibe-Timestamp': String(timestamp),
|
|
91
91
|
'X-Vibe-Signature': signature,
|
|
92
|
-
...(
|
|
92
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
93
93
|
},
|
|
94
94
|
body: JSON.stringify({
|
|
95
95
|
endpoint,
|
|
@@ -64,9 +64,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
64
64
|
|
|
65
65
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
66
66
|
|
|
67
|
-
// Get the
|
|
67
|
+
// Get the client slug from startup config for multi-client admin support
|
|
68
68
|
const idpConfig = getStartupIDPConfig();
|
|
69
|
-
const
|
|
69
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const res = await fetch(proxyUrl, {
|
|
@@ -76,7 +76,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
76
76
|
'X-Vibe-Client-Id': clientId,
|
|
77
77
|
'X-Vibe-Timestamp': String(timestamp),
|
|
78
78
|
'X-Vibe-Signature': signature,
|
|
79
|
-
...(
|
|
79
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
80
80
|
},
|
|
81
81
|
body: JSON.stringify({
|
|
82
82
|
endpoint,
|
|
@@ -79,9 +79,9 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
79
79
|
|
|
80
80
|
const proxyUrl = `${idpUrl}/api/vibe/proxy`;
|
|
81
81
|
|
|
82
|
-
// Get the
|
|
82
|
+
// Get the client slug from startup config for multi-client admin support
|
|
83
83
|
const idpConfig = getStartupIDPConfig();
|
|
84
|
-
const
|
|
84
|
+
const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
|
|
85
85
|
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetch(proxyUrl, {
|
|
@@ -92,7 +92,7 @@ async function vibeServiceRequest<T = unknown>(
|
|
|
92
92
|
'X-Vibe-Timestamp': String(timestamp),
|
|
93
93
|
'X-Vibe-Signature': signature,
|
|
94
94
|
// For multi-client admins: specify which client context to use
|
|
95
|
-
...(
|
|
95
|
+
...(idpClientId && { 'X-Client-Id': idpClientId }),
|
|
96
96
|
},
|
|
97
97
|
body: JSON.stringify({
|
|
98
98
|
endpoint,
|
|
@@ -83,7 +83,11 @@ export function UserAvatarMenu({
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
// Derive display initial from name or email — ignore anon/internal IDs
|
|
87
|
+
const userName = (session.user as any)?.name;
|
|
88
|
+
const userEmail = session.user.email;
|
|
89
|
+
const displaySource = userName || userEmail;
|
|
90
|
+
const userInitial = displaySource?.charAt(0).toUpperCase() || '?';
|
|
87
91
|
|
|
88
92
|
const handleNavigation = (path: string) => {
|
|
89
93
|
setIsOpen(false);
|
|
@@ -132,11 +136,23 @@ export function UserAvatarMenu({
|
|
|
132
136
|
role="menu"
|
|
133
137
|
aria-orientation="vertical"
|
|
134
138
|
>
|
|
135
|
-
{/* User
|
|
139
|
+
{/* User identity label */}
|
|
136
140
|
<div className="px-4 py-3 border-b border-gray-200 dark:border-slate-700">
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
{userName && (
|
|
142
|
+
<p className="text-sm font-medium text-gray-700 dark:text-slate-200 truncate">
|
|
143
|
+
{userName}
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
{userEmail && (
|
|
147
|
+
<p className="text-sm text-gray-500 dark:text-slate-400 truncate">
|
|
148
|
+
{userEmail}
|
|
149
|
+
</p>
|
|
150
|
+
)}
|
|
151
|
+
{!userName && !userEmail && (
|
|
152
|
+
<p className="text-sm text-gray-500 dark:text-slate-400">
|
|
153
|
+
Signed in
|
|
154
|
+
</p>
|
|
155
|
+
)}
|
|
140
156
|
</div>
|
|
141
157
|
|
|
142
158
|
{/* Menu items */}
|
|
@@ -56,7 +56,7 @@ export interface BrandingConfig {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export interface IDPClientConfig {
|
|
59
|
-
clientId:
|
|
59
|
+
clientId: string;
|
|
60
60
|
clientSlug: string;
|
|
61
61
|
nextAuthSecret: string;
|
|
62
62
|
configCacheTtlSeconds: number;
|
|
@@ -371,7 +371,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
371
371
|
|
|
372
372
|
// Map response to our interface (IDP always returns snake_case)
|
|
373
373
|
const config: IDPClientConfig = {
|
|
374
|
-
clientId:
|
|
374
|
+
clientId: String(rawClientId),
|
|
375
375
|
clientSlug: configData.clientSlug ?? configData.client_slug ?? configData.slug ?? '',
|
|
376
376
|
nextAuthSecret: configData.nextAuthSecret ?? configData.next_auth_secret ?? '',
|
|
377
377
|
configCacheTtlSeconds: configData.configCacheTtlSeconds ?? configData.config_cache_ttl_seconds ?? 300,
|
|
@@ -420,7 +420,7 @@ async function fetchConfigFromIDP(idpUrl: string, clientIdStr: string): Promise<
|
|
|
420
420
|
|
|
421
421
|
// Validate we got what we need
|
|
422
422
|
if (!config.clientId) {
|
|
423
|
-
throw new Error('[IDP_CONFIG] FATAL: clientId is
|
|
423
|
+
throw new Error('[IDP_CONFIG] FATAL: clientId is empty or missing after parsing');
|
|
424
424
|
}
|
|
425
425
|
if (!config.nextAuthSecret) {
|
|
426
426
|
throw new Error('[IDP_CONFIG] FATAL: nextAuthSecret is empty after parsing');
|
|
@@ -32,10 +32,6 @@ export async function resolveNextAuthSecret(): Promise<string> {
|
|
|
32
32
|
const clientIdStr = process.env.CLIENT_ID;
|
|
33
33
|
if (!clientIdStr || clientIdStr.trim() === '') throw new Error('CLIENT_ID is required (e.g., "ideal_resume_website")');
|
|
34
34
|
|
|
35
|
-
// Determine if clientId is numeric or string
|
|
36
|
-
const isNumeric = /^[0-9]+$/.test(clientIdStr);
|
|
37
|
-
const clientId = isNumeric ? parseInt(clientIdStr, 10) : clientIdStr;
|
|
38
|
-
|
|
39
35
|
// Step 1: Request IDP to sign a client assertion (IDP has the keys, not us)
|
|
40
36
|
|
|
41
37
|
const signingUrl = new URL(`${base.replace(/\/$/, '')}/api/ExternalAuth/sign-client-assertion`);
|
package/src/lib/session-store.ts
CHANGED
|
@@ -87,6 +87,14 @@ export async function getSession(sessionToken: string): Promise<SessionData | nu
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Refresh session TTL without reading/writing data (sliding window expiry).
|
|
92
|
+
*/
|
|
93
|
+
export async function touchSession(token: string): Promise<void> {
|
|
94
|
+
const key = getSessionKey(token);
|
|
95
|
+
await redis.expire(key, SESSION_TTL);
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
/**
|
|
91
99
|
* Retrieves a session along with a version identifier for optimistic locking.
|
|
92
100
|
* @param sessionToken The session token to look up.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { Suspense } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { useBranding, useColors } from '../../theme/useTheme';
|
|
6
|
+
|
|
7
|
+
interface ComingSoonPageProps {
|
|
8
|
+
homeUrl?: string;
|
|
9
|
+
/** Override logo — pass a React node (e.g. inline SVG or themed <img>) */
|
|
10
|
+
logo?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ComingSoonContent({ homeUrl = '/', logo }: ComingSoonPageProps) {
|
|
14
|
+
const branding = useBranding();
|
|
15
|
+
const colors = useColors();
|
|
16
|
+
|
|
17
|
+
const fallbackLogo = branding.logo?.dark || branding.logo?.light;
|
|
18
|
+
const logoAlt = branding.logo?.alt || branding.appName || 'App Logo';
|
|
19
|
+
const logoHeight = branding.logo?.height || 48;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
23
|
+
<div
|
|
24
|
+
className="max-w-md w-full text-center rounded-xl p-8 shadow-lg border"
|
|
25
|
+
style={{
|
|
26
|
+
backgroundColor: 'var(--bg-card, #ffffff)',
|
|
27
|
+
borderColor: 'var(--border-default, #e5e7eb)',
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<div className="mb-6 flex justify-center">
|
|
31
|
+
{logo || (fallbackLogo && (
|
|
32
|
+
<img
|
|
33
|
+
src={fallbackLogo}
|
|
34
|
+
alt={logoAlt}
|
|
35
|
+
style={{ height: logoHeight }}
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<h1
|
|
41
|
+
className="text-2xl font-bold mb-2"
|
|
42
|
+
style={{ color: 'var(--text-primary, #111827)' }}
|
|
43
|
+
>
|
|
44
|
+
{branding.appName || 'Our App'}
|
|
45
|
+
</h1>
|
|
46
|
+
|
|
47
|
+
<span
|
|
48
|
+
className="inline-flex items-center px-3 py-1 text-xs font-medium rounded-full lowercase tracking-wide border mb-4"
|
|
49
|
+
style={{
|
|
50
|
+
borderColor: colors.primary || '#3b82f6',
|
|
51
|
+
color: colors.primary || '#3b82f6',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
coming soon
|
|
55
|
+
</span>
|
|
56
|
+
|
|
57
|
+
<p
|
|
58
|
+
className="mb-6"
|
|
59
|
+
style={{ color: 'var(--text-secondary, #6b7280)' }}
|
|
60
|
+
>
|
|
61
|
+
We're currently in beta and access is limited to approved users.
|
|
62
|
+
Check back soon — we're working hard to open the doors!
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<Link
|
|
66
|
+
href={homeUrl}
|
|
67
|
+
className="inline-block w-full font-medium py-3 px-4 rounded-lg transition-colors text-white"
|
|
68
|
+
style={{ backgroundColor: colors.primary || '#3b82f6' }}
|
|
69
|
+
>
|
|
70
|
+
Go to Home
|
|
71
|
+
</Link>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default function ComingSoonPage(props: ComingSoonPageProps) {
|
|
78
|
+
return (
|
|
79
|
+
<Suspense>
|
|
80
|
+
<ComingSoonContent {...props} />
|
|
81
|
+
</Suspense>
|
|
82
|
+
);
|
|
83
|
+
}
|