@cat-factory/app 0.58.5 → 0.59.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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref } from 'vue'
2
+ import { computed, ref, watch } from 'vue'
3
3
  import { apiErrorEnvelope } from '~/composables/api/errors'
4
4
 
5
5
  const auth = useAuthStore()
@@ -123,6 +123,51 @@ function setMode(next: 'login' | 'signup' | 'forgot') {
123
123
  const showOAuthDivider = computed(
124
124
  () => auth.providers.password && (auth.providers.github || auth.providers.google),
125
125
  )
126
+
127
+ // Hosted (remote node) PAT login: the user pastes their OWN source-control PAT, which the
128
+ // server resolves to an account and holds to its login/org/domain allowlist. The available
129
+ // providers come from the server (`auth.patProviders`); empty in local mode (which uses the
130
+ // configured-token flow above) and on OAuth-only facades like the Worker.
131
+ const remotePatProviders = computed<PatProvider[]>(() =>
132
+ isLocalMode.value ? [] : (auth.patProviders as PatProvider[]),
133
+ )
134
+ const remotePatProvider = ref<PatProvider>('github')
135
+ watch(
136
+ remotePatProviders,
137
+ (list) => {
138
+ if (list.length && !list.includes(remotePatProvider.value)) remotePatProvider.value = list[0]!
139
+ },
140
+ { immediate: true },
141
+ )
142
+ const remotePatToken = ref('')
143
+ const remotePatBusy = ref(false)
144
+ const remotePatError = ref<string | null>(null)
145
+
146
+ async function submitRemotePat() {
147
+ remotePatError.value = null
148
+ remotePatBusy.value = true
149
+ try {
150
+ await auth.patLogin({ provider: remotePatProvider.value, token: remotePatToken.value.trim() })
151
+ if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
152
+ } catch (e) {
153
+ remotePatError.value = apiErrorEnvelope(e)?.message ?? t('auth.login.signInFailed')
154
+ } finally {
155
+ remotePatBusy.value = false
156
+ }
157
+ }
158
+
159
+ // A remote deployment (node service / Worker) that advertises no sign-in method at all:
160
+ // no OAuth, no password, no PAT, and not local mode. The auth gate still routes here (a
161
+ // remote facade has no anonymous tier), so instead of a blank card we explain that
162
+ // authentication isn't configured and there's nothing to sign in with yet.
163
+ const noSignInMethod = computed(
164
+ () =>
165
+ !isLocalMode.value &&
166
+ !auth.providers.github &&
167
+ !auth.providers.google &&
168
+ !auth.providers.password &&
169
+ remotePatProviders.value.length === 0,
170
+ )
126
171
  </script>
127
172
 
128
173
  <template>
@@ -296,6 +341,76 @@ const showOAuthDivider = computed(
296
341
  </p>
297
342
  </form>
298
343
 
344
+ <!-- Hosted (remote node) PAT login: paste your own source-control PAT -->
345
+ <template v-if="remotePatProviders.length > 0 && mode !== 'forgot'">
346
+ <div
347
+ v-if="auth.providers.github || auth.providers.google || auth.providers.password"
348
+ class="my-4 flex items-center gap-3 text-xs text-slate-500"
349
+ >
350
+ <span class="h-px flex-1 bg-slate-800" /> {{ t('auth.login.or') }}
351
+ <span class="h-px flex-1 bg-slate-800" />
352
+ </div>
353
+ <form class="space-y-3" @submit.prevent="submitRemotePat">
354
+ <div v-if="remotePatProviders.length > 1" class="flex gap-2">
355
+ <UButton
356
+ v-for="p in remotePatProviders"
357
+ :key="p"
358
+ :color="p === remotePatProvider ? 'primary' : 'neutral'"
359
+ :variant="p === remotePatProvider ? 'solid' : 'subtle'"
360
+ :icon="PROVIDER_ICONS[p]"
361
+ size="sm"
362
+ @click="remotePatProvider = p"
363
+ >
364
+ {{ PROVIDER_LABELS[p] }}
365
+ </UButton>
366
+ </div>
367
+ <UInput
368
+ v-model="remotePatToken"
369
+ type="password"
370
+ required
371
+ :placeholder="
372
+ t('auth.login.patPlaceholder', { provider: PROVIDER_LABELS[remotePatProvider] })
373
+ "
374
+ icon="i-lucide-key-round"
375
+ size="lg"
376
+ class="w-full"
377
+ />
378
+ <p v-if="remotePatError" class="text-sm text-rose-400">{{ remotePatError }}</p>
379
+ <UButton
380
+ block
381
+ size="lg"
382
+ color="primary"
383
+ type="submit"
384
+ :icon="PROVIDER_ICONS[remotePatProvider]"
385
+ :loading="remotePatBusy"
386
+ >
387
+ {{ t('auth.login.signInWithPat', { provider: PROVIDER_LABELS[remotePatProvider] }) }}
388
+ </UButton>
389
+ <p class="px-1 text-center">
390
+ <a
391
+ :href="tokenCreateUrl(remotePatProvider)"
392
+ target="_blank"
393
+ rel="noopener noreferrer"
394
+ class="text-xs text-indigo-400 hover:underline"
395
+ >
396
+ {{
397
+ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[remotePatProvider] })
398
+ }}
399
+ </a>
400
+ </p>
401
+ </form>
402
+ </template>
403
+
404
+ <!-- No sign-in method configured on a remote deployment: explain, don't show a blank card -->
405
+ <UAlert
406
+ v-if="noSignInMethod"
407
+ color="warning"
408
+ variant="subtle"
409
+ icon="i-lucide-shield-alert"
410
+ :title="t('auth.login.notConfiguredTitle')"
411
+ :description="t('auth.login.notConfiguredBody')"
412
+ />
413
+
299
414
  <!-- Forgot password: request a reset link by email -->
