@cat-factory/app 0.47.6 → 0.47.7
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/app/components/auth/LoginScreen.vue +41 -69
- package/app/stores/auth.ts +41 -2
- package/i18n/locales/en.json +4 -5
- package/i18n/locales/es.json +4 -5
- package/i18n/locales/fr.json +4 -5
- package/i18n/locales/pl.json +4 -5
- package/i18n/locales/uk.json +4 -5
- package/package.json +2 -2
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
import { apiErrorEnvelope } from '~/composables/api/errors'
|
|
4
4
|
|
|
5
5
|
const auth = useAuthStore()
|
|
6
6
|
const { t } = useI18n()
|
|
7
7
|
|
|
8
|
-
// Local-mode source-control PAT login.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// Local-mode source-control PAT login. The PAT lives server-side in env (GITHUB_PAT /
|
|
9
|
+
// GITLAB_PAT); the login screen only SELECTS a configured provider — no token is ever typed
|
|
10
|
+
// into or shown in the browser. GitHub/GitLab are brand names (kept verbatim across locales),
|
|
11
|
+
// as are the token-settings URLs, so they're inline constants rather than catalog keys — same
|
|
12
|
+
// convention as the provider descriptors in ApiKeysSection. The "create a token" link prefers
|
|
13
|
+
// the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the fallback.
|
|
13
14
|
type PatProvider = 'github' | 'gitlab'
|
|
15
|
+
const ALL_PROVIDERS: PatProvider[] = ['github', 'gitlab']
|
|
14
16
|
const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
|
|
15
17
|
const PROVIDER_ICONS: Record<PatProvider, string> = {
|
|
16
18
|
github: 'i-lucide-github',
|
|
@@ -23,44 +25,29 @@ const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const patLoginCfg = computed(() => auth.localMode?.patLogin)
|
|
28
|
+
// Only providers whose PAT is configured in env can sign in (the token is the operational
|
|
29
|
+
// credential too). A provider without one gets no button — see the no-PAT notice instead.
|
|
26
30
|
const configuredProviders = computed<PatProvider[]>(
|
|
27
31
|
() => (patLoginCfg.value?.configured ?? []) as PatProvider[],
|
|
28
32
|
)
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
const showLocalLogin = computed(() => availableProviders.value.length > 0)
|
|
33
|
+
const isLocalMode = computed(() => auth.localMode?.enabled === true)
|
|
34
|
+
const hasConfiguredPat = computed(() => configuredProviders.value.length > 0)
|
|
33
35
|
|
|
34
|
-
const patProvider = ref<PatProvider>('github')
|
|
35
|
-
const patToken = ref('')
|
|
36
36
|
const patBusy = ref(false)
|
|
37
37
|
const patError = ref<string | null>(null)
|
|
38
38
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
{ immediate: true },
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
const patProviderItems = computed(() =>
|
|
49
|
-
availableProviders.value.map((p) => ({ label: PROVIDER_LABELS[p], value: p })),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
// Prefer the server's scopes-preselected deep link (it owns the per-provider scopes);
|
|
53
|
-
// fall back to the plain token page if it wasn't advertised.
|
|
54
|
-
const tokenCreateUrl = computed(
|
|
55
|
-
() => patLoginCfg.value?.setupUrls?.[patProvider.value] ?? PROVIDER_TOKEN_URLS[patProvider.value],
|
|
56
|
-
)
|
|
39
|
+
// Per-provider "create a token" link: prefer the server's scopes-preselected deep link (it
|
|
40
|
+
// owns the per-provider scopes), fall back to the plain token page.
|
|
41
|
+
function tokenCreateUrl(provider: PatProvider): string {
|
|
42
|
+
return patLoginCfg.value?.setupUrls?.[provider] ?? PROVIDER_TOKEN_URLS[provider]
|
|
43
|
+
}
|
|
57
44
|
|
|
58
|
-
/**
|
|
59
|
-
async function submitPat(provider: PatProvider
|
|
45
|
+
/** Sign in as the account the configured env PAT belongs to; reloads so the app boots in. */
|
|
46
|
+
async function submitPat(provider: PatProvider) {
|
|
60
47
|
patError.value = null
|
|
61
48
|
patBusy.value = true
|
|
62
49
|
try {
|
|
63
|
-
await auth.patLogin(
|
|
50
|
+
await auth.patLogin({ provider })
|
|
64
51
|
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
65
52
|
} catch (e) {
|
|
66
53
|
patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
|
|
@@ -154,9 +141,10 @@ const showOAuthDivider = computed(
|
|
|
154
141
|
</p>
|
|
155
142
|
</div>
|
|
156
143
|
|
|
157
|
-
<!-- Local mode: sign in with
|
|
158
|
-
|
|
159
|
-
|
|
144
|
+
<!-- Local mode: sign in with the env-configured source-control PAT. The token lives
|
|
145
|
+
server-side (GITHUB_PAT / GITLAB_PAT); we only pick a provider here. -->
|
|
146
|
+
<div v-if="isLocalMode && mode !== 'forgot'" class="space-y-3">
|
|
147
|
+
<!-- One button per provider whose PAT is configured in env -->
|
|
160
148
|
<UButton
|
|
161
149
|
v-for="p in configuredProviders"
|
|
162
150
|
:key="p"
|
|
@@ -167,53 +155,37 @@ const showOAuthDivider = computed(
|
|
|
167
155
|
:loading="patBusy"
|
|
168
156
|
@click="submitPat(p)"
|
|
169
157
|
>
|
|
170
|
-
{{ t('auth.localMode.
|
|
158
|
+
{{ t('auth.localMode.continueWithConfigured', { provider: PROVIDER_LABELS[p] }) }}
|
|
171
159
|
</UButton>
|
|
172
160
|
|
|
173
|
-
<!--
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
:
|
|
180
|
-
|
|
181
|
-
class="w-full"
|
|
182
|
-
/>
|
|
183
|
-
<UTextarea
|
|
184
|
-
v-model="patToken"
|
|
185
|
-
:rows="2"
|
|
186
|
-
:placeholder="
|
|
187
|
-
t('auth.localMode.tokenPlaceholder', { provider: PROVIDER_LABELS[patProvider] })
|
|
188
|
-
"
|
|
189
|
-
class="w-full font-mono"
|
|
161
|
+
<!-- Neither GITHUB_PAT nor GITLAB_PAT is set: tell the developer how to configure one -->
|
|
162
|
+
<template v-if="!hasConfiguredPat">
|
|
163
|
+
<UAlert
|
|
164
|
+
color="warning"
|
|
165
|
+
variant="subtle"
|
|
166
|
+
icon="i-lucide-key-round"
|
|
167
|
+
:title="t('auth.localMode.noPatTitle')"
|
|
168
|
+
:description="t('auth.localMode.noPatBody')"
|
|
190
169
|
/>
|
|
191
|
-
<div class="flex
|
|
170
|
+
<div class="flex flex-wrap gap-3 px-1">
|
|
192
171
|
<a
|
|
193
|
-
|
|
172
|
+
v-for="p in ALL_PROVIDERS"
|
|
173
|
+
:key="p"
|
|
174
|
+
:href="tokenCreateUrl(p)"
|
|
194
175
|
target="_blank"
|
|
195
176
|
rel="noopener noreferrer"
|
|
196
177
|
class="text-xs text-indigo-400 hover:underline"
|
|
197
178
|
>
|
|
198
|
-
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[
|
|
179
|
+
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[p] }) }}
|
|
199
180
|
</a>
|
|
200
|
-
<UButton
|
|
201
|
-
size="lg"
|
|
202
|
-
color="neutral"
|
|
203
|
-
variant="subtle"
|
|
204
|
-
type="submit"
|
|
205
|
-
:loading="patBusy"
|
|
206
|
-
:disabled="!patToken.trim()"
|
|
207
|
-
>
|
|
208
|
-
{{ t('auth.localMode.submit') }}
|
|
209
|
-
</UButton>
|
|
210
181
|
</div>
|
|
211
|
-
</
|
|
182
|
+
</template>
|
|
183
|
+
|
|
212
184
|
<p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
|
|
213
185
|
</div>
|
|
214
186
|
|
|
215
187
|
<div
|
|
216
|
-
v-if="
|
|
188
|
+
v-if="isLocalMode && auth.providers.password && mode !== 'forgot'"
|
|
217
189
|
class="my-4 flex items-center gap-3 text-xs text-slate-500"
|
|
218
190
|
>
|
|
219
191
|
<span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
|
package/app/stores/auth.ts
CHANGED
|
@@ -31,6 +31,14 @@ export const useAuthStore = defineStore(
|
|
|
31
31
|
* setup banner). Null on every other facade.
|
|
32
32
|
*/
|
|
33
33
|
const localMode = ref<LocalModeConfig | null>(null)
|
|
34
|
+
/**
|
|
35
|
+
* Local mode only: the source-control provider the user last chose to sign in with
|
|
36
|
+
* (its PAT lives server-side in env — this is just the non-secret choice). Persisted, so
|
|
37
|
+
* on a later load we silently re-mint a session from that env PAT without showing the
|
|
38
|
+
* login screen. Set on an explicit sign-in, cleared on logout / 401 (so logout actually
|
|
39
|
+
* signs out — no auto re-login loop).
|
|
40
|
+
*/
|
|
41
|
+
const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
|
|
34
42
|
/** True once the initial auth handshake has settled. */
|
|
35
43
|
const ready = ref(false)
|
|
36
44
|
|
|
@@ -83,6 +91,25 @@ export const useAuthStore = defineStore(
|
|
|
83
91
|
}
|
|
84
92
|
if (!user.value) token.value = null
|
|
85
93
|
}
|
|
94
|
+
|
|
95
|
+
// Local mode: if no live session resolved but the user previously signed in with a
|
|
96
|
+
// configured env PAT, silently re-mint a session from it — so an expired/rotated token
|
|
97
|
+
// never forces the login screen again. The token itself stays server-side; we only
|
|
98
|
+
// remembered the provider choice. Guard on the provider STILL being configured (PAT could
|
|
99
|
+
// have been removed) and clear the choice on failure so we fall back to the login screen
|
|
100
|
+
// instead of looping.
|
|
101
|
+
if (
|
|
102
|
+
localMode.value?.enabled === true &&
|
|
103
|
+
user.value === null &&
|
|
104
|
+
autoLoginProvider.value &&
|
|
105
|
+
localMode.value.patLogin?.configured.includes(autoLoginProvider.value)
|
|
106
|
+
) {
|
|
107
|
+
try {
|
|
108
|
+
await patLogin({ provider: autoLoginProvider.value })
|
|
109
|
+
} catch {
|
|
110
|
+
autoLoginProvider.value = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
86
113
|
// An already-signed-in user who followed an invite link redeems it here (a
|
|
87
114
|
// brand-new user redeems it server-side during signup/OAuth instead).
|
|
88
115
|
if (user.value) await maybeAcceptInvite()
|
|
@@ -153,6 +180,8 @@ export const useAuthStore = defineStore(
|
|
|
153
180
|
*/
|
|
154
181
|
async function patLogin(body: { provider: 'github' | 'gitlab'; token?: string }) {
|
|
155
182
|
applySession(await api.patLogin(body))
|
|
183
|
+
// Remember the choice so a later load re-mints the session from the env PAT silently.
|
|
184
|
+
autoLoginProvider.value = body.provider
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
/** Request a password-reset link by email (always resolves; never reveals existence). */
|
|
@@ -170,9 +199,18 @@ export const useAuthStore = defineStore(
|
|
|
170
199
|
api.logout().catch(() => {})
|
|
171
200
|
token.value = null
|
|
172
201
|
user.value = null
|
|
202
|
+
// Forget the remembered provider so logout sticks (otherwise bootstrap would
|
|
203
|
+
// immediately re-mint a session from the env PAT).
|
|
204
|
+
autoLoginProvider.value = null
|
|
173
205
|
}
|
|
174
206
|
|
|
175
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Called by the API client when a request comes back 401. Drops the dead session but KEEPS
|
|
209
|
+
* the remembered provider (unlike logout): a 401 from an expired/rotated token or a
|
|
210
|
+
* transient blip should let the next load silently re-mint from the env PAT, not force the
|
|
211
|
+
* login screen. The guarded re-mint in `bootstrap` clears the choice itself if it genuinely
|
|
212
|
+
* fails (PAT removed/revoked), so there's no re-login loop.
|
|
213
|
+
*/
|
|
176
214
|
function handleUnauthorized() {
|
|
177
215
|
token.value = null
|
|
178
216
|
user.value = null
|
|
@@ -184,6 +222,7 @@ export const useAuthStore = defineStore(
|
|
|
184
222
|
required,
|
|
185
223
|
providers,
|
|
186
224
|
localMode,
|
|
225
|
+
autoLoginProvider,
|
|
187
226
|
ready,
|
|
188
227
|
isAuthenticated,
|
|
189
228
|
needsLogin,
|
|
@@ -199,5 +238,5 @@ export const useAuthStore = defineStore(
|
|
|
199
238
|
handleUnauthorized,
|
|
200
239
|
}
|
|
201
240
|
},
|
|
202
|
-
{ persist: { pick: ['token'] } },
|
|
241
|
+
{ persist: { pick: ['token', 'autoLoginProvider'] } },
|
|
203
242
|
)
|
package/i18n/locales/en.json
CHANGED
|
@@ -752,13 +752,12 @@
|
|
|
752
752
|
"loading": "Loading…"
|
|
753
753
|
},
|
|
754
754
|
"localMode": {
|
|
755
|
-
"
|
|
756
|
-
"
|
|
757
|
-
"
|
|
755
|
+
"continueWithConfigured": "Sign in with configured {provider} PAT",
|
|
756
|
+
"noPatTitle": "No source-control token configured",
|
|
757
|
+
"noPatBody": "Set GITHUB_PAT or GITLAB_PAT in your .env to sign in with a personal access token, then restart the server.",
|
|
758
758
|
"createToken": "Create a {provider} token ↗",
|
|
759
|
-
"submit": "Sign in",
|
|
760
759
|
"orDivider": "or",
|
|
761
|
-
"failed": "Sign-in failed. Check the token and try again."
|
|
760
|
+
"failed": "Sign-in failed. Check that the configured token is valid and try again."
|
|
762
761
|
},
|
|
763
762
|
"signInRequired": {
|
|
764
763
|
"personalSubscriptions": "Personal subscriptions are saved to your account, but this deployment runs without sign-in, so they can't be stored here. Use the workspace pool or a provider API key instead.",
|
package/i18n/locales/es.json
CHANGED
|
@@ -716,13 +716,12 @@
|
|
|
716
716
|
"loading": "Cargando…"
|
|
717
717
|
},
|
|
718
718
|
"localMode": {
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
719
|
+
"continueWithConfigured": "Inicia sesión con el PAT de {provider} configurado",
|
|
720
|
+
"noPatTitle": "No hay ningún token de control de versiones configurado",
|
|
721
|
+
"noPatBody": "Define GITHUB_PAT o GITLAB_PAT en tu .env para iniciar sesión con un token de acceso personal y reinicia el servidor.",
|
|
722
722
|
"createToken": "Crear un token de {provider} ↗",
|
|
723
|
-
"submit": "Iniciar sesión",
|
|
724
723
|
"orDivider": "o",
|
|
725
|
-
"failed": "Error al iniciar sesión. Comprueba el token e inténtalo de nuevo."
|
|
724
|
+
"failed": "Error al iniciar sesión. Comprueba que el token configurado sea válido e inténtalo de nuevo."
|
|
726
725
|
},
|
|
727
726
|
"signInRequired": {
|
|
728
727
|
"personalSubscriptions": "Las suscripciones personales se guardan en tu cuenta, pero este despliegue se ejecuta sin inicio de sesión, así que no se pueden almacenar aquí. Usa el grupo del espacio de trabajo o una clave de API de proveedor.",
|
package/i18n/locales/fr.json
CHANGED
|
@@ -716,13 +716,12 @@
|
|
|
716
716
|
"loading": "Chargement…"
|
|
717
717
|
},
|
|
718
718
|
"localMode": {
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
719
|
+
"continueWithConfigured": "Se connecter avec le PAT {provider} configuré",
|
|
720
|
+
"noPatTitle": "Aucun jeton de gestion de versions configuré",
|
|
721
|
+
"noPatBody": "Définissez GITHUB_PAT ou GITLAB_PAT dans votre .env pour vous connecter avec un jeton d'accès personnel, puis redémarrez le serveur.",
|
|
722
722
|
"createToken": "Créer un jeton {provider} ↗",
|
|
723
|
-
"submit": "Se connecter",
|
|
724
723
|
"orDivider": "ou",
|
|
725
|
-
"failed": "Échec de la connexion. Vérifiez le jeton et réessayez."
|
|
724
|
+
"failed": "Échec de la connexion. Vérifiez que le jeton configuré est valide et réessayez."
|
|
726
725
|
},
|
|
727
726
|
"signInRequired": {
|
|
728
727
|
"personalSubscriptions": "Les abonnements personnels sont enregistrés sur votre compte, mais ce déploiement fonctionne sans connexion, ils ne peuvent donc pas être stockés ici. Utilisez le pool de l'espace de travail ou une clé d'API de fournisseur.",
|
package/i18n/locales/pl.json
CHANGED
|
@@ -716,13 +716,12 @@
|
|
|
716
716
|
"loading": "Ładowanie…"
|
|
717
717
|
},
|
|
718
718
|
"localMode": {
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
719
|
+
"continueWithConfigured": "Zaloguj się skonfigurowanym tokenem PAT {provider}",
|
|
720
|
+
"noPatTitle": "Nie skonfigurowano tokenu systemu kontroli wersji",
|
|
721
|
+
"noPatBody": "Ustaw GITHUB_PAT lub GITLAB_PAT w pliku .env, aby zalogować się za pomocą osobistego tokenu dostępu, a następnie zrestartuj serwer.",
|
|
722
722
|
"createToken": "Utwórz token {provider} ↗",
|
|
723
|
-
"submit": "Zaloguj się",
|
|
724
723
|
"orDivider": "lub",
|
|
725
|
-
"failed": "Logowanie nie powiodło się.
|
|
724
|
+
"failed": "Logowanie nie powiodło się. Sprawdź, czy skonfigurowany token jest prawidłowy, i spróbuj ponownie."
|
|
726
725
|
},
|
|
727
726
|
"signInRequired": {
|
|
728
727
|
"personalSubscriptions": "Subskrypcje osobiste są zapisywane na Twoim koncie, ale to wdrożenie działa bez logowania, więc nie można ich tutaj przechowywać. Użyj puli obszaru roboczego lub klucza API dostawcy.",
|
package/i18n/locales/uk.json
CHANGED
|
@@ -716,13 +716,12 @@
|
|
|
716
716
|
"loading": "Завантаження…"
|
|
717
717
|
},
|
|
718
718
|
"localMode": {
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
719
|
+
"continueWithConfigured": "Увійти за допомогою налаштованого PAT {provider}",
|
|
720
|
+
"noPatTitle": "Токен системи контролю версій не налаштовано",
|
|
721
|
+
"noPatBody": "Установіть GITHUB_PAT або GITLAB_PAT у файлі .env, щоб увійти за допомогою особистого токена доступу, потім перезапустіть сервер.",
|
|
722
722
|
"createToken": "Створити токен {provider} ↗",
|
|
723
|
-
"submit": "Увійти",
|
|
724
723
|
"orDivider": "або",
|
|
725
|
-
"failed": "Не вдалося увійти.
|
|
724
|
+
"failed": "Не вдалося увійти. Перевірте, що налаштований токен дійсний, і спробуйте ще раз."
|
|
726
725
|
},
|
|
727
726
|
"signInRequired": {
|
|
728
727
|
"personalSubscriptions": "Особисті підписки зберігаються у вашому обліковому записі, але це розгортання працює без входу, тому їх не можна зберегти тут. Скористайтеся пулом робочого простору або ключем API постачальника.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.7",
|
|
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.45.
|
|
37
|
+
"@cat-factory/contracts": "0.45.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|