@vertesia/ui 1.6.0-dev.20261004.085740Z → 1.6.0-dev.20261009.095813Z
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 +24 -0
- package/lib/boot/vite.d.ts +3 -0
- package/lib/boot/vite.d.ts.map +1 -1
- package/lib/boot/vite.js +12 -0
- package/lib/boot/vite.js.map +1 -1
- package/lib/core/components/shadcn/button.d.ts +1 -0
- package/lib/core/components/shadcn/button.d.ts.map +1 -1
- package/lib/core/components/shadcn/button.js +2 -2
- package/lib/core/components/shadcn/button.js.map +1 -1
- package/lib/core/components/shadcn/selectBox.d.ts.map +1 -1
- package/lib/core/components/shadcn/selectBox.js +2 -2
- package/lib/core/components/shadcn/selectBox.js.map +1 -1
- package/lib/core/components/shadcn/tabs.d.ts +2 -1
- package/lib/core/components/shadcn/tabs.d.ts.map +1 -1
- package/lib/core/components/shadcn/tabs.js +6 -4
- package/lib/core/components/shadcn/tabs.js.map +1 -1
- package/lib/env/index.d.ts +9 -1
- package/lib/env/index.d.ts.map +1 -1
- package/lib/env/index.js +1 -1
- package/lib/env/index.js.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.d.ts.map +1 -1
- package/lib/features/agent/chat/AgentRequestInputOverlay.js +16 -10
- package/lib/features/agent/chat/AgentRequestInputOverlay.js.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts +7 -1
- package/lib/features/agent/chat/AskUserWidget.d.ts.map +1 -1
- package/lib/features/agent/chat/AskUserWidget.js +17 -16
- package/lib/features/agent/chat/AskUserWidget.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js +2 -253
- package/lib/features/agent/chat/ModernAgentOutput/AllMessagesMixed.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts +1 -3
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js +2 -8
- package/lib/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts +4 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.d.ts.map +1 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js +248 -0
- package/lib/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.js.map +1 -0
- package/lib/features/media-viewer/ImagePanel.d.ts +1 -1
- package/lib/features/media-viewer/ImagePanel.d.ts.map +1 -1
- package/lib/features/media-viewer/ImagePanel.js +40 -10
- package/lib/features/media-viewer/ImagePanel.js.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
- package/lib/i18n/locales/ar.json +15 -0
- package/lib/i18n/locales/de.json +15 -0
- package/lib/i18n/locales/en.json +15 -0
- package/lib/i18n/locales/es.json +15 -0
- package/lib/i18n/locales/fr.json +15 -0
- package/lib/i18n/locales/it.json +15 -0
- package/lib/i18n/locales/ja.json +15 -0
- package/lib/i18n/locales/ko.json +15 -0
- package/lib/i18n/locales/pt.json +15 -0
- package/lib/i18n/locales/ru.json +15 -0
- package/lib/i18n/locales/tr.json +15 -0
- package/lib/i18n/locales/zh-TW.json +15 -0
- package/lib/i18n/locales/zh.json +15 -0
- package/lib/session/auth/composable.d.ts +1 -5
- package/lib/session/auth/composable.d.ts.map +1 -1
- package/lib/session/auth/composable.js +1 -10
- package/lib/session/auth/composable.js.map +1 -1
- package/lib/session/auth/firebase.d.ts.map +1 -1
- package/lib/session/auth/firebase.js +8 -5
- package/lib/session/auth/firebase.js.map +1 -1
- package/lib/session/auth/oauth.d.ts.map +1 -1
- package/lib/session/auth/oauth.js +43 -17
- package/lib/session/auth/oauth.js.map +1 -1
- package/lib/session/auth/vertesiaHosts.d.ts +0 -40
- package/lib/session/auth/vertesiaHosts.d.ts.map +1 -1
- package/lib/session/auth/vertesiaHosts.js +2 -45
- package/lib/session/auth/vertesiaHosts.js.map +1 -1
- package/lib/session/index.d.ts +1 -1
- package/lib/session/index.d.ts.map +1 -1
- package/lib/session/index.js +1 -1
- package/lib/session/index.js.map +1 -1
- package/lib/session/scopeSelection.d.ts +4 -0
- package/lib/session/scopeSelection.d.ts.map +1 -1
- package/lib/session/scopeSelection.js +11 -0
- package/lib/session/scopeSelection.js.map +1 -1
- package/lib/shell/AuthScreenPreview.d.ts.map +1 -1
- package/lib/shell/AuthScreenPreview.js +1 -0
- package/lib/shell/AuthScreenPreview.js.map +1 -1
- package/lib/shell/login/SignInFlow.d.ts +3 -1
- package/lib/shell/login/SignInFlow.d.ts.map +1 -1
- package/lib/shell/login/SignInFlow.js +13 -4
- package/lib/shell/login/SignInFlow.js.map +1 -1
- package/lib/shell/login/SignInIcons.d.ts.map +1 -1
- package/lib/shell/login/SignInIcons.js +2 -1
- package/lib/shell/login/SignInIcons.js.map +1 -1
- package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
- package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
- package/lib/shell/login/SignInPasswordStep.js +58 -0
- package/lib/shell/login/SignInPasswordStep.js.map +1 -0
- package/lib/shell/login/SignInPrimitives.d.ts +13 -0
- package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
- package/lib/shell/login/SignInPrimitives.js +11 -1
- package/lib/shell/login/SignInPrimitives.js.map +1 -1
- package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
- package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
- package/lib/shell/login/SignInProvidersStep.js +4 -2
- package/lib/shell/login/SignInProvidersStep.js.map +1 -1
- package/lib/shell/login/SignInReturningStep.d.ts +3 -1
- package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
- package/lib/shell/login/SignInReturningStep.js +18 -8
- package/lib/shell/login/SignInReturningStep.js.map +1 -1
- package/lib/shell/login/SignInTenantStep.d.ts +3 -1
- package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
- package/lib/shell/login/SignInTenantStep.js +2 -2
- package/lib/shell/login/SignInTenantStep.js.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
- package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.js +8 -7
- package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
- package/lib/shell/login/SigninScreen.d.ts.map +1 -1
- package/lib/shell/login/SigninScreen.js +9 -3
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +24 -4
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +53 -1
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-boot.js.map +1 -1
- package/lib/vertesia-ui-core.js +42 -42
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-env.js +1 -1
- package/lib/vertesia-ui-env.js.map +1 -1
- package/lib/vertesia-ui-features.js +574 -589
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.1NWiB6fw.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.6Ozlf6Ks.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.AttJpg8U.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BI_xdp49.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C-2EEX6w.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CAmo54FO.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CbBzmHNP.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DZgTzkIg.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DZsITxq5.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Dbf2GHth.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +2 -2
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.lleWnH5n.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.wG_XSkbr.chunk.js.map +1 -0
- package/lib/vertesia-ui-layout.js +15 -15
- package/lib/vertesia-ui-layout.js.map +1 -1
- package/lib/vertesia-ui-rich-text.js +40 -40
- package/lib/vertesia-ui-rich-text.js.map +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +54 -47
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +47 -47
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +20 -20
- package/src/boot/vite.ts +15 -0
- package/src/core/components/shadcn/button.tsx +3 -2
- package/src/core/components/shadcn/selectBox.tsx +12 -4
- package/src/core/components/shadcn/tabs.tsx +9 -3
- package/src/css/index.css +1 -1
- package/src/css/typography.css +111 -88
- package/src/env/index.ts +10 -3
- package/src/features/agent/chat/AgentRequestInputOverlay.tsx +73 -34
- package/src/features/agent/chat/AskUserWidget.tsx +225 -206
- package/src/features/agent/chat/ModernAgentOutput/AllMessagesMixed.tsx +2 -253
- package/src/features/agent/chat/ModernAgentOutput/CollapsibleAgentMarkdown.tsx +2 -8
- package/src/features/agent/chat/ModernAgentOutput/agentMarkdownStyles.ts +248 -0
- package/src/features/media-viewer/ImagePanel.tsx +51 -10
- package/src/features/store/types/IntakePolicyEditor.tsx +2 -1
- package/src/i18n/locales/ar.json +15 -0
- package/src/i18n/locales/de.json +15 -0
- package/src/i18n/locales/en.json +15 -0
- package/src/i18n/locales/es.json +15 -0
- package/src/i18n/locales/fr.json +15 -0
- package/src/i18n/locales/it.json +15 -0
- package/src/i18n/locales/ja.json +15 -0
- package/src/i18n/locales/ko.json +15 -0
- package/src/i18n/locales/pt.json +15 -0
- package/src/i18n/locales/ru.json +15 -0
- package/src/i18n/locales/tr.json +15 -0
- package/src/i18n/locales/zh-TW.json +15 -0
- package/src/i18n/locales/zh.json +15 -0
- package/src/session/auth/composable.ts +1 -11
- package/src/session/auth/firebase.ts +9 -6
- package/src/session/auth/oauth.ts +47 -16
- package/src/session/auth/vertesiaHosts.ts +2 -47
- package/src/session/index.ts +1 -1
- package/src/session/scopeSelection.ts +13 -0
- package/src/shell/AuthScreenPreview.tsx +1 -0
- package/src/shell/login/SignInFlow.tsx +23 -2
- package/src/shell/login/SignInIcons.tsx +2 -1
- package/src/shell/login/SignInPasswordStep.tsx +103 -0
- package/src/shell/login/SignInPrimitives.tsx +53 -1
- package/src/shell/login/SignInProvidersStep.tsx +8 -4
- package/src/shell/login/SignInReturningStep.tsx +20 -8
- package/src/shell/login/SignInTenantStep.tsx +6 -2
- package/src/shell/login/SignInWithProviderButton.tsx +11 -7
- package/src/shell/login/SigninScreen.tsx +8 -3
- package/src/shell/login/signInUtils.ts +68 -8
- package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BCPusS2_.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BHWlHchf.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BkNx8SZu.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C1LasLrO.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D8PQ5yKU.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DUtVV1Bq.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DgA36_k5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DmG-fjjZ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DreWvQIk.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.Pp8Tj48d.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.gais--x5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.gais--x5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.xkqaKpY_.chunk.js.map +0 -1
package/src/i18n/locales/zh.json
CHANGED
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"agent.fullscreen": "全屏",
|
|
140
140
|
"agent.hideActiveWorkstreams": "隐藏活跃工作流",
|
|
141
141
|
"agent.hideDetails": "隐藏详情",
|
|
142
|
+
"agent.hideQuestions": "隐藏问题",
|
|
142
143
|
"agent.hideSidebar": "隐藏侧边栏",
|
|
143
144
|
"agent.hideThinkingDetails": "隐藏思考详情",
|
|
144
145
|
"agent.inProgress": "进行中",
|
|
@@ -181,6 +182,7 @@
|
|
|
181
182
|
"agent.pdfExportFailed": "PDF导出失败",
|
|
182
183
|
"agent.pdfExportReady": "PDF导出已就绪",
|
|
183
184
|
"agent.pending": "待处理",
|
|
185
|
+
"agent.pendingQuestions": "等待您的回复",
|
|
184
186
|
"agent.plan": "计划",
|
|
185
187
|
"agent.planProgress": "计划进度",
|
|
186
188
|
"agent.plansWillAppear": "当代理创建计划时,将在此处显示",
|
|
@@ -225,6 +227,7 @@
|
|
|
225
227
|
"agent.showDetails": "显示详情",
|
|
226
228
|
"agent.showLess": "显示更少",
|
|
227
229
|
"agent.showMore": "显示更多",
|
|
230
|
+
"agent.showQuestions": "显示问题",
|
|
228
231
|
"agent.showSidebar": "显示侧边栏",
|
|
229
232
|
"agent.showSystemArtifacts": "系统文件",
|
|
230
233
|
"agent.staged": "已暂存",
|
|
@@ -246,6 +249,7 @@
|
|
|
246
249
|
"agent.thinking": "思考中...",
|
|
247
250
|
"agent.timeout": "已超时",
|
|
248
251
|
"agent.toggleRightSidebar": "切换右侧边栏",
|
|
252
|
+
"agent.typeDifferentResponse": "或输入其他回复...",
|
|
249
253
|
"agent.typeYourMessage": "输入消息...",
|
|
250
254
|
"agent.typeYourResponse": "输入您的回复...",
|
|
251
255
|
"agent.unableToOpenPrint": "无法打开打印预览",
|
|
@@ -286,6 +290,17 @@
|
|
|
286
290
|
"auth.email.placeholder": "you@company.com",
|
|
287
291
|
"auth.email.title": "欢迎使用 Vertesia",
|
|
288
292
|
"auth.error": "错误:{{message}}",
|
|
293
|
+
"auth.password.body": "请输入此账户的密码。",
|
|
294
|
+
"auth.password.continue": "使用密码继续",
|
|
295
|
+
"auth.password.eyebrow": "密码登录",
|
|
296
|
+
"auth.password.failed": "无法登录,请重试。",
|
|
297
|
+
"auth.password.invalidCredentials": "电子邮件或密码不正确。",
|
|
298
|
+
"auth.password.label": "密码",
|
|
299
|
+
"auth.password.notAvailable": "此电子邮件不支持密码登录。",
|
|
300
|
+
"auth.password.required": "请输入密码。",
|
|
301
|
+
"auth.password.submit": "登录",
|
|
302
|
+
"auth.password.title": "输入密码",
|
|
303
|
+
"auth.password.tooManyAttempts": "尝试次数过多。请等待几分钟后重试。",
|
|
289
304
|
"auth.pending.authenticating": "正在认证",
|
|
290
305
|
"auth.pending.body": "请稍候 — 我们正在为您安全跳转。只需稍候片刻。",
|
|
291
306
|
"auth.pending.cancel": "取消",
|
|
@@ -116,17 +116,7 @@ function identityFromAcceptedToken(token: string): AuthenticatedIdentity | undef
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export
|
|
120
|
-
// A URL selection owns the whole pair: never attach an unrelated configured account/project.
|
|
121
|
-
const hasUrlScope = currentUrl.searchParams.has('a') || currentUrl.searchParams.has('p');
|
|
122
|
-
const defaults = hasUrlScope ? undefined : Env.defaultAuthSelection;
|
|
123
|
-
const urlAccount = currentUrl.searchParams.get('a') ?? defaults?.accountId;
|
|
124
|
-
const urlProject = currentUrl.searchParams.get('p') ?? defaults?.projectId;
|
|
125
|
-
// A project alone identifies its account, so the stored selection only fills in a missing project.
|
|
126
|
-
const stored = urlProject === undefined ? readScopeSelection(urlAccount) : undefined;
|
|
127
|
-
|
|
128
|
-
return { accountId: urlAccount ?? stored?.accountId, projectId: urlProject ?? stored?.projectId };
|
|
129
|
-
}
|
|
119
|
+
export { resolveAuthSelection } from '../scopeSelection';
|
|
130
120
|
|
|
131
121
|
function decodeToken(token: string): AuthTokenPayload {
|
|
132
122
|
return jwtDecode(token) as AuthTokenPayload;
|
|
@@ -92,10 +92,9 @@ export async function setFirebaseTenant(tenantEmail?: string) {
|
|
|
92
92
|
// Handle HTTP error responses
|
|
93
93
|
if (!response.ok) {
|
|
94
94
|
// If the error is 404 Not Found, no need to retry
|
|
95
|
+
// How older servers answered "this address has no SSO tenant": an ordinary outcome.
|
|
95
96
|
if (response.status === 404) {
|
|
96
|
-
Env.logger.
|
|
97
|
-
vertesia: { status: response.status },
|
|
98
|
-
});
|
|
97
|
+
Env.logger.debug('No Firebase tenant for this address; using the default tenant');
|
|
99
98
|
return;
|
|
100
99
|
}
|
|
101
100
|
|
|
@@ -103,10 +102,14 @@ export async function setFirebaseTenant(tenantEmail?: string) {
|
|
|
103
102
|
throw new Error(errorData?.error ?? `HTTP error ${response.status}`);
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
// Successfully got a response, parse it
|
|
107
|
-
const data = (await response.json()) as UIResolvedTenant;
|
|
105
|
+
// Successfully got a response, parse it. `null` means the address has no SSO tenant.
|
|
106
|
+
const data = (await response.json()) as UIResolvedTenant | null;
|
|
107
|
+
if (data === null) {
|
|
108
|
+
Env.logger.debug('No Firebase tenant for this address; using the default tenant');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
108
111
|
|
|
109
|
-
if (data
|
|
112
|
+
if (data.firebaseTenantId) {
|
|
110
113
|
const auth = getFirebaseAuth();
|
|
111
114
|
auth.tenantId = data.firebaseTenantId;
|
|
112
115
|
Env.firebase.providerType = data.provider ?? 'oidc';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AuthTokenPayload, REQUESTED_SCOPE_UNAVAILABLE_ERROR_CODE } from '@vertesia/common';
|
|
2
2
|
import { Env } from '@vertesia/ui/env';
|
|
3
3
|
import { jwtDecode } from 'jwt-decode';
|
|
4
|
+
import { forgetRejectedScopeSelection, resolveAuthSelection, type ScopeSelection } from '../scopeSelection';
|
|
4
5
|
import { markCentralAuthRoundTripStarted } from './authRoundTrip';
|
|
5
6
|
import { verifyAuthState } from './authState';
|
|
6
7
|
import { usesGatewaySession } from './gateway';
|
|
@@ -61,6 +62,8 @@ interface Transaction {
|
|
|
61
62
|
redirectUri: string;
|
|
62
63
|
issuer: string;
|
|
63
64
|
target: string;
|
|
65
|
+
rememberedScope?: ScopeSelection;
|
|
66
|
+
retryScope?: ScopeSelection;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
export function clearAppOAuth(): void {
|
|
@@ -86,8 +89,9 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
86
89
|
}
|
|
87
90
|
const redirectUri = redirect.toString();
|
|
88
91
|
if (new URL(redirectUri).origin !== window.location.origin) throw new Error('OAuth callback must be same-origin');
|
|
89
|
-
const issuer = Env.endpoints.sts.replace(/\/+$/, '');
|
|
90
|
-
|
|
92
|
+
const issuer = (config.issuer ? httpsUrl(config.issuer).toString() : Env.endpoints.sts).replace(/\/+$/, '');
|
|
93
|
+
let current = new URL(window.location.href);
|
|
94
|
+
let { accountId, projectId } = resolveAuthSelection(current);
|
|
91
95
|
const callback = current.origin + current.pathname === new URL(redirectUri).origin + new URL(redirectUri).pathname;
|
|
92
96
|
const state = callback ? current.searchParams.get('state') : null;
|
|
93
97
|
const code = callback ? current.searchParams.get('code') : null;
|
|
@@ -109,7 +113,19 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
109
113
|
) {
|
|
110
114
|
throw new Error('Invalid or expired OAuth login state');
|
|
111
115
|
}
|
|
112
|
-
if (error
|
|
116
|
+
if (error === REQUESTED_SCOPE_UNAVAILABLE_ERROR_CODE && transaction.rememberedScope?.accountId) {
|
|
117
|
+
const target = new URL(transaction.target);
|
|
118
|
+
if (target.origin !== window.location.origin) throw new Error('Invalid OAuth return target');
|
|
119
|
+
forgetRejectedScopeSelection(transaction.rememberedScope.accountId, transaction.rememberedScope.projectId);
|
|
120
|
+
clearAppOAuth();
|
|
121
|
+
current = target;
|
|
122
|
+
window.history.replaceState(window.history.state, '', current);
|
|
123
|
+
accountId = transaction.retryScope?.accountId;
|
|
124
|
+
projectId = undefined;
|
|
125
|
+
transaction = undefined;
|
|
126
|
+
} else if (error || !code) {
|
|
127
|
+
throw new Error(`Application sign-in was not completed (${error || 'missing code'})`);
|
|
128
|
+
}
|
|
113
129
|
} else {
|
|
114
130
|
const raw = sessionStorage.getItem(TOKEN_KEY);
|
|
115
131
|
if (raw) {
|
|
@@ -124,8 +140,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
124
140
|
config.offlineAccess &&
|
|
125
141
|
cached.clientId === clientId &&
|
|
126
142
|
cached.issuer === issuer &&
|
|
127
|
-
(!
|
|
128
|
-
(!
|
|
143
|
+
(!projectId || claims.project?.id === projectId) &&
|
|
144
|
+
(!accountId || claims.account?.id === accountId)
|
|
129
145
|
)
|
|
130
146
|
refreshCredential =
|
|
131
147
|
refreshSession?.clientId === clientId && refreshSession.issuer === issuer
|
|
@@ -138,14 +154,19 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
138
154
|
cached.issuer === issuer &&
|
|
139
155
|
!forceRefresh &&
|
|
140
156
|
claims.exp > Date.now() / 1000 + (config.offlineAccess ? 300 : 30) &&
|
|
141
|
-
(!
|
|
142
|
-
(!
|
|
157
|
+
(!projectId || claims.project?.id === projectId) &&
|
|
158
|
+
(!accountId || claims.account?.id === accountId)
|
|
143
159
|
) {
|
|
144
160
|
return cached.token;
|
|
145
161
|
}
|
|
146
162
|
}
|
|
147
163
|
}
|
|
148
|
-
const
|
|
164
|
+
const issuerUrl = new URL(issuer);
|
|
165
|
+
const discoveryUrl = new URL(
|
|
166
|
+
`/.well-known/oauth-authorization-server${issuerUrl.pathname.replace(/\/$/, '')}`,
|
|
167
|
+
issuerUrl.origin,
|
|
168
|
+
);
|
|
169
|
+
const discovery = await fetch(discoveryUrl);
|
|
149
170
|
if (!discovery.ok) throw new Error(`OAuth discovery failed (${discovery.status})`);
|
|
150
171
|
const metadata: {
|
|
151
172
|
issuer: string;
|
|
@@ -158,7 +179,7 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
158
179
|
const tokenEndpoint = httpsUrl(metadata.token_endpoint);
|
|
159
180
|
const revocationEndpoint =
|
|
160
181
|
metadata.revocation_endpoint === undefined ? undefined : httpsUrl(metadata.revocation_endpoint).toString();
|
|
161
|
-
const resource = new URL(metadata.issuer).toString();
|
|
182
|
+
const resource = config.resource ? httpsUrl(config.resource).toString() : new URL(metadata.issuer).toString();
|
|
162
183
|
if (refreshCredential) {
|
|
163
184
|
try {
|
|
164
185
|
const response = await fetch(tokenEndpoint, {
|
|
@@ -184,8 +205,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
184
205
|
claims.iss.replace(/\/+$/, '') !== issuer ||
|
|
185
206
|
claims.client_id !== clientId ||
|
|
186
207
|
claims.exp <= Date.now() / 1000 ||
|
|
187
|
-
(!!
|
|
188
|
-
(!!
|
|
208
|
+
(!!accountId && claims.account?.id !== accountId) ||
|
|
209
|
+
(!!projectId && claims.project?.id !== projectId)
|
|
189
210
|
)
|
|
190
211
|
throw new OAuthLoginError('Invalid OAuth refresh identity');
|
|
191
212
|
sessionStorage.setItem(
|
|
@@ -272,6 +293,13 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
272
293
|
throw new Error('Application CIMD does not allow this callback');
|
|
273
294
|
if (!config.scopes && document.scope) scopes = document.scope.split(/\s+/);
|
|
274
295
|
}
|
|
296
|
+
const explicitScope =
|
|
297
|
+
current.searchParams.has('a') || current.searchParams.has('p')
|
|
298
|
+
? {
|
|
299
|
+
accountId: current.searchParams.get('a') ?? undefined,
|
|
300
|
+
projectId: current.searchParams.get('p') ?? undefined,
|
|
301
|
+
}
|
|
302
|
+
: Env.defaultAuthSelection;
|
|
275
303
|
const verifier = randomValue();
|
|
276
304
|
const next: Transaction = {
|
|
277
305
|
state: randomValue(),
|
|
@@ -280,6 +308,11 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
280
308
|
redirectUri,
|
|
281
309
|
issuer,
|
|
282
310
|
target: current.toString(),
|
|
311
|
+
rememberedScope:
|
|
312
|
+
accountId && explicitScope?.projectId === undefined && (!explicitScope?.accountId || projectId)
|
|
313
|
+
? { accountId, projectId }
|
|
314
|
+
: undefined,
|
|
315
|
+
retryScope: explicitScope?.accountId ? { accountId: explicitScope.accountId } : undefined,
|
|
283
316
|
created: Date.now(),
|
|
284
317
|
};
|
|
285
318
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
@@ -297,10 +330,8 @@ async function acquireToken(forceRefresh = false): Promise<string> {
|
|
|
297
330
|
: scopes.filter((s) => s !== 'offline_access')
|
|
298
331
|
).join(' '),
|
|
299
332
|
);
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
const project = current.searchParams.get('p');
|
|
303
|
-
if (project) authorize.searchParams.set('project_id', project);
|
|
333
|
+
if (accountId) authorize.searchParams.set('account_id', accountId);
|
|
334
|
+
if (projectId) authorize.searchParams.set('project_id', projectId);
|
|
304
335
|
sessionStorage.setItem(TRANSACTION_KEY, JSON.stringify(next));
|
|
305
336
|
markCentralAuthRoundTripStarted();
|
|
306
337
|
window.location.replace(authorize.toString());
|
|
@@ -1,54 +1,9 @@
|
|
|
1
1
|
const LOOPBACK_IPV4 = /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
|
|
2
2
|
/** A single DNS label: alphanumeric, inner hyphens allowed. */
|
|
3
|
-
|
|
3
|
+
const LABEL = '[a-z0-9](?:[a-z0-9-]*[a-z0-9])?';
|
|
4
4
|
|
|
5
5
|
/** The region label is optional throughout, because each regional host has a global US alias. */
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Hostnames served by the UI backend — Vertesia's own code, not merely a Vertesia-owned name:
|
|
10
|
-
* `cloud.vertesia.io`, `cloud.us1.vertesia.io` — the Cloud UI
|
|
11
|
-
* `preprod.cloud.vertesia.io`, `preview.cloud.us1.vertesia.io` — its preview/preprod tiers
|
|
12
|
-
* `acme.cloud.us1.vertesia.io` — per-IdP-tenant branded hosts
|
|
13
|
-
* `dev-feat-x.ui.dev1.vertesia.io` — dynamic per-branch dev envs
|
|
14
|
-
*
|
|
15
|
-
* Mirrors the load-balancer host rules (region-lb/main.tf: `cloud.${var.domain}`,
|
|
16
|
-
* `*.cloud.${var.domain}`, `*.ui.${var.domain}`) rather than the DNS zone, so a name we merely own
|
|
17
|
-
* is not trusted for what serves it.
|
|
18
|
-
*/
|
|
19
|
-
export const FIRST_PARTY_HOST_PATTERNS: readonly RegExp[] = [
|
|
20
|
-
new RegExp(`^cloud\\.${REGION}vertesia\\.io$`),
|
|
21
|
-
new RegExp(`^${LABEL}\\.cloud\\.${REGION}vertesia\\.io$`),
|
|
22
|
-
new RegExp(`^${LABEL}\\.ui\\.${LABEL}\\.vertesia\\.io$`),
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Public app-gateway origins. Stable tiers use the `apps*` browser-facing host, while development
|
|
27
|
-
* environments reach the gateway through its `*.api.*` Cloud Run route:
|
|
28
|
-
* `apps.us1.vertesia.io`, `apps-preview.us1.vertesia.io`
|
|
29
|
-
* `app-gateway-dev1.api.dev1.vertesia.io`, `app-gateway-us3.api.us3.vertesia.io`
|
|
30
|
-
*
|
|
31
|
-
* The second label is deliberately unconstrained. Branch environments do NOT each get a gateway —
|
|
32
|
-
* they share their region's (`detectGatewayUrl` in packages/server-common/src/env/ServerEnv.ts:444
|
|
33
|
-
* and `getAppgenServerBaseUrl` in packages/workflows/.../app_workspace/workspace-utils.ts), so the
|
|
34
|
-
* host published to browsers is region-scoped, while the per-branch `app-gateway-dev-<branch>`
|
|
35
|
-
* shape in the infrastructure README also exists. Matching `app-gateway-<anything>` covers both,
|
|
36
|
-
* and every Cloud Run service named that way *is* a gateway (region-compute/main.tf:373).
|
|
37
|
-
*
|
|
38
|
-
* These serve every tenant's published application from one shared origin, and published assets
|
|
39
|
-
* need no authentication — so this is generated, low-trust code sitting on a Vertesia-owned
|
|
40
|
-
* hostname. Owning the domain is not the test; which backend serves the origin is.
|
|
41
|
-
*
|
|
42
|
-
* Two load-balancer variables can map further hostnames onto the same backend —
|
|
43
|
-
* `appgen_host_aliases` and `appgen_preview_host_aliases` in
|
|
44
|
-
* infrastructure/terraform/modules/region-lb/variables.tf. Both are empty in every region today; if
|
|
45
|
-
* one is ever set, add the alias here too, or it silently falls through to the permissive branch of
|
|
46
|
-
* Policy L and becomes an origin that may receive a token at any path.
|
|
47
|
-
*/
|
|
48
|
-
export const GATEWAY_HOST_PATTERNS: readonly RegExp[] = [
|
|
49
|
-
new RegExp(`^apps(?:-preview|-preprod)?\\.${REGION}vertesia\\.io$`),
|
|
50
|
-
new RegExp(`^app-gateway-${LABEL}\\.api\\.${LABEL}\\.vertesia\\.io$`),
|
|
51
|
-
];
|
|
6
|
+
const REGION = `(?:${LABEL}\\.)?`;
|
|
52
7
|
|
|
53
8
|
export function isLoopbackHostname(hostname: string): boolean {
|
|
54
9
|
if (hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
package/src/session/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
redirectToCentralAuth,
|
|
9
9
|
} from './auth/domainRouting';
|
|
10
10
|
export * from './auth/firebase';
|
|
11
|
-
export { isAppOAuthLoginPending, startAppOAuthLogin } from './auth/oauth';
|
|
11
|
+
export { isAppOAuthLoginPending, revokeAppOAuthSession, startAppOAuthLogin } from './auth/oauth';
|
|
12
12
|
export * from './auth/useAuthState';
|
|
13
13
|
export * from './auth/useCurrentTenant';
|
|
14
14
|
export * from './DevSessionProvider';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* another tab's next token refresh. `localStorage` holds the last selection made in any tab, and
|
|
7
7
|
* only seeds a tab that has no scope of its own yet (a new tab, or the app after a restart).
|
|
8
8
|
*/
|
|
9
|
+
import { Env } from '@vertesia/ui/env';
|
|
9
10
|
import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY } from './constants';
|
|
10
11
|
|
|
11
12
|
const TabAccountId_KEY = 'composableai.tabAccountId';
|
|
@@ -52,6 +53,18 @@ export function readScopeSelection(accountId?: string): ScopeSelection {
|
|
|
52
53
|
return { accountId: account, projectId: projectId || undefined };
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export function resolveAuthSelection(currentUrl: URL): { accountId?: string; projectId?: string } {
|
|
57
|
+
// A URL selection owns the whole pair: never attach an unrelated configured account/project.
|
|
58
|
+
const hasUrlScope = currentUrl.searchParams.has('a') || currentUrl.searchParams.has('p');
|
|
59
|
+
const defaults = hasUrlScope ? undefined : Env.defaultAuthSelection;
|
|
60
|
+
const urlAccount = currentUrl.searchParams.get('a') ?? defaults?.accountId;
|
|
61
|
+
const urlProject = currentUrl.searchParams.get('p') ?? defaults?.projectId;
|
|
62
|
+
// A project alone identifies its account, so the stored selection only fills in a missing project.
|
|
63
|
+
const stored = urlProject === undefined ? readScopeSelection(urlAccount) : undefined;
|
|
64
|
+
|
|
65
|
+
return { accountId: urlAccount ?? stored?.accountId, projectId: urlProject ?? stored?.projectId };
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
/** Make this the tab's scope and the default for new tabs. */
|
|
56
69
|
export function rememberScopeSelection(accountId: string, projectId?: string): void {
|
|
57
70
|
sessionSet(TabAccountId_KEY, accountId);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { useCallback, useEffect, useState } from 'react';
|
|
14
14
|
import SignInAuthPending from './SignInAuthPending';
|
|
15
15
|
import SignInEmailStep, { type TenantInfo } from './SignInEmailStep';
|
|
16
|
+
import SignInPasswordStep from './SignInPasswordStep';
|
|
16
17
|
import SignInProvidersStep from './SignInProvidersStep';
|
|
17
18
|
import SignInReturningStep from './SignInReturningStep';
|
|
18
19
|
import SignInTenantStep from './SignInTenantStep';
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
} from './signInUtils';
|
|
29
30
|
|
|
30
31
|
/** The modes every host shares. Hosts may add their own; see the `M` parameter below. */
|
|
31
|
-
export type SignInCoreMode = 'email' | 'providers' | 'tenant' | 'returning' | 'pending';
|
|
32
|
+
export type SignInCoreMode = 'email' | 'providers' | 'tenant' | 'password' | 'returning' | 'pending';
|
|
32
33
|
|
|
33
34
|
export interface SignInFlowOptions {
|
|
34
35
|
/**
|
|
@@ -64,6 +65,8 @@ export interface SignInFlowController<M extends string = never> {
|
|
|
64
65
|
pendingProvider: ProviderId | null;
|
|
65
66
|
/** Email accepted: go to the tenant step when one resolved, otherwise the provider list. */
|
|
66
67
|
onProceedFromEmail: (email: string, tenant?: TenantInfo) => void;
|
|
68
|
+
/** Show password entry when tenant discovery or a remembered provider requires it. */
|
|
69
|
+
onPasswordRequired: (email: string) => void;
|
|
67
70
|
/** Back to the email step, dropping the resolved tenant. */
|
|
68
71
|
onBack: () => void;
|
|
69
72
|
/** "Not you?" — forget the remembered identity and sign out. */
|
|
@@ -92,7 +95,12 @@ export function useSignInFlow<M extends string = never>(options: SignInFlowOptio
|
|
|
92
95
|
const onProceedFromEmail = useCallback((e: string, t: TenantInfo | undefined) => {
|
|
93
96
|
setEmail(e);
|
|
94
97
|
setTenant(t);
|
|
95
|
-
setMode(t ? 'tenant' : 'providers');
|
|
98
|
+
setMode(t?.provider === 'password' ? 'password' : t ? 'tenant' : 'providers');
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const onPasswordRequired = useCallback((e: string) => {
|
|
102
|
+
setEmail(e);
|
|
103
|
+
setMode('password');
|
|
96
104
|
}, []);
|
|
97
105
|
|
|
98
106
|
const onBack = useCallback(() => {
|
|
@@ -144,6 +152,7 @@ export function useSignInFlow<M extends string = never>(options: SignInFlowOptio
|
|
|
144
152
|
setStoredSession,
|
|
145
153
|
pendingProvider,
|
|
146
154
|
onProceedFromEmail,
|
|
155
|
+
onPasswordRequired,
|
|
147
156
|
onBack,
|
|
148
157
|
onNotYou,
|
|
149
158
|
onProviderClicked,
|
|
@@ -178,6 +187,15 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
178
187
|
if (mode === 'pending' && pendingProvider) {
|
|
179
188
|
return <SignInAuthPending provider={pendingProvider} />;
|
|
180
189
|
}
|
|
190
|
+
if (mode === 'password' && email) {
|
|
191
|
+
return (
|
|
192
|
+
<SignInPasswordStep
|
|
193
|
+
email={email}
|
|
194
|
+
tenantName={tenant?.label || tenant?.name || storedSession?.tenantName || undefined}
|
|
195
|
+
onBack={flow.onBack}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
181
199
|
if (mode === 'tenant' && tenant) {
|
|
182
200
|
return (
|
|
183
201
|
<SignInTenantStep
|
|
@@ -185,6 +203,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
185
203
|
tenant={tenant}
|
|
186
204
|
onBack={flow.onBack}
|
|
187
205
|
onProviderClicked={() => flow.onProviderClicked((tenant.provider ?? 'oidc') as ProviderId)}
|
|
206
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
188
207
|
redirectTo={redirectTo}
|
|
189
208
|
/>
|
|
190
209
|
);
|
|
@@ -195,6 +214,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
195
214
|
email={email}
|
|
196
215
|
onBack={flow.onBack}
|
|
197
216
|
onProviderClicked={flow.onProviderClicked}
|
|
217
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
198
218
|
redirectTo={redirectTo}
|
|
199
219
|
/>
|
|
200
220
|
);
|
|
@@ -205,6 +225,7 @@ export function SignInFlowSteps({ flow, redirectTo }: SignInFlowStepsProps) {
|
|
|
205
225
|
session={storedSession}
|
|
206
226
|
onNotYou={flow.onNotYou}
|
|
207
227
|
onProviderClicked={flow.onProviderClicked}
|
|
228
|
+
onPasswordRequired={flow.onPasswordRequired}
|
|
208
229
|
redirectTo={redirectTo}
|
|
209
230
|
/>
|
|
210
231
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LockKeyhole } from 'lucide-react';
|
|
1
|
+
import { KeyRound, LockKeyhole } from 'lucide-react';
|
|
2
2
|
import type { ComponentType, SVGProps } from 'react';
|
|
3
3
|
import type { ProviderId } from './signInUtils';
|
|
4
4
|
|
|
@@ -58,6 +58,7 @@ export const PROVIDER_ICONS: Record<ProviderId, ComponentType<{ className?: stri
|
|
|
58
58
|
github: GithubIcon,
|
|
59
59
|
microsoft: MicrosoftIcon,
|
|
60
60
|
oidc: OidcIcon,
|
|
61
|
+
password: KeyRound,
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
export function providerIcon(provider: ProviderId | string | undefined): ComponentType<{ className?: string }> {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Spinner } from '@vertesia/ui/core';
|
|
2
|
+
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
SignInEmailRow,
|
|
7
|
+
SignInPasswordField,
|
|
8
|
+
SignInStepButton,
|
|
9
|
+
SignInStepHeader,
|
|
10
|
+
SignInStepLayout,
|
|
11
|
+
} from './SignInPrimitives';
|
|
12
|
+
import { type PasswordSignInFailure, signInWithPassword } from './signInUtils';
|
|
13
|
+
|
|
14
|
+
interface SignInPasswordStepProps {
|
|
15
|
+
email: string;
|
|
16
|
+
tenantName?: string;
|
|
17
|
+
onBack: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Password sign-in for an address whose tenant issues passwords instead of federating an IdP. */
|
|
21
|
+
export default function SignInPasswordStep({ email, tenantName, onBack }: SignInPasswordStepProps) {
|
|
22
|
+
const { t } = useUITranslation();
|
|
23
|
+
const [password, setPassword] = useState('');
|
|
24
|
+
const [failure, setFailure] = useState<PasswordSignInFailure | null>(null);
|
|
25
|
+
const [emptyError, setEmptyError] = useState(false);
|
|
26
|
+
const [loading, setLoading] = useState(false);
|
|
27
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
inputRef.current?.focus();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const submit = async (e: React.FormEvent) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (loading) return;
|
|
36
|
+
if (!password) {
|
|
37
|
+
setEmptyError(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setEmptyError(false);
|
|
41
|
+
setFailure(null);
|
|
42
|
+
setLoading(true);
|
|
43
|
+
const result = await signInWithPassword(email, password);
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
setFailure(result.reason);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
// On success the spinner stays up: the session's auth-state listener finishes the login and
|
|
49
|
+
// unmounts this screen.
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Literal keys, one per failure, so i18next-cli extraction sees every message.
|
|
53
|
+
const failureMessage = (reason: PasswordSignInFailure) => {
|
|
54
|
+
switch (reason) {
|
|
55
|
+
case 'invalid-credentials':
|
|
56
|
+
return t('auth.password.invalidCredentials');
|
|
57
|
+
case 'too-many-attempts':
|
|
58
|
+
return t('auth.password.tooManyAttempts');
|
|
59
|
+
case 'not-password-tenant':
|
|
60
|
+
return t('auth.password.notAvailable');
|
|
61
|
+
case 'failed':
|
|
62
|
+
return t('auth.password.failed');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const error = emptyError ? t('auth.password.required') : failure ? failureMessage(failure) : undefined;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<SignInStepLayout>
|
|
69
|
+
<SignInStepHeader
|
|
70
|
+
eyebrow={tenantName ?? t('auth.password.eyebrow')}
|
|
71
|
+
title={t('auth.password.title')}
|
|
72
|
+
body={t('auth.password.body')}
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<SignInEmailRow email={email} actionLabel={t('auth.change')} onAction={onBack} />
|
|
76
|
+
|
|
77
|
+
<form onSubmit={submit} noValidate className={`flex flex-col ${error ? 'gap-2' : 'gap-6'}`}>
|
|
78
|
+
<SignInPasswordField
|
|
79
|
+
inputRef={inputRef}
|
|
80
|
+
label={t('auth.password.label')}
|
|
81
|
+
value={password}
|
|
82
|
+
onChange={(value) => {
|
|
83
|
+
setPassword(value);
|
|
84
|
+
if (emptyError) setEmptyError(false);
|
|
85
|
+
}}
|
|
86
|
+
invalid={!!error}
|
|
87
|
+
error={error}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<SignInStepButton type="submit" disabled={loading}>
|
|
91
|
+
{loading ? (
|
|
92
|
+
<Spinner />
|
|
93
|
+
) : (
|
|
94
|
+
<>
|
|
95
|
+
<span>{t('auth.password.submit')}</span>
|
|
96
|
+
<ArrowRight className="!size-3.5" />
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</SignInStepButton>
|
|
100
|
+
</form>
|
|
101
|
+
</SignInStepLayout>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -366,6 +366,11 @@ export function SignInOrDivider({ children }: { children: ReactNode }) {
|
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
const SIGNIN_INPUT_CLASS =
|
|
370
|
+
'h-[42px] px-3.5 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted ' +
|
|
371
|
+
'outline-none transition focus:border-info focus:ring-4 focus:ring-info/15 ' +
|
|
372
|
+
'aria-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/15';
|
|
373
|
+
|
|
369
374
|
interface SignInEmailFieldProps {
|
|
370
375
|
inputRef?: Ref<HTMLInputElement>;
|
|
371
376
|
label: ReactNode;
|
|
@@ -396,7 +401,7 @@ export function SignInEmailField({
|
|
|
396
401
|
id="vt-login-email"
|
|
397
402
|
name="vt-login-email"
|
|
398
403
|
type="email"
|
|
399
|
-
className=
|
|
404
|
+
className={SIGNIN_INPUT_CLASS}
|
|
400
405
|
placeholder={placeholder}
|
|
401
406
|
value={value}
|
|
402
407
|
onChange={(e) => onChange(e.target.value)}
|
|
@@ -417,3 +422,50 @@ export function SignInEmailField({
|
|
|
417
422
|
</div>
|
|
418
423
|
);
|
|
419
424
|
}
|
|
425
|
+
|
|
426
|
+
interface SignInPasswordFieldProps {
|
|
427
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
428
|
+
label: ReactNode;
|
|
429
|
+
value: string;
|
|
430
|
+
onChange: (value: string) => void;
|
|
431
|
+
invalid?: boolean;
|
|
432
|
+
error?: ReactNode;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Labeled password input with an inline error. Unlike the email field it leaves password managers
|
|
437
|
+
* enabled: a password account's credential is issued once and is expected to be stored in one.
|
|
438
|
+
*/
|
|
439
|
+
export function SignInPasswordField({
|
|
440
|
+
inputRef,
|
|
441
|
+
label,
|
|
442
|
+
value,
|
|
443
|
+
onChange,
|
|
444
|
+
invalid = false,
|
|
445
|
+
error,
|
|
446
|
+
}: SignInPasswordFieldProps) {
|
|
447
|
+
return (
|
|
448
|
+
<div className="flex flex-col gap-1.5">
|
|
449
|
+
<label htmlFor="vt-login-password" className="text-xs font-medium text-foreground/80">
|
|
450
|
+
{label}
|
|
451
|
+
</label>
|
|
452
|
+
<input
|
|
453
|
+
ref={inputRef}
|
|
454
|
+
id="vt-login-password"
|
|
455
|
+
name="vt-login-password"
|
|
456
|
+
type="password"
|
|
457
|
+
className={SIGNIN_INPUT_CLASS}
|
|
458
|
+
value={value}
|
|
459
|
+
onChange={(e) => onChange(e.target.value)}
|
|
460
|
+
aria-invalid={invalid}
|
|
461
|
+
aria-describedby={error ? 'vt-login-password-error' : undefined}
|
|
462
|
+
autoComplete="current-password"
|
|
463
|
+
/>
|
|
464
|
+
{error && (
|
|
465
|
+
<div id="vt-login-password-error" role="alert" className="text-xs text-destructive">
|
|
466
|
+
{error}
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import { useUITranslation } from '@vertesia/ui/i18n';
|
|
2
2
|
import { SignInEmailRow, SignInProviderButton, SignInStepHeader, SignInStepLayout } from './SignInPrimitives';
|
|
3
|
-
import { type ProviderId, providerLabel, startSignIn } from './signInUtils';
|
|
3
|
+
import { type ProviderId, providerLabel, type RedirectProviderId, startSignIn } from './signInUtils';
|
|
4
4
|
|
|
5
5
|
interface SignInProvidersStepProps {
|
|
6
6
|
email: string;
|
|
7
7
|
onBack: () => void;
|
|
8
8
|
onProviderClicked: (provider: ProviderId) => void;
|
|
9
|
+
/** The address resolved to a password tenant on retry, so the parent shows the password step. */
|
|
10
|
+
onPasswordRequired: (email: string) => void;
|
|
9
11
|
redirectTo?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
const PROVIDERS:
|
|
14
|
+
const PROVIDERS: RedirectProviderId[] = ['google', 'github', 'microsoft'];
|
|
13
15
|
|
|
14
16
|
export default function SignInProvidersStep({
|
|
15
17
|
email,
|
|
16
18
|
onBack,
|
|
17
19
|
onProviderClicked,
|
|
20
|
+
onPasswordRequired,
|
|
18
21
|
redirectTo,
|
|
19
22
|
}: SignInProvidersStepProps) {
|
|
20
23
|
const { t } = useUITranslation();
|
|
21
24
|
|
|
22
|
-
const pick = async (provider:
|
|
25
|
+
const pick = async (provider: RedirectProviderId) => {
|
|
23
26
|
onProviderClicked(provider);
|
|
24
|
-
await startSignIn(provider, email, redirectTo);
|
|
27
|
+
const result = await startSignIn(provider, email, redirectTo);
|
|
28
|
+
if (!result.ok && result.reason === 'password-required') onPasswordRequired(email);
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
return (
|