main
vue 484 lines 14.5 KB
Raw
1 <template>
2 <!-- SSO Configuration — admin only -->
3 <n-card v-if="isAdmin" title="Single Sign-On (SSO) Configuration" class="mb-4">
4 <template #header-extra>
5 <n-tag :type="ssoEnabled ? 'success' : 'default'" size="small">
6 {{ ssoEnabled ? "Enabled" : "Disabled" }}
7 </n-tag>
8 </template>
9
10 <n-spin :show="loading">
11 <n-form :model="form" label-placement="left" label-width="220">
12 <!-- Global toggle -->
13 <n-form-item label="Enable SSO">
14 <n-switch v-model:value="form.sso_enabled" />
15 <n-text class="text-secondary ml-4 text-sm">
16 When enabled, SSO login buttons will appear on the login page.
17 </n-text>
18 </n-form-item>
19
20 <n-divider />
21
22 <!-- ── Azure Entra ID ────────────────────────────────────── -->
23 <div class="mb-4 flex items-center gap-3">
24 <Icon :name="AzureIcon" :size="22" />
25 <h3 class="text-lg font-semibold">Azure Entra ID (OAuth2 / OIDC)</h3>
26 <n-button text size="small" @click="showAzureGuide = !showAzureGuide">
27 <template #icon>
28 <Icon :name="showAzureGuide ? 'carbon:chevron-up' : 'carbon:information'" />
29 </template>
30 {{ showAzureGuide ? "Hide guide" : "Setup guide" }}
31 </n-button>
32 </div>
33
34 <n-collapse-transition :show="showAzureGuide">
35 <n-alert type="info" class="mb-5" :show-icon="false">
36 <div class="text-xs">
37 <p class="mb-2 font-semibold">How to configure Azure Entra ID:</p>
38 <ol>
39 <li>
40 Go to
41 <n-button text tag="a" href="https://portal.azure.com" target="_blank" type="info">
42 portal.azure.com
43 </n-button>
44
45 <strong>Azure Active Directory</strong>
46
47 <strong>App registrations</strong>
48
49 <strong>New registration</strong>
50 </li>
51 <li>
52 Set a name (e.g.
53 <code>CoPilot SSO</code>
54 ), choose
55 <em>Accounts in this organizational directory only</em>
56 , then click
57 <strong>Register</strong>
58 .
59 </li>
60 <li>
61 Copy the
62 <strong>Application (client) ID</strong>
63 → paste as
64 <em>Client ID</em>
65 below.
66 </li>
67 <li>
68 Copy the
69 <strong>Directory (tenant) ID</strong>
70 → paste as
71 <em>Tenant ID</em>
72 below.
73 </li>
74 <li>
75 Go to
76 <strong>Certificates &amp; secrets</strong>
77
78 <strong>New client secret</strong>
79 → copy the value → paste as
80 <em>Client Secret</em>
81 below.
82 </li>
83 <li>
84 Go to
85 <strong>Authentication</strong>
86
87 <strong>Add a platform</strong>
88
89 <strong>Web</strong>
90 → set Redirect URI to:
91 <br />
92 <code>https://&lt;your-domain&gt;/api/auth/sso/azure/callback</code>
93 </li>
94 <li>
95 Under
96 <strong>Token configuration</strong>
97 add optional claim
98 <code>email</code>
99 (ID token).
100 </li>
101 <li>Fill in the fields below, save, then add allowed emails in the section below.</li>
102 </ol>
103 </div>
104 </n-alert>
105 </n-collapse-transition>
106
107 <n-form-item label="Enable Azure SSO">
108 <n-switch v-model:value="form.azure_enabled" :disabled="!form.sso_enabled" />
109 </n-form-item>
110 <n-form-item label="Tenant ID">
111 <n-input
112 v-model:value="form.azure_tenant_id"
113 placeholder="e.g. 12345678-abcd-1234-efgh-123456789012"
114 :disabled="!form.azure_enabled || !form.sso_enabled"
115 />
116 </n-form-item>
117 <n-form-item label="Client ID (Application ID)">
118 <n-input
119 v-model:value="form.azure_client_id"
120 placeholder="e.g. 87654321-dcba-4321-hgfe-210987654321"
121 :disabled="!form.azure_enabled || !form.sso_enabled"
122 />
123 </n-form-item>
124 <n-form-item label="Client Secret">
125 <n-input
126 v-model:value="form.azure_client_secret"
127 type="password"
128 show-password-on="click"
129 :placeholder="azureSecretSet ? '••••••• (saved — leave empty to keep)' : 'Enter client secret'"
130 :disabled="!form.azure_enabled || !form.sso_enabled"
131 />
132 </n-form-item>
133 <n-form-item label="Redirect URI">
134 <n-input-group>
135 <n-input
136 v-model:value="form.azure_redirect_uri"
137 placeholder="https://your-domain.com/api/auth/sso/azure/callback"
138 :disabled="!form.azure_enabled || !form.sso_enabled"
139 />
140 <n-button :disabled="!form.azure_enabled || !form.sso_enabled" @click="prefillRedirectUri">
141 Auto-fill
142 </n-button>
143 </n-input-group>
144 </n-form-item>
145 <n-divider />
146
147 <!-- ── Google OAuth2 / OIDC ──────────────────────────────────── -->
148 <div class="mb-4 flex items-center gap-3">
149 <Icon :name="GoogleIcon" :size="22" />
150 <h3 class="text-lg font-semibold">Google (OAuth2 / OIDC)</h3>
151 <n-button text size="small" @click="showGoogleGuide = !showGoogleGuide">
152 <template #icon>
153 <Icon :name="showGoogleGuide ? 'carbon:chevron-up' : 'carbon:information'" />
154 </template>
155 {{ showGoogleGuide ? "Hide guide" : "Setup guide" }}
156 </n-button>
157 </div>
158
159 <n-collapse-transition :show="showGoogleGuide">
160 <n-alert type="info" class="mb-5" :show-icon="false">
161 <div class="text-xs">
162 <p class="mb-2 font-semibold">How to configure Google OAuth2:</p>
163 <ol>
164 <li>
165 Go to
166 <n-button
167 text
168 tag="a"
169 href="https://console.cloud.google.com/apis/credentials"
170 target="_blank"
171 type="info"
172 >
173 Google Cloud Console &rarr; Credentials
174 </n-button>
175 and click
176 <strong>Create Credentials &rarr; OAuth client ID</strong>
177 .
178 </li>
179 <li>
180 Choose
181 <strong>Web application</strong>
182 as the application type.
183 </li>
184 <li>
185 Under
186 <strong>Authorized redirect URIs</strong>
187 , add:
188 <code>https://&lt;your-domain&gt;/api/auth/sso/google/callback</code>
189 </li>
190 <li>
191 Copy the
192 <strong>Client ID</strong>
193 &rarr; paste as
194 <em>Client ID</em>
195 below.
196 </li>
197 <li>
198 Copy the
199 <strong>Client Secret</strong>
200 &rarr; paste as
201 <em>Client Secret</em>
202 below.
203 </li>
204 <li>
205 Make sure the
206 <strong>People API</strong>
207 is enabled in your project (required for
208 <code>email</code>
209 and
210 <code>profile</code>
211 scopes).
212 </li>
213 <li>Fill in the fields below, save, then add allowed emails in the section below.</li>
214 </ol>
215 </div>
216 </n-alert>
217 </n-collapse-transition>
218
219 <n-form-item label="Enable Google SSO">
220 <n-switch v-model:value="form.google_enabled" :disabled="!form.sso_enabled" />
221 </n-form-item>
222 <n-form-item label="Client ID">
223 <n-input
224 v-model:value="form.google_client_id"
225 placeholder="e.g. 123456789-abc...xyz.apps.googleusercontent.com"
226 :disabled="!form.google_enabled || !form.sso_enabled"
227 />
228 </n-form-item>
229 <n-form-item label="Client Secret">
230 <n-input
231 v-model:value="form.google_client_secret"
232 type="password"
233 show-password-on="click"
234 :placeholder="
235 googleSecretSet
236 ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022 (saved \u2014 leave empty to keep)'
237 : 'Enter client secret'
238 "
239 :disabled="!form.google_enabled || !form.sso_enabled"
240 />
241 </n-form-item>
242 <n-form-item label="Redirect URI">
243 <n-input-group>
244 <n-input
245 v-model:value="form.google_redirect_uri"
246 placeholder="https://your-domain.com/api/auth/sso/google/callback"
247 :disabled="!form.google_enabled || !form.sso_enabled"
248 />
249 <n-button
250 :disabled="!form.google_enabled || !form.sso_enabled"
251 @click="prefillGoogleRedirectUri"
252 >
253 Auto-fill
254 </n-button>
255 </n-input-group>
256 </n-form-item>
257
258 <n-divider />
259
260 <!-- ── Cloudflare Access ───────────────────────────────────── -->
261 <div class="mb-4 flex items-center gap-3">
262 <Icon :name="CloudflareIcon" :size="22" />
263 <h3 class="text-lg font-semibold">Cloudflare Access (JWT Assertion)</h3>
264 <n-button text size="small" @click="showCFGuide = !showCFGuide">
265 <template #icon>
266 <Icon :name="showCFGuide ? 'carbon:chevron-up' : 'carbon:information'" />
267 </template>
268 {{ showCFGuide ? "Hide guide" : "Setup guide" }}
269 </n-button>
270 </div>
271
272 <n-collapse-transition :show="showCFGuide">
273 <n-alert type="info" class="mb-5" :show-icon="false">
274 <div class="text-xs">
275 <p class="mb-2 font-semibold">How to configure Cloudflare Access:</p>
276 <ol>
277 <li>
278 In
279 <n-button
280 text
281 tag="a"
282 href="https://one.dash.cloudflare.com"
283 target="_blank"
284 type="info"
285 >
286 Cloudflare Zero Trust dashboard
287 </n-button>
288 go to
289 <strong>Access → Applications → Add an application</strong>
290 .
291 </li>
292 <li>
293 Choose
294 <strong>Self-hosted</strong>
295 . Set the domain to your CoPilot URL (e.g.
296 <code>copilot.example.com</code>
297 ).
298 </li>
299 <li>
300 Under
301 <strong>Identity providers</strong>
302 connect your IdP (e.g. Entra ID, Google, GitHub).
303 </li>
304 <li>
305 After creating the app, open it →
306 <strong>Overview</strong>
307 → copy the
308 <strong>Application Audience (AUD) Tag</strong>
309 → paste as
310 <em>Application Audience</em>
311 below.
312 </li>
313 <li>
314 Copy your
315 <strong>Team Domain</strong>
316 from
317 <strong>Settings → Custom Pages</strong>
318 (e.g.
319 <code>myteam.cloudflareaccess.com</code>
320 ) → paste as
321 <em>Team Domain</em>
322 below.
323 </li>
324 <li>
325 <strong>How it works:</strong>
326 Cloudflare injects a signed
327 <code>Cf-Access-Jwt-Assertion</code>
328 header into every request. CoPilot verifies the JWT signature against Cloudflare's
329 public JWKS — it is cryptographically impossible to forge without Cloudflare's
330 private key.
331 </li>
332 <li>
333 On the CoPilot login page click
334 <strong>"Sign in with Cloudflare Access"</strong>
335 — the backend reads the header automatically.
336 </li>
337 </ol>
338 </div>
339 </n-alert>
340 </n-collapse-transition>
341
342 <n-form-item label="Enable Cloudflare Access">
343 <n-switch v-model:value="form.cf_enabled" :disabled="!form.sso_enabled" />
344 </n-form-item>
345 <n-form-item label="Team Domain">
346 <n-input
347 v-model:value="form.cf_team_domain"
348 placeholder="e.g. myteam.cloudflareaccess.com"
349 :disabled="!form.cf_enabled || !form.sso_enabled"
350 />
351 </n-form-item>
352 <n-form-item label="Application Audience (AUD)">
353 <n-input
354 v-model:value="form.cf_audience"
355 placeholder="AUD tag from Cloudflare Access dashboard"
356 :disabled="!form.cf_enabled || !form.sso_enabled"
357 />
358 </n-form-item>
359
360 <n-divider />
361
362 <div class="flex justify-end">
363 <n-button type="primary" :loading="saving" @click="saveSettings">Save SSO Settings</n-button>
364 </div>
365 </n-form>
366 </n-spin>
367 </n-card>
368 </template>
369
370 <script setup lang="ts">
371 import type { SSOConfigUpdate } from "@/api/endpoints/sso"
372 import {
373 NAlert,
374 NButton,
375 NCard,
376 NCollapseTransition,
377 NDivider,
378 NForm,
379 NFormItem,
380 NInput,
381 NInputGroup,
382 NSpin,
383 NSwitch,
384 NTag,
385 NText,
386 useMessage
387 } from "naive-ui"
388 import { computed, onBeforeMount, ref } from "vue"
389 import Api from "@/api"
390 import Icon from "@/components/common/Icon.vue"
391 import { useAuthStore } from "@/stores/auth"
392
393 const AzureIcon = "devicon-plain:azure"
394 const GoogleIcon = "devicon-plain:google"
395 const CloudflareIcon = "simple-icons:cloudflare"
396
397 const authStore = useAuthStore()
398 const message = useMessage()
399
400 const isAdmin = computed(() => authStore.isAdmin)
401
402 const loading = ref(false)
403 const saving = ref(false)
404 const showAzureGuide = ref(false)
405 const showGoogleGuide = ref(false)
406 const showCFGuide = ref(false)
407 const azureSecretSet = ref(false)
408 const googleSecretSet = ref(false)
409 const ssoEnabled = ref(false)
410
411 const form = ref<SSOConfigUpdate>({
412 sso_enabled: false,
413 azure_enabled: false,
414 azure_tenant_id: null,
415 azure_client_id: null,
416 azure_client_secret: null,
417 azure_redirect_uri: null,
418 google_enabled: false,
419 google_client_id: null,
420 google_client_secret: null,
421 google_redirect_uri: null,
422 cf_enabled: false,
423 cf_team_domain: null,
424 cf_audience: null
425 })
426
427 function prefillRedirectUri() {
428 form.value.azure_redirect_uri = `${window.location.origin}/api/auth/sso/azure/callback`
429 }
430
431 function prefillGoogleRedirectUri() {
432 form.value.google_redirect_uri = `${window.location.origin}/api/auth/sso/google/callback`
433 }
434
435 async function loadSettings() {
436 loading.value = true
437
438 try {
439 const res = await Api.sso.getSettings()
440 ssoEnabled.value = res.data.sso_enabled
441 azureSecretSet.value = res.data.azure_client_secret_set
442 googleSecretSet.value = res.data.google_client_secret_set
443 form.value = {
444 sso_enabled: res.data.sso_enabled,
445 azure_enabled: res.data.azure_enabled,
446 azure_tenant_id: res.data.azure_tenant_id,
447 azure_client_id: res.data.azure_client_id,
448 azure_client_secret: null,
449 azure_redirect_uri: res.data.azure_redirect_uri,
450 google_enabled: res.data.google_enabled,
451 google_client_id: res.data.google_client_id,
452 google_client_secret: null,
453 google_redirect_uri: res.data.google_redirect_uri,
454 cf_enabled: res.data.cf_enabled,
455 cf_team_domain: res.data.cf_team_domain,
456 cf_audience: res.data.cf_audience
457 }
458 } finally {
459 loading.value = false
460 }
461 }
462
463 async function saveSettings() {
464 saving.value = true
465
466 try {
467 const res = await Api.sso.updateSettings(form.value)
468 ssoEnabled.value = res.data.sso_enabled
469 azureSecretSet.value = res.data.azure_client_secret_set
470 googleSecretSet.value = res.data.google_client_secret_set
471 message.success("SSO settings saved successfully")
472 } catch (err: any) {
473 message.error(err.response?.data?.message || err.response?.data?.detail || "Failed to save SSO settings")
474 } finally {
475 saving.value = false
476 }
477 }
478
479 onBeforeMount(() => {
480 if (isAdmin.value) {
481 loadSettings()
482 }
483 })
484 </script>