main
vue 190 lines 5.14 KB
Raw
1 <template>
2 <div v-if="isAdmin">
3 <!-- Allowed Emails Card -->
4 <n-card title="SSO Allowed Emails" size="small">
5 <template #header-extra>
6 <div class="flex justify-end gap-2">
7 <n-button size="small" @click="routeSSOConfig().navigate()">
8 <template #icon>
9 <Icon :name="SSOConfigIcon" />
10 </template>
11 SSO Configuration
12 </n-button>
13
14 <n-button size="small" type="primary" @click="showAddEmail = true">
15 <template #icon>
16 <Icon :name="AddIcon" />
17 </template>
18 Add Email
19 </n-button>
20 </div>
21 </template>
22
23 <n-text class="text-secondary mb-4 block text-sm">
24 Existing users are automatically enabled for SSO login. Add email addresses to allow new users to sign
25 in and create an account via SSO.
26 </n-text>
27
28 <n-spin :show="loadingEmails">
29 <p v-if="!allowedEmails.length" class="text-secondary py-4 text-center">
30 No allowed emails configured. Add an email to allow access via SSO.
31 </p>
32 <n-table v-else>
33 <thead>
34 <tr>
35 <th>Email</th>
36 <th>Role</th>
37 <th>Added</th>
38 <th class="w-16"></th>
39 </tr>
40 </thead>
41 <tbody>
42 <tr v-for="entry of allowedEmails" :key="entry.id">
43 <td>{{ entry.email }}</td>
44 <td>
45 <n-tag :type="getRoleTagType(entry.role_id)" size="small">
46 {{ getRoleName(entry.role_id) }}
47 </n-tag>
48 </td>
49 <td>{{ formatDate(entry.created_at, dFormats.datetime) }}</td>
50 <td>
51 <n-button text type="error" size="small" @click="removeEmail(entry.id)">
52 <template #icon>
53 <Icon :name="DeleteIcon" />
54 </template>
55 </n-button>
56 </td>
57 </tr>
58 </tbody>
59 </n-table>
60 </n-spin>
61 </n-card>
62
63 <!-- Add Email Modal -->
64 <n-modal v-model:show="showAddEmail" preset="card" title="Add SSO Allowed Email" :style="{ maxWidth: '450px' }">
65 <n-form :model="newEmail" label-placement="top">
66 <n-form-item label="Email Address">
67 <n-input v-model:value="newEmail.email" placeholder="user@company.com" />
68 </n-form-item>
69 <n-form-item label="Assigned Role" class="w-full">
70 <div class="flex w-full flex-col gap-2">
71 <n-select v-model:value="newEmail.role_id" :options="roleOptions" />
72 <span class="text-secondary text-xs">
73 Role assigned when the user logs in for the first time via SSO.
74 </span>
75 </div>
76 </n-form-item>
77 <div class="flex justify-end gap-2 pt-2">
78 <n-button @click="showAddEmail = false">Cancel</n-button>
79 <n-button type="primary" :loading="addingEmail" :disabled="!newEmail.email" @click="addEmail">
80 Add Email
81 </n-button>
82 </div>
83 </n-form>
84 </n-modal>
85 </div>
86 </template>
87
88 <script setup lang="ts">
89 import type { SSOAllowedEmail } from "@/api/endpoints/sso"
90 import {
91 NButton,
92 NCard,
93 NForm,
94 NFormItem,
95 NInput,
96 NModal,
97 NSelect,
98 NSpin,
99 NTable,
100 NTag,
101 NText,
102 useMessage
103 } from "naive-ui"
104 import { computed, onBeforeMount, ref } from "vue"
105 import Api from "@/api"
106 import Icon from "@/components/common/Icon.vue"
107 import { useNavigation } from "@/composables/useNavigation"
108 import { useAuthStore } from "@/stores/auth"
109 import { useSettingsStore } from "@/stores/settings"
110 import { formatDate } from "@/utils/format"
111
112 const { routeSSOConfig } = useNavigation()
113
114 const SSOConfigIcon = "carbon:rule-locked"
115 const AddIcon = "carbon:add"
116 const DeleteIcon = "carbon:trash-can"
117
118 const authStore = useAuthStore()
119 const message = useMessage()
120 const dFormats = useSettingsStore().dateFormat
121
122 const isAdmin = computed(() => authStore.isAdmin)
123
124 const loadingEmails = ref(false)
125 const addingEmail = ref(false)
126 const showAddEmail = ref(false)
127
128 const allowedEmails = ref<SSOAllowedEmail[]>([])
129 const newEmail = ref({ email: "", role_id: 2 })
130
131 const roleOptions = [
132 { label: "Admin", value: 1 },
133 { label: "Analyst (default)", value: 2 }
134 ]
135
136 function getRoleName(roleId: number) {
137 return roleId === 1 ? "admin" : roleId === 2 ? "analyst" : roleId === 3 ? "scheduler" : "customer_user"
138 }
139
140 function getRoleTagType(roleId: number) {
141 return roleId === 1 ? "error" : roleId === 2 ? "warning" : "default"
142 }
143
144 async function loadEmails() {
145 loadingEmails.value = true
146
147 try {
148 const res = await Api.sso.getAllowedEmails()
149 allowedEmails.value = res.data.emails
150 } catch {
151 allowedEmails.value = []
152 } finally {
153 loadingEmails.value = false
154 }
155 }
156
157 async function addEmail() {
158 if (!newEmail.value.email) return
159
160 addingEmail.value = true
161
162 try {
163 await Api.sso.addAllowedEmail({ email: newEmail.value.email, role_id: newEmail.value.role_id })
164 message.success(`Email ${newEmail.value.email} added to allowlist`)
165 newEmail.value = { email: "", role_id: 2 }
166 showAddEmail.value = false
167 loadEmails()
168 } catch (err: any) {
169 message.error(err.response?.data?.message || err.response?.data?.detail || "Failed to add email")
170 } finally {
171 addingEmail.value = false
172 }
173 }
174
175 async function removeEmail(id: number) {
176 try {
177 await Api.sso.removeAllowedEmail(id)
178 message.success("Email removed from allowlist")
179 loadEmails()
180 } catch (err: any) {
181 message.error(err.response?.data?.message || err.response?.data?.detail || "Failed to remove email")
182 }
183 }
184
185 onBeforeMount(() => {
186 if (isAdmin.value) {
187 loadEmails()
188 }
189 })
190 </script>