300
415
  <form
301
416
  v-if="auth.providers.password && mode === 'forgot'"
@@ -25,6 +25,18 @@ export const useAuthStore = defineStore(
25
25
  const required = ref(false)
26
26
  /** Which login providers the backend offers (drives the login UI). */
27
27
  const providers = ref({ github: false, password: false, google: false })
28
+ /**
29
+ * Source-control providers a HOSTED facade (remote node) accepts a user-supplied PAT for.
30
+ * Drives the login screen's "sign in with a PAT" option on non-local deployments. Empty on
31
+ * the Worker (OAuth-only) and in local mode (which uses `localMode.patLogin` instead).
32
+ */
33
+ const patProviders = ref<('github' | 'gitlab')[]>([])
34
+ /**
35
+ * Test-only: the backend advertised that it runs with NO authentication (its
36
+ * `TESTING_NO_AUTH` opt-in). When set, the SPA renders the board anonymously instead of
37
+ * gating to the login screen — even on a remote facade. Only ever true under the e2e suite.
38
+ */
39
+ const testingNoAuth = ref(false)
28
40
  /**
29
41
  * Local-mode signals from the backend. Present only when running the local facade;
30
42
  * `githubPatSetupUrl` is set when local mode has no GitHub PAT configured (drives the
@@ -48,18 +60,44 @@ export const useAuthStore = defineStore(
48
60
  const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
49
61
  /** True once the initial auth handshake has settled. */
50
62
  const ready = ref(false)
63
+ /**
64
+ * True only once `getAuthConfig()` has resolved successfully. Distinguishes "the backend
65
+ * told us auth is off" from "we never reached the backend" (the bootstrap catch path),
66
+ * so an unreachable backend falls through to the board's own error UI instead of being
67
+ * mistaken for an unauthenticated session and gated to the login screen.
68
+ */
69
+ const configLoaded = ref(false)
70
+ /**
71
+ * Whether this is the local-mode facade. Only the local facade reports `localMode`; the
72
+ * remote node service and the Cloudflare Worker never do. Used to tell "a developer's
73
+ * own machine (anonymous-but-dev-open is its own thing)" apart from "a remote deployment
74
+ * that has no anonymous tier".
75
+ */
76
+ const isLocalFacade = computed(() => localMode.value !== null)
51
77
 
52
78
  /** May the app render? True when auth is off, or on with a known user. */
53
79
  const isAuthenticated = computed(() => !required.value || user.value !== null)
54
80
 
55
81
  /**
56
- * Whether the SPA must show the login screen before the board. Auth-enabled deployments
57
- * gate on a user as before; local mode ALSO gates (even though its API stays dev-open),
58
- * because anonymous local use can't store per-user credentials — see the login flow.
82
+ * Whether the SPA must show the login screen before the board.
83
+ *
84
+ * - Auth-enabled deployments gate on a user as before.
85
+ * - Local mode ALSO gates (even though its API stays dev-open), because anonymous local
86
+ * use can't store per-user credentials — see the login flow.
87
+ * - A REMOTE facade (node service / Worker) has NO anonymous tier: once the auth handshake
88
+ * has resolved and there's no user, gate — even when the backend reports auth "disabled"
89
+ * (a misconfigured/dev-open remote running without a provider). Previously this slipped
90
+ * through and dropped the user onto a board where every per-user action silently failed
91
+ * with no sign-in affordance; the login screen now surfaces that state (offering a
92
+ * provider, or explaining that none is configured).
59
93
  */
60
- const needsLogin = computed(
61
- () => (required.value || localMode.value?.enabled === true) && user.value === null,
62
- )
94
+ const needsLogin = computed(() => {
95
+ if (!configLoaded.value || user.value !== null) return false
96
+ // A deployment that explicitly runs with no auth (the test opt-in) renders anonymously.
97
+ if (testingNoAuth.value) return false
98
+ if (isLocalFacade.value) return required.value || localMode.value?.enabled === true
99
+ return true
100
+ })
63
101
 
64
102
  /** Pull a token handed back in the post-login URL fragment (#token=…). */
65
103
  function consumeRedirectToken() {
@@ -78,10 +116,14 @@ export const useAuthStore = defineStore(
78
116
  const config = await api.getAuthConfig()
79
117
  required.value = config.enabled
80
118
  if (config.providers) providers.value = config.providers
119
+ patProviders.value = config.patLogin?.providers ?? []
120
+ testingNoAuth.value = config.testingNoAuth ?? false
81
121
  localMode.value = config.localMode ?? null
82
122
  infrastructure.value = config.infrastructure ?? null
123
+ configLoaded.value = true
83
124
  } catch {
84
- // Backend unreachable — let the board's own error UI handle it.
125
+ // Backend unreachable — let the board's own error UI handle it (configLoaded stays
126
+ // false, so we never mistake this for an unauthenticated session and gate it).
85
127
  required.value = false
86
128
  ready.value = true
87
129
  return
@@ -229,10 +271,14 @@ export const useAuthStore = defineStore(
229
271
  user,
230
272
  required,
231
273
  providers,
274
+ patProviders,
275
+ testingNoAuth,
232
276
  localMode,
233
277
  infrastructure,
234
278
  autoLoginProvider,
235
279
  ready,
280
+ configLoaded,
281
+ isLocalFacade,
236
282
  isAuthenticated,
237
283
  needsLogin,
238
284
  bootstrap,
@@ -812,7 +812,11 @@
812
812
  "sendResetLink": "Send reset link",
813
813
  "backToSignIn": "Back to sign in",
814
814
  "signInFailed": "Sign-in failed. Check your details and try again.",
815
- "genericError": "Something went wrong. Please try again."
815
+ "genericError": "Something went wrong. Please try again.",
816
+ "notConfiguredTitle": "Authentication isn't configured",
817
+ "notConfiguredBody": "This deployment has no sign-in method enabled, so you can't sign in or access your workspaces. An administrator needs to configure an authentication provider (GitHub or Google OAuth, or email and password login).",
818
+ "patPlaceholder": "{provider} personal access token",
819
+ "signInWithPat": "Sign in with {provider} PAT"
816
820
  },
817
821
  "resetPassword": {
818
822
  "title": "Reset password",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "Enviar enlace de restablecimiento",
773
773
  "backToSignIn": "Volver al inicio de sesión",
774
774
  "signInFailed": "Error al iniciar sesión. Revisa tus datos e inténtalo de nuevo.",
775
- "genericError": "Algo salió mal. Inténtalo de nuevo."
775
+ "genericError": "Algo salió mal. Inténtalo de nuevo.",
776
+ "notConfiguredTitle": "La autenticación no está configurada",
777
+ "notConfiguredBody": "Este despliegue no tiene ningún método de inicio de sesión habilitado, por lo que no puedes iniciar sesión ni acceder a tus espacios de trabajo. Un administrador debe configurar un proveedor de autenticación (OAuth de GitHub o Google, o inicio de sesión con correo y contraseña).",
778
+ "patPlaceholder": "Token de acceso personal de {provider}",
779
+ "signInWithPat": "Iniciar sesión con un PAT de {provider}"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "Restablecer contraseña",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "Envoyer le lien de réinitialisation",
773
773
  "backToSignIn": "Retour à la connexion",
774
774
  "signInFailed": "Échec de la connexion. Vérifiez vos informations et réessayez.",
775
- "genericError": "Une erreur s'est produite. Veuillez réessayer."
775
+ "genericError": "Une erreur s'est produite. Veuillez réessayer.",
776
+ "notConfiguredTitle": "L'authentification n'est pas configurée",
777
+ "notConfiguredBody": "Ce déploiement n'a aucune méthode de connexion activée, vous ne pouvez donc pas vous connecter ni accéder à vos espaces de travail. Un administrateur doit configurer un fournisseur d'authentification (OAuth GitHub ou Google, ou connexion par e-mail et mot de passe).",
778
+ "patPlaceholder": "Jeton d'accès personnel {provider}",
779
+ "signInWithPat": "Se connecter avec un PAT {provider}"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "Réinitialiser le mot de passe",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "שלח קישור איפוס",
773
773
  "backToSignIn": "חזרה להתחברות",
774
774
  "signInFailed": "ההתחברות נכשלה. בדוק את הפרטים שלך ונסה שוב.",
775
- "genericError": "משהו השתבש. אנא נסה שוב."
775
+ "genericError": "משהו השתבש. אנא נסה שוב.",
776
+ "notConfiguredTitle": "האימות אינו מוגדר",
777
+ "notConfiguredBody": "בפריסה זו לא מופעלת אף שיטת התחברות, ולכן לא ניתן להתחבר או לגשת למרחבי העבודה שלך. מנהל המערכת צריך להגדיר ספק אימות (GitHub או Google OAuth, או התחברות עם אימייל וסיסמה).",
778
+ "patPlaceholder": "אסימון גישה אישי של {provider}",
779
+ "signInWithPat": "התחברות עם PAT של {provider}"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "אפס סיסמה",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "リセットリンクを送信",
773
773
  "backToSignIn": "サインインに戻る",
774
774
  "signInFailed": "サインインに失敗しました。入力内容を確認して、もう一度お試しください。",
775
- "genericError": "問題が発生しました。もう一度お試しください。"
775
+ "genericError": "問題が発生しました。もう一度お試しください。",
776
+ "notConfiguredTitle": "認証が設定されていません",
777
+ "notConfiguredBody": "このデプロイにはサインイン方法が有効になっていないため、サインインやワークスペースへのアクセスができません。管理者が認証プロバイダー(GitHub または Google の OAuth、あるいはメールアドレスとパスワードによるログイン)を設定する必要があります。",
778
+ "patPlaceholder": "{provider} のパーソナルアクセストークン",
779
+ "signInWithPat": "{provider} の PAT でサインイン"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "パスワードをリセット",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "Wyślij link resetujący",
773
773
  "backToSignIn": "Powrót do logowania",
774
774
  "signInFailed": "Logowanie nie powiodło się. Sprawdź swoje dane i spróbuj ponownie.",
775
- "genericError": "Coś poszło nie tak. Spróbuj ponownie."
775
+ "genericError": "Coś poszło nie tak. Spróbuj ponownie.",
776
+ "notConfiguredTitle": "Uwierzytelnianie nie jest skonfigurowane",
777
+ "notConfiguredBody": "To wdrożenie nie ma włączonej żadnej metody logowania, więc nie możesz się zalogować ani uzyskać dostępu do swoich przestrzeni roboczych. Administrator musi skonfigurować dostawcę uwierzytelniania (OAuth GitHub lub Google albo logowanie e-mailem i hasłem).",
778
+ "patPlaceholder": "Osobisty token dostępu {provider}",
779
+ "signInWithPat": "Zaloguj się tokenem PAT {provider}"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "Zresetuj hasło",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "Sıfırlama bağlantısı gönder",
773
773
  "backToSignIn": "Oturum açmaya dön",
774
774
  "signInFailed": "Oturum açma başarısız. Bilgilerinizi kontrol edip tekrar deneyin.",
775
- "genericError": "Bir şeyler ters gitti. Lütfen tekrar deneyin."
775
+ "genericError": "Bir şeyler ters gitti. Lütfen tekrar deneyin.",
776
+ "notConfiguredTitle": "Kimlik doğrulama yapılandırılmamış",
777
+ "notConfiguredBody": "Bu dağıtımda etkin bir oturum açma yöntemi yok, bu nedenle oturum açamaz veya çalışma alanlarınıza erişemezsiniz. Bir yönetici, bir kimlik doğrulama sağlayıcısı (GitHub veya Google OAuth ya da e-posta ve parola ile oturum açma) yapılandırmalıdır.",
778
+ "patPlaceholder": "{provider} kişisel erişim belirteci",
779
+ "signInWithPat": "{provider} PAT ile oturum aç"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "Parolayı sıfırla",
@@ -772,7 +772,11 @@
772
772
  "sendResetLink": "Надіслати посилання для скидання",
773
773
  "backToSignIn": "Повернутися до входу",
774
774
  "signInFailed": "Не вдалося увійти. Перевірте свої дані та спробуйте ще раз.",
775
- "genericError": "Щось пішло не так. Спробуйте ще раз."
775
+ "genericError": "Щось пішло не так. Спробуйте ще раз.",
776
+ "notConfiguredTitle": "Автентифікацію не налаштовано",
777
+ "notConfiguredBody": "У цьому розгортанні не ввімкнено жодного способу входу, тому ви не можете увійти чи отримати доступ до своїх робочих просторів. Адміністратор має налаштувати постачальника автентифікації (OAuth GitHub або Google чи вхід за електронною поштою та паролем).",
778
+ "patPlaceholder": "Особистий токен доступу {provider}",
779
+ "signInWithPat": "Увійти за допомогою PAT {provider}"
776
780
  },
777
781
  "resetPassword": {
778
782
  "title": "Скинути пароль",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.58.5",
3
+ "version": "0.59.0",
4
4
  "description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "^3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.62.0"
37
+ "@cat-factory/contracts": "0.63.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",