@cat-factory/app 0.46.12 → 0.47.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/app/components/auth/AuthGate.vue +1 -1
- package/app/components/auth/LoginScreen.vue +131 -1
- package/app/components/layout/AccountDeploymentSettings.vue +232 -5
- package/app/components/providers/ApiKeysSection.vue +27 -3
- package/app/components/providers/PersonalSubscriptionSection.vue +17 -2
- package/app/components/providers/SignInRequiredNotice.vue +19 -0
- package/app/components/settings/AccountSettingsPanel.vue +9 -6
- package/app/components/settings/IssueTrackerPanel.vue +114 -53
- package/app/components/settings/LocalModeSettingsPanel.vue +60 -28
- package/app/components/settings/LocalModelEndpointsPanel.vue +72 -27
- package/app/components/settings/MergeThresholdsPanel.vue +93 -45
- package/app/components/settings/ModelConfigurationPanel.vue +62 -36
- package/app/components/settings/ObservabilityConnectionPanel.vue +65 -35
- package/app/components/settings/OpenRouterCatalogPanel.vue +70 -40
- package/app/components/settings/ProviderConnectionPanel.vue +115 -61
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +9 -12
- package/app/components/settings/UserSecretsSection.vue +37 -18
- package/app/components/settings/WorkspaceSettingsPanel.vue +114 -62
- package/app/composables/api/auth.ts +7 -0
- package/app/stores/auth.ts +25 -1
- package/app/types/accountSettings.ts +4 -0
- package/i18n/locales/en.json +496 -0
- package/i18n/locales/es.json +490 -0
- package/i18n/locales/fr.json +490 -0
- package/i18n/locales/pl.json +490 -0
- package/i18n/locales/uk.json +490 -0
- package/package.json +2 -2
|
@@ -1,10 +1,74 @@
|
|
|
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()
|
|
6
6
|
const { t } = useI18n()
|
|
7
7
|
|
|
8
|
+
// Local-mode source-control PAT login. GitHub/GitLab are brand names (kept verbatim across
|
|
9
|
+
// locales), as are the token-settings URLs, so they're inline constants rather than catalog
|
|
10
|
+
// keys — same convention as the provider descriptors in ApiKeysSection. The actual link
|
|
11
|
+
// prefers the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the
|
|
12
|
+
// fallback when it's absent.
|
|
13
|
+
type PatProvider = 'github' | 'gitlab'
|
|
14
|
+
const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
|
|
15
|
+
const PROVIDER_ICONS: Record<PatProvider, string> = {
|
|
16
|
+
github: 'i-lucide-github',
|
|
17
|
+
gitlab: 'i-lucide-gitlab',
|
|
18
|
+
}
|
|
19
|
+
// Fallback token-creation pages, used only if the server didn't advertise a deep link.
|
|
20
|
+
const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
|
|
21
|
+
github: 'https://github.com/settings/tokens/new',
|
|
22
|
+
gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const patLoginCfg = computed(() => auth.localMode?.patLogin)
|
|
26
|
+
const configuredProviders = computed<PatProvider[]>(
|
|
27
|
+
() => (patLoginCfg.value?.configured ?? []) as PatProvider[],
|
|
28
|
+
)
|
|
29
|
+
const availableProviders = computed<PatProvider[]>(
|
|
30
|
+
() => (patLoginCfg.value?.available ?? []) as PatProvider[],
|
|
31
|
+
)
|
|
32
|
+
const showLocalLogin = computed(() => availableProviders.value.length > 0)
|
|
33
|
+
|
|
34
|
+
const patProvider = ref<PatProvider>('github')
|
|
35
|
+
const patToken = ref('')
|
|
36
|
+
const patBusy = ref(false)
|
|
37
|
+
const patError = ref<string | null>(null)
|
|
38
|
+
|
|
39
|
+
// Keep the picker on an actually-available provider.
|
|
40
|
+
watch(
|
|
41
|
+
availableProviders,
|
|
42
|
+
(list) => {
|
|
43
|
+
if (list.length && !list.includes(patProvider.value)) patProvider.value = list[0]!
|
|
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
|
+
)
|
|
57
|
+
|
|
58
|
+
/** One-click (configured PAT) or pasted-token sign-in; reloads so the app boots signed in. */
|
|
59
|
+
async function submitPat(provider: PatProvider, token?: string) {
|
|
60
|
+
patError.value = null
|
|
61
|
+
patBusy.value = true
|
|
62
|
+
try {
|
|
63
|
+
await auth.patLogin(token ? { provider, token } : { provider })
|
|
64
|
+
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
65
|
+
} catch (e) {
|
|
66
|
+
patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
|
|
67
|
+
} finally {
|
|
68
|
+
patBusy.value = false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
8
72
|
// An invite token may ride in on the URL (?invite=…) — it flows through the OAuth
|
|
9
73
|
// redirect and the password signup so a brand-new user can join the org on first login.
|
|
10
74
|
const invite = computed(() => {
|
|
@@ -90,6 +154,72 @@ const showOAuthDivider = computed(
|
|
|
90
154
|
</p>
|
|
91
155
|
</div>
|
|
92
156
|
|
|
157
|
+
<!-- Local mode: sign in with a source-control PAT (no OAuth round-trip needed) -->
|
|
158
|
+
<div v-if="showLocalLogin && mode !== 'forgot'" class="space-y-3">
|
|
159
|
+
<!-- One-click: a PAT is already configured server-side -->
|
|
160
|
+
<UButton
|
|
161
|
+
v-for="p in configuredProviders"
|
|
162
|
+
:key="p"
|
|
163
|
+
block
|
|
164
|
+
size="lg"
|
|
165
|
+
color="primary"
|
|
166
|
+
:icon="PROVIDER_ICONS[p]"
|
|
167
|
+
:loading="patBusy"
|
|
168
|
+
@click="submitPat(p)"
|
|
169
|
+
>
|
|
170
|
+
{{ t('auth.localMode.continueWith', { provider: PROVIDER_LABELS[p] }) }}
|
|
171
|
+
</UButton>
|
|
172
|
+
|
|
173
|
+
<!-- Enter a PAT inline -->
|
|
174
|
+
<form class="space-y-2" @submit.prevent="submitPat(patProvider, patToken.trim())">
|
|
175
|
+
<p class="text-xs font-medium text-slate-400">{{ t('auth.localMode.enterPatTitle') }}</p>
|
|
176
|
+
<USelect
|
|
177
|
+
v-if="patProviderItems.length > 1"
|
|
178
|
+
v-model="patProvider"
|
|
179
|
+
:items="patProviderItems"
|
|
180
|
+
size="lg"
|
|
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"
|
|
190
|
+
/>
|
|
191
|
+
<div class="flex items-center justify-between gap-2">
|
|
192
|
+
<a
|
|
193
|
+
:href="tokenCreateUrl"
|
|
194
|
+
target="_blank"
|
|
195
|
+
rel="noopener noreferrer"
|
|
196
|
+
class="text-xs text-indigo-400 hover:underline"
|
|
197
|
+
>
|
|
198
|
+
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[patProvider] }) }}
|
|
199
|
+
</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
|
+
</div>
|
|
211
|
+
</form>
|
|
212
|
+
<p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div
|
|
216
|
+
v-if="showLocalLogin && auth.providers.password && mode !== 'forgot'"
|
|
217
|
+
class="my-4 flex items-center gap-3 text-xs text-slate-500"
|
|
218
|
+
>
|
|
219
|
+
<span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
|
|
220
|
+
<span class="h-px flex-1 bg-slate-800" />
|
|
221
|
+
</div>
|
|
222
|
+
|
|
93
223
|
<!-- OAuth providers -->
|
|
94
224
|
<div v-if="mode !== 'forgot'" class="space-y-2">
|
|
95
225
|
<UButton
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, reactive, ref } from 'vue'
|
|
3
|
+
import type { ContentStorageBackend, ContentStorageConfig } from '~/types/accountSettings'
|
|
3
4
|
|
|
4
5
|
// Deployment integration secrets for an account (admin only): the Slack app OAuth
|
|
5
|
-
// credentials
|
|
6
|
-
//
|
|
7
|
-
// ever shows whether each integration is
|
|
8
|
-
// blank inputs leave a configured secret
|
|
9
|
-
// wired (no ENCRYPTION_KEY).
|
|
6
|
+
// credentials, the container web-search upstream keys, and the binary-artifact (screenshot)
|
|
7
|
+
// content-storage backend — all moved out of env into the per-account settings store, sealed
|
|
8
|
+
// at rest. Secrets are write-only: the panel only ever shows whether each integration is
|
|
9
|
+
// configured (the `summary`), never the values; blank inputs leave a configured secret
|
|
10
|
+
// unchanged. Hidden when the settings store isn't wired (no ENCRYPTION_KEY).
|
|
10
11
|
const props = defineProps<{ accountId: string }>()
|
|
11
12
|
|
|
12
13
|
const store = useAccountSettingsStore()
|
|
@@ -20,9 +21,54 @@ const savingWeb = ref(false)
|
|
|
20
21
|
|
|
21
22
|
const summary = computed(() => store.view?.summary ?? null)
|
|
22
23
|
|
|
24
|
+
// ---- Content storage (binary artifacts / screenshots) --------------------
|
|
25
|
+
// Exhaustive enum→key map (drift guard tier 2): every backend resolves to a static literal
|
|
26
|
+
// `t()` key, so adding a backend without a label fails the typecheck on this Record.
|
|
27
|
+
const contentBackendLabels = computed<Record<ContentStorageBackend, string>>(() => ({
|
|
28
|
+
off: t('layout.accountDeployment.contentStorage.backends.off'),
|
|
29
|
+
fs: t('layout.accountDeployment.contentStorage.backends.fs'),
|
|
30
|
+
s3: t('layout.accountDeployment.contentStorage.backends.s3'),
|
|
31
|
+
r2: t('layout.accountDeployment.contentStorage.backends.r2'),
|
|
32
|
+
db: t('layout.accountDeployment.contentStorage.backends.db'),
|
|
33
|
+
}))
|
|
34
|
+
const storageCapability = computed(() => store.view?.contentStorageCapability ?? null)
|
|
35
|
+
const storageSummary = computed(() => summary.value?.contentStorage ?? null)
|
|
36
|
+
const backendItems = computed(() =>
|
|
37
|
+
(storageCapability.value?.supportedBackends ?? []).map((b) => ({
|
|
38
|
+
label: contentBackendLabels.value[b],
|
|
39
|
+
value: b,
|
|
40
|
+
})),
|
|
41
|
+
)
|
|
42
|
+
const csBackend = ref<ContentStorageBackend>('off')
|
|
43
|
+
const cs = reactive({
|
|
44
|
+
basePath: '',
|
|
45
|
+
region: '',
|
|
46
|
+
bucket: '',
|
|
47
|
+
prefix: '',
|
|
48
|
+
endpoint: '',
|
|
49
|
+
forcePathStyle: false,
|
|
50
|
+
accessKeyId: '',
|
|
51
|
+
secretAccessKey: '',
|
|
52
|
+
})
|
|
53
|
+
const savingStorage = ref(false)
|
|
54
|
+
|
|
55
|
+
function hydrateStorage() {
|
|
56
|
+
const cfg = store.view?.config?.contentStorage
|
|
57
|
+
csBackend.value = cfg?.backend ?? storageCapability.value?.defaultBackend ?? 'off'
|
|
58
|
+
cs.basePath = cfg?.fs?.basePath ?? ''
|
|
59
|
+
cs.region = cfg?.s3?.region ?? ''
|
|
60
|
+
cs.bucket = cfg?.s3?.bucket ?? ''
|
|
61
|
+
cs.prefix = cfg?.s3?.prefix ?? ''
|
|
62
|
+
cs.endpoint = cfg?.s3?.endpoint ?? ''
|
|
63
|
+
cs.forcePathStyle = cfg?.s3?.forcePathStyle ?? false
|
|
64
|
+
cs.accessKeyId = ''
|
|
65
|
+
cs.secretAccessKey = ''
|
|
66
|
+
}
|
|
67
|
+
|
|
23
68
|
onMounted(async () => {
|
|
24
69
|
try {
|
|
25
70
|
await store.load(props.accountId)
|
|
71
|
+
hydrateStorage()
|
|
26
72
|
} catch (e) {
|
|
27
73
|
toast.add({
|
|
28
74
|
title: t('layout.accountDeployment.loadFailed'),
|
|
@@ -33,6 +79,72 @@ onMounted(async () => {
|
|
|
33
79
|
}
|
|
34
80
|
})
|
|
35
81
|
|
|
82
|
+
async function saveStorage() {
|
|
83
|
+
const backend = csBackend.value
|
|
84
|
+
const config: ContentStorageConfig = { backend }
|
|
85
|
+
if (backend === 'fs' && cs.basePath.trim()) {
|
|
86
|
+
config.fs = { basePath: cs.basePath.trim() }
|
|
87
|
+
}
|
|
88
|
+
if (backend === 's3') {
|
|
89
|
+
if (!cs.region.trim() || !cs.bucket.trim()) {
|
|
90
|
+
toast.add({
|
|
91
|
+
title: t('layout.accountDeployment.contentStorage.regionBucketValidation'),
|
|
92
|
+
color: 'error',
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
config.s3 = {
|
|
97
|
+
region: cs.region.trim(),
|
|
98
|
+
bucket: cs.bucket.trim(),
|
|
99
|
+
...(cs.prefix.trim() ? { prefix: cs.prefix.trim() } : {}),
|
|
100
|
+
...(cs.endpoint.trim() ? { endpoint: cs.endpoint.trim() } : {}),
|
|
101
|
+
...(cs.forcePathStyle ? { forcePathStyle: true } : {}),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const input: Parameters<typeof store.save>[1] = { config: { contentStorage: config } }
|
|
105
|
+
if (backend === 's3') {
|
|
106
|
+
const id = cs.accessKeyId.trim()
|
|
107
|
+
const key = cs.secretAccessKey.trim()
|
|
108
|
+
if (id && key) {
|
|
109
|
+
input.secrets = { s3: { accessKeyId: id, secretAccessKey: key } }
|
|
110
|
+
} else if (id || key) {
|
|
111
|
+
toast.add({
|
|
112
|
+
title: t('layout.accountDeployment.contentStorage.bothKeysValidation'),
|
|
113
|
+
color: 'error',
|
|
114
|
+
})
|
|
115
|
+
return
|
|
116
|
+
} else if (!storageSummary.value?.s3CredentialsConfigured) {
|
|
117
|
+
toast.add({
|
|
118
|
+
title: t('layout.accountDeployment.contentStorage.keysValidation'),
|
|
119
|
+
color: 'error',
|
|
120
|
+
})
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
// else: keys already stored and none re-entered → leave them unchanged.
|
|
124
|
+
} else {
|
|
125
|
+
// Switching off S3: drop any stored S3 credentials.
|
|
126
|
+
input.secrets = { s3: null }
|
|
127
|
+
}
|
|
128
|
+
savingStorage.value = true
|
|
129
|
+
try {
|
|
130
|
+
await store.save(props.accountId, input)
|
|
131
|
+
hydrateStorage()
|
|
132
|
+
toast.add({
|
|
133
|
+
title: t('layout.accountDeployment.contentStorage.saved'),
|
|
134
|
+
icon: 'i-lucide-check',
|
|
135
|
+
color: 'success',
|
|
136
|
+
})
|
|
137
|
+
} catch (e) {
|
|
138
|
+
toast.add({
|
|
139
|
+
title: t('layout.accountDeployment.contentStorage.saveFailed'),
|
|
140
|
+
description: e instanceof Error ? e.message : String(e),
|
|
141
|
+
color: 'error',
|
|
142
|
+
})
|
|
143
|
+
} finally {
|
|
144
|
+
savingStorage.value = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
36
148
|
async function saveSlack() {
|
|
37
149
|
if (!slack.clientId.trim() || !slack.clientSecret.trim() || !slack.redirectUrl.trim()) {
|
|
38
150
|
toast.add({ title: t('layout.accountDeployment.slack.validation'), color: 'error' })
|
|
@@ -275,5 +387,120 @@ async function clearWeb() {
|
|
|
275
387
|
</UButton>
|
|
276
388
|
</div>
|
|
277
389
|
</section>
|
|
390
|
+
|
|
391
|
+
<!-- Content storage (binary artifacts / screenshots) -->
|
|
392
|
+
<section v-if="storageCapability" class="space-y-2 border-t border-slate-800 pt-6">
|
|
393
|
+
<div class="flex items-center gap-2">
|
|
394
|
+
<h4 class="text-sm font-semibold text-slate-200">
|
|
395
|
+
{{ t('layout.accountDeployment.contentStorage.title') }}
|
|
396
|
+
</h4>
|
|
397
|
+
<UBadge
|
|
398
|
+
:color="
|
|
399
|
+
storageSummary?.backend && storageSummary.backend !== 'off' ? 'success' : 'neutral'
|
|
400
|
+
"
|
|
401
|
+
variant="subtle"
|
|
402
|
+
size="xs"
|
|
403
|
+
>
|
|
404
|
+
{{
|
|
405
|
+
storageSummary?.backend
|
|
406
|
+
? contentBackendLabels[storageSummary.backend]
|
|
407
|
+
: t('layout.accountDeployment.contentStorage.default', {
|
|
408
|
+
backend: contentBackendLabels[storageCapability.defaultBackend],
|
|
409
|
+
})
|
|
410
|
+
}}
|
|
411
|
+
</UBadge>
|
|
412
|
+
</div>
|
|
413
|
+
<p class="text-[11px] text-slate-400">
|
|
414
|
+
{{ t('layout.accountDeployment.contentStorage.description') }}
|
|
415
|
+
</p>
|
|
416
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
417
|
+
<USelect v-model="csBackend" :items="backendItems" value-key="value" size="sm" />
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<!-- Filesystem -->
|
|
421
|
+
<div v-if="csBackend === 'fs'" class="grid grid-cols-1 gap-2">
|
|
422
|
+
<UInput
|
|
423
|
+
v-model="cs.basePath"
|
|
424
|
+
:placeholder="t('layout.accountDeployment.contentStorage.basePath')"
|
|
425
|
+
size="sm"
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- S3 / S3-compatible -->
|
|
430
|
+
<template v-if="csBackend === 's3'">
|
|
431
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
432
|
+
<UInput
|
|
433
|
+
v-model="cs.region"
|
|
434
|
+
:placeholder="t('layout.accountDeployment.contentStorage.region')"
|
|
435
|
+
size="sm"
|
|
436
|
+
/>
|
|
437
|
+
<UInput
|
|
438
|
+
v-model="cs.bucket"
|
|
439
|
+
:placeholder="t('layout.accountDeployment.contentStorage.bucket')"
|
|
440
|
+
size="sm"
|
|
441
|
+
/>
|
|
442
|
+
<UInput
|
|
443
|
+
v-model="cs.prefix"
|
|
444
|
+
:placeholder="t('layout.accountDeployment.contentStorage.prefix')"
|
|
445
|
+
size="sm"
|
|
446
|
+
/>
|
|
447
|
+
<UInput
|
|
448
|
+
v-model="cs.endpoint"
|
|
449
|
+
:placeholder="t('layout.accountDeployment.contentStorage.endpoint')"
|
|
450
|
+
size="sm"
|
|
451
|
+
/>
|
|
452
|
+
</div>
|
|
453
|
+
<UCheckbox
|
|
454
|
+
v-model="cs.forcePathStyle"
|
|
455
|
+
:label="t('layout.accountDeployment.contentStorage.forcePathStyle')"
|
|
456
|
+
size="sm"
|
|
457
|
+
/>
|
|
458
|
+
<div class="flex items-center gap-2">
|
|
459
|
+
<span class="text-[11px] text-slate-400">
|
|
460
|
+
{{ t('layout.accountDeployment.contentStorage.accessKeys') }}
|
|
461
|
+
</span>
|
|
462
|
+
<UBadge
|
|
463
|
+
:color="storageSummary?.s3CredentialsConfigured ? 'success' : 'neutral'"
|
|
464
|
+
variant="subtle"
|
|
465
|
+
size="xs"
|
|
466
|
+
>
|
|
467
|
+
{{
|
|
468
|
+
storageSummary?.s3CredentialsConfigured
|
|
469
|
+
? t('layout.accountDeployment.configured')
|
|
470
|
+
: t('layout.accountDeployment.notSet')
|
|
471
|
+
}}
|
|
472
|
+
</UBadge>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
475
|
+
<UInput
|
|
476
|
+
v-model="cs.accessKeyId"
|
|
477
|
+
type="password"
|
|
478
|
+
:placeholder="t('layout.accountDeployment.contentStorage.accessKeyId')"
|
|
479
|
+
size="sm"
|
|
480
|
+
/>
|
|
481
|
+
<UInput
|
|
482
|
+
v-model="cs.secretAccessKey"
|
|
483
|
+
type="password"
|
|
484
|
+
:placeholder="t('layout.accountDeployment.contentStorage.secretAccessKey')"
|
|
485
|
+
size="sm"
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
<p class="text-[11px] text-slate-400">
|
|
489
|
+
{{ t('layout.accountDeployment.contentStorage.keysHint') }}
|
|
490
|
+
</p>
|
|
491
|
+
</template>
|
|
492
|
+
|
|
493
|
+
<div class="flex gap-2">
|
|
494
|
+
<UButton
|
|
495
|
+
color="primary"
|
|
496
|
+
size="xs"
|
|
497
|
+
icon="i-lucide-save"
|
|
498
|
+
:loading="savingStorage"
|
|
499
|
+
@click="saveStorage"
|
|
500
|
+
>
|
|
501
|
+
{{ t('common.save') }}
|
|
502
|
+
</UButton>
|
|
503
|
+
</div>
|
|
504
|
+
</section>
|
|
278
505
|
</div>
|
|
279
506
|
</template>
|
|
@@ -24,11 +24,18 @@ const props = withDefaults(defineProps<{ accountId?: string; category?: 'direct'
|
|
|
24
24
|
const workspace = useWorkspaceStore()
|
|
25
25
|
const keys = useApiKeysStore()
|
|
26
26
|
const models = useModelsStore()
|
|
27
|
+
const auth = useAuthStore()
|
|
27
28
|
const toast = useToast()
|
|
29
|
+
const { t } = useI18n()
|
|
28
30
|
|
|
29
31
|
/** Account-wide mode (single account scope) vs the default workspace/user toggle. */
|
|
30
32
|
const isAccount = computed(() => !!props.accountId)
|
|
31
33
|
|
|
34
|
+
// "My keys" (user scope) are stored per-user, so they need a signed-in user. Block just
|
|
35
|
+
// that scope when there's none (a deployment without sign-in); workspace/account keys are
|
|
36
|
+
// unaffected. The scope toggle stays enabled so the user can switch back to a shared scope.
|
|
37
|
+
const needsSignIn = computed(() => !isAccount.value && scope.value === 'user' && !auth.user)
|
|
38
|
+
|
|
32
39
|
interface ProviderMeta {
|
|
33
40
|
value: ApiKeyProvider
|
|
34
41
|
label: string
|
|
@@ -257,11 +264,17 @@ async function remove(k: ApiKey) {
|
|
|
257
264
|
<USelect
|
|
258
265
|
v-model="provider"
|
|
259
266
|
:items="PROVIDERS.map((p) => ({ label: p.label, value: p.value }))"
|
|
267
|
+
:disabled="needsSignIn"
|
|
260
268
|
class="w-64"
|
|
261
269
|
/>
|
|
262
270
|
</UFormField>
|
|
263
271
|
</div>
|
|
264
272
|
|
|
273
|
+
<ProvidersSignInRequiredNotice
|
|
274
|
+
v-if="needsSignIn"
|
|
275
|
+
:message="t('auth.signInRequired.userApiKeys')"
|
|
276
|
+
/>
|
|
277
|
+
|
|
265
278
|
<!-- where to get the key -->
|
|
266
279
|
<ol
|
|
267
280
|
class="list-decimal space-y-1.5 rounded-lg border border-slate-700 bg-slate-900/60 p-4 pl-8 text-sm text-slate-300"
|
|
@@ -290,13 +303,24 @@ async function remove(k: ApiKey) {
|
|
|
290
303
|
<!-- add form -->
|
|
291
304
|
<div class="space-y-2">
|
|
292
305
|
<UFormField label="Label (optional)">
|
|
293
|
-
<UInput v-model="label" placeholder="e.g. team key" />
|
|
306
|
+
<UInput v-model="label" :disabled="needsSignIn" placeholder="e.g. team key" />
|
|
294
307
|
</UFormField>
|
|
295
308
|
<UFormField label="API key">
|
|
296
|
-
<UTextarea
|
|
309
|
+
<UTextarea
|
|
310
|
+
v-model="key"
|
|
311
|
+
:rows="2"
|
|
312
|
+
:disabled="needsSignIn"
|
|
313
|
+
placeholder="paste the API key"
|
|
314
|
+
class="font-mono"
|
|
315
|
+
/>
|
|
297
316
|
</UFormField>
|
|
298
317
|
<div class="flex justify-end">
|
|
299
|
-
<UButton
|
|
318
|
+
<UButton
|
|
319
|
+
:loading="busy"
|
|
320
|
+
:disabled="needsSignIn || !key.trim()"
|
|
321
|
+
icon="i-lucide-plus"
|
|
322
|
+
@click="add()"
|
|
323
|
+
>
|
|
300
324
|
Connect
|
|
301
325
|
</UButton>
|
|
302
326
|
</div>
|
|
@@ -8,9 +8,15 @@ import { computed, onMounted, ref } from 'vue'
|
|
|
8
8
|
import type { SubscriptionVendor } from '~/types/domain'
|
|
9
9
|
|
|
10
10
|
const personal = usePersonalSubscriptionsStore()
|
|
11
|
+
const auth = useAuthStore()
|
|
11
12
|
const toast = useToast()
|
|
12
13
|
const { t, d } = useI18n()
|
|
13
14
|
|
|
15
|
+
// Personal subscriptions are stored per-user, so they need a signed-in user. When there
|
|
16
|
+
// isn't one (a deployment running without sign-in), block the form so the user doesn't
|
|
17
|
+
// enter a token + password that can't be saved.
|
|
18
|
+
const needsSignIn = computed(() => !auth.user)
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Per-vendor metadata driving the connect form + connected-row labels. Reactive to the
|
|
16
22
|
* locale (rebuilds on switch). The token placeholders for `claude`/`codex` are literal
|
|
@@ -150,6 +156,11 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
150
156
|
<p class="mt-1 text-sm text-slate-400">{{ t('personalSubscriptions.intro') }}</p>
|
|
151
157
|
</div>
|
|
152
158
|
|
|
159
|
+
<ProvidersSignInRequiredNotice
|
|
160
|
+
v-if="needsSignIn"
|
|
161
|
+
:message="t('auth.signInRequired.personalSubscriptions')"
|
|
162
|
+
/>
|
|
163
|
+
|
|
153
164
|
<!-- connected subscriptions -->
|
|
154
165
|
<div
|
|
155
166
|
v-for="sub in personal.subscriptions"
|
|
@@ -182,6 +193,7 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
182
193
|
<USelect
|
|
183
194
|
v-model="vendor"
|
|
184
195
|
:items="PERSONAL_VENDORS.map((m) => ({ label: m.label, value: m.value }))"
|
|
196
|
+
:disabled="needsSignIn"
|
|
185
197
|
class="w-64"
|
|
186
198
|
/>
|
|
187
199
|
</UFormField>
|
|
@@ -197,6 +209,7 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
197
209
|
<UFormField :label="t('personalSubscriptions.labelField')">
|
|
198
210
|
<UInput
|
|
199
211
|
v-model="label"
|
|
212
|
+
:disabled="needsSignIn"
|
|
200
213
|
:placeholder="t('personalSubscriptions.labelPlaceholder', { vendor: selectedMeta.label })"
|
|
201
214
|
/>
|
|
202
215
|
</UFormField>
|
|
@@ -204,6 +217,7 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
204
217
|
<UTextarea
|
|
205
218
|
v-model="token"
|
|
206
219
|
:rows="2"
|
|
220
|
+
:disabled="needsSignIn"
|
|
207
221
|
:placeholder="selectedMeta.tokenPlaceholder"
|
|
208
222
|
class="font-mono"
|
|
209
223
|
/>
|
|
@@ -213,17 +227,18 @@ async function disconnect(v: SubscriptionVendor) {
|
|
|
213
227
|
<UInput
|
|
214
228
|
v-model="password"
|
|
215
229
|
type="password"
|
|
230
|
+
:disabled="needsSignIn"
|
|
216
231
|
:placeholder="t('personalSubscriptions.passwordPlaceholder')"
|
|
217
232
|
/>
|
|
218
233
|
</UFormField>
|
|
219
234
|
<UFormField :label="t('personalSubscriptions.renewsField')">
|
|
220
|
-
<UInput v-model="expiresOn" type="date" />
|
|
235
|
+
<UInput v-model="expiresOn" type="date" :disabled="needsSignIn" />
|
|
221
236
|
</UFormField>
|
|
222
237
|
</div>
|
|
223
238
|
<div class="flex justify-end">
|
|
224
239
|
<UButton
|
|
225
240
|
:loading="busy"
|
|
226
|
-
:disabled="!token.trim() || password.length < 6"
|
|
241
|
+
:disabled="needsSignIn || !token.trim() || password.length < 6"
|
|
227
242
|
icon="i-lucide-shield-check"
|
|
228
243
|
@click="connect()"
|
|
229
244
|
>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// A guard banner for a per-USER credential form (personal subscriptions, your own API
|
|
3
|
+
// keys) when there is no signed-in user to store it against. On auth-enabled deployments
|
|
4
|
+
// and in local mode the app forces a login before these forms are reachable, so this only
|
|
5
|
+
// surfaces on a deployment running WITHOUT sign-in (auth fully disabled, non-local) — where
|
|
6
|
+
// storing would 401. It blocks the inputs and explains why, instead of letting the user
|
|
7
|
+
// type a token that can't be saved.
|
|
8
|
+
defineProps<{ message: string }>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
class="flex items-start gap-2.5 rounded-lg border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-200"
|
|
14
|
+
role="alert"
|
|
15
|
+
>
|
|
16
|
+
<UIcon name="i-lucide-lock" class="mt-0.5 h-4 w-4 shrink-0" />
|
|
17
|
+
<p>{{ message }}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import AccountTeamSettings from '~/components/layout/AccountTeamSettings.vue'
|
|
9
9
|
import AccountFragmentSettings from '~/components/layout/AccountFragmentSettings.vue'
|
|
10
10
|
|
|
11
|
+
const { t } = useI18n()
|
|
11
12
|
const ui = useUiStore()
|
|
12
13
|
const accounts = useAccountsStore()
|
|
13
14
|
|
|
@@ -23,21 +24,23 @@ const activeTab = computed({
|
|
|
23
24
|
set: (v: string) => ui.setAccountSettingsTab(v),
|
|
24
25
|
})
|
|
25
26
|
|
|
26
|
-
const tabs = [
|
|
27
|
-
{ value: 'team', label: '
|
|
27
|
+
const tabs = computed(() => [
|
|
28
|
+
{ value: 'team', label: t('settings.account.tabs.team'), icon: 'i-lucide-users', slot: 'team' },
|
|
28
29
|
{
|
|
29
30
|
value: 'fragments',
|
|
30
|
-
label: '
|
|
31
|
+
label: t('settings.account.tabs.fragments'),
|
|
31
32
|
icon: 'i-lucide-book-marked',
|
|
32
33
|
slot: 'fragments',
|
|
33
34
|
},
|
|
34
|
-
]
|
|
35
|
+
])
|
|
35
36
|
</script>
|
|
36
37
|
|
|
37
38
|
<template>
|
|
38
|
-
<UModal v-model:open="open" title="
|
|
39
|
+
<UModal v-model:open="open" :title="t('settings.account.title')" :ui="{ content: 'max-w-3xl' }">
|
|
39
40
|
<template #body>
|
|
40
|
-
<p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">
|
|
41
|
+
<p v-if="!accounts.activeAccountId" class="text-sm text-slate-400">
|
|
42
|
+
{{ t('settings.account.noAccount') }}
|
|
43
|
+
</p>
|
|
41
44
|
<UTabs
|
|
42
45
|
v-else
|
|
43
46
|
v-model="activeTab"
|