hesabixCore/webUI/src/components/OAuthManager.vue

577 lines
20 KiB
Vue
Raw Normal View History

<template>
<div>
<!-- هدر اصلی -->
<div class="d-flex align-center mb-8">
<div class="d-flex align-center bg-primary-lighten-5 pa-4 rounded-lg">
<v-icon size="32" color="primary" class="mr-3">mdi-oauth</v-icon>
<div>
<h3 class="text-h5 font-weight-medium text-primary mb-1">برنامههای OAuth</h3>
<p class="text-caption text-medium-emphasis mb-0">مدیریت برنامههای احراز هویت خارجی</p>
</div>
</div>
</div>
<!-- توضیحات OAuth -->
<v-alert
type="info"
variant="tonal"
class="mb-6"
>
<template v-slot:prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-right">
<strong>OAuth چیست؟</strong><br>
OAuth یک پروتکل استاندارد برای احراز هویت است که به برنامههای خارجی اجازه میدهد
بدون نیاز به رمز عبور، به حساب کاربران دسترسی داشته باشند. این روش امنتر و راحتتر از
روشهای سنتی است.
</div>
</v-alert>
<!-- کارت مدیریت برنامهها -->
<v-card variant="outlined" class="pa-6" elevation="0">
<v-card-title class="text-subtitle-1 font-weight-medium pb-4 d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon start class="mr-2" color="primary">mdi-oauth</v-icon>
مدیریت برنامهها
</div>
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="showCreateDialog = true"
>
برنامه جدید
</v-btn>
</v-card-title>
<v-card-text class="pa-0">
<v-alert
v-if="applications.length === 0"
type="info"
variant="tonal"
class="mb-4"
>
هنوز هیچ برنامه OAuth ایجاد نکردهاید. برای شروع، یک برنامه جدید ایجاد کنید.
</v-alert>
<v-row v-else>
<v-col
v-for="app in applications"
:key="app.id"
cols="12"
md="6"
lg="4"
>
<v-card
variant="outlined"
class="h-100 oauth-card"
:class="{ 'oauth-card-active': app.isActive, 'oauth-card-inactive': !app.isActive }"
>
<v-card-title class="d-flex justify-space-between align-center">
<div class="d-flex align-center">
<v-avatar
:color="app.isActive ? 'success' : 'grey'"
size="32"
class="mr-2"
>
<v-icon size="16" color="white">
{{ app.isActive ? 'mdi-check' : 'mdi-close' }}
</v-icon>
</v-avatar>
{{ app.name }}
</div>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-dots-vertical"
variant="text"
v-bind="props"
></v-btn>
</template>
<v-list>
<v-list-item @click="editApplication(app)" class="oauth-menu-item">
<template v-slot:prepend>
<v-icon>mdi-pencil</v-icon>
</template>
<v-list-item-title>ویرایش</v-list-item-title>
</v-list-item>
<v-list-item @click="showStats(app)" class="oauth-menu-item">
<template v-slot:prepend>
<v-icon>mdi-chart-line</v-icon>
</template>
<v-list-item-title>آمار</v-list-item-title>
</v-list-item>
<v-list-item @click="regenerateSecret(app)" class="oauth-menu-item">
<template v-slot:prepend>
<v-icon>mdi-refresh</v-icon>
</template>
<v-list-item-title>بازسازی کلید</v-list-item-title>
</v-list-item>
<v-list-item @click="revokeTokens(app)" class="oauth-menu-item">
<template v-slot:prepend>
<v-icon>mdi-logout</v-icon>
</template>
<v-list-item-title>لغو توکنها</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<v-list-item @click="deleteApplication(app)" color="error" class="oauth-menu-item">
<template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon>
</template>
<v-list-item-title class="text-error">حذف</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-card-title>
<v-card-text>
<div v-if="app.description" class="mb-3 oauth-info-text">
{{ app.description }}
</div>
<div class="text-caption text-grey">
<div class="mb-1">
<strong>Client ID:</strong>
<span class="oauth-code">{{ app.clientId }}</span>
<v-btn
icon="mdi-content-copy"
size="x-small"
variant="text"
@click="copyToClipboard(app.clientId)"
></v-btn>
</div>
<div class="mb-1">
<strong>Redirect URI:</strong> {{ app.redirectUri }}
</div>
<div class="mb-1">
<strong>وضعیت:</strong>
<v-chip
:color="app.isActive ? 'success' : 'error'"
size="x-small"
class="ml-1 oauth-status-chip"
>
{{ app.isActive ? 'فعال' : 'غیرفعال' }}
</v-chip>
</div>
<div class="mb-1">
<strong>تایید:</strong>
<v-chip
:color="app.isVerified ? 'success' : 'warning'"
size="x-small"
class="ml-1 oauth-status-chip"
>
{{ app.isVerified ? 'تایید شده' : 'در انتظار تایید' }}
</v-chip>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Dialog ایجاد/ویرایش برنامه -->
<v-dialog v-model="showCreateDialog" max-width="700px" persistent class="oauth-dialog">
<v-card>
<v-card-title class="text-h6 pa-6 pb-4">
<v-icon start class="mr-2" color="primary">mdi-oauth</v-icon>
{{ editingApp ? 'ویرایش برنامه OAuth' : 'ایجاد برنامه OAuth جدید' }}
</v-card-title>
<v-card-text class="pa-6 pt-0">
<v-form ref="form" @submit.prevent="saveApplication">
<v-row>
<v-col cols="12" class="oauth-form-field">
<v-text-field
v-model="form.name"
label="نام برنامه *"
:rules="formRules.name"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-application"
required
></v-text-field>
</v-col>
<v-col cols="12" class="oauth-form-field">
<v-textarea
v-model="form.description"
label="توضیحات"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-text"
rows="3"
auto-grow
></v-textarea>
</v-col>
<v-col cols="12" md="6" class="oauth-form-field">
<v-text-field
v-model="form.website"
label="آدرس وب‌سایت"
:rules="formRules.website"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-web"
type="url"
placeholder="https://example.com"
></v-text-field>
</v-col>
<v-col cols="12" md="6" class="oauth-form-field">
<v-text-field
v-model="form.redirectUri"
label="آدرس بازگشت (Redirect URI) *"
:rules="formRules.redirectUri"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-link"
type="url"
placeholder="https://example.com/callback"
required
></v-text-field>
</v-col>
<v-col cols="12" md="6" class="oauth-form-field">
<v-text-field
v-model.number="form.rateLimit"
label="محدودیت درخواست (در ساعت)"
:rules="formRules.rateLimit"
variant="outlined"
density="comfortable"
prepend-inner-icon="mdi-speedometer"
type="number"
min="1"
max="10000"
hint="تعداد درخواست مجاز در هر ساعت"
persistent-hint
></v-text-field>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-6">
<v-spacer></v-spacer>
<v-btn
variant="outlined"
@click="cancelEdit"
class="mr-2"
>
انصراف
</v-btn>
<v-btn
color="primary"
@click="saveApplication"
:loading="saving"
prepend-icon="mdi-content-save"
>
{{ editingApp ? 'ویرایش' : 'ایجاد' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar برای نمایش پیامها -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
location="top"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'
import axios from 'axios'
import Swal from 'sweetalert2'
export default defineComponent({
name: 'OAuthManager',
setup() {
const applications = ref([])
const loading = ref(false)
const saving = ref(false)
const showCreateDialog = ref(false)
const editingApp = ref(null)
const form = ref({
name: '',
description: '',
website: '',
redirectUri: '',
allowedScopes: [],
rateLimit: 1000
})
const formRules = {
name: [
(v: any) => !!v || 'نام برنامه الزامی است',
(v: any) => v.length >= 3 || 'نام برنامه باید حداقل 3 کاراکتر باشد',
(v: any) => v.length <= 255 || 'نام برنامه نمی‌تواند بیشتر از 255 کاراکتر باشد'
],
redirectUri: [
(v: any) => !!v || 'آدرس بازگشت الزامی است',
(v: any) => /^https?:\/\/.+/.test(v) || 'آدرس بازگشت باید یک URL معتبر باشد'
],
website: [
(v: any) => !v || /^https?:\/\/.+/.test(v) || 'آدرس وب‌سایت باید یک URL معتبر باشد'
],
rateLimit: [
(v: any) => v >= 1 || 'محدودیت درخواست باید حداقل 1 باشد',
(v: any) => v <= 10000 || 'محدودیت درخواست نمی‌تواند بیشتر از 10000 باشد'
]
}
const snackbar = ref({
show: false,
text: '',
color: 'success',
timeout: 3000
})
const loadApplications = async () => {
loading.value = true
try {
const response = await axios.get('/api/oauth/applications')
applications.value = response.data.data || []
} catch (error) {
console.error('خطا در بارگذاری برنامه‌ها:', error)
showSnackbar('خطا در بارگذاری برنامه‌ها', 'error')
} finally {
loading.value = false
}
}
const saveApplication = async () => {
const { valid } = await form.value.$refs?.validate()
if (!valid) {
showSnackbar('لطفاً خطاهای فرم را برطرف کنید', 'error')
return
}
saving.value = true
try {
if (editingApp.value) {
await axios.put(`/api/oauth/applications/${editingApp.value.id}`, form.value)
showSnackbar('برنامه با موفقیت ویرایش شد', 'success')
} else {
const response = await axios.post('/api/oauth/applications', form.value)
showSnackbar('برنامه با موفقیت ایجاد شد', 'success')
// نمایش client_id و client_secret
if (response.data.data?.client_id) {
Swal.fire({
title: 'اطلاعات برنامه',
html: `
<div class="text-right">
<p><strong>Client ID:</strong> <code>${response.data.data.client_id}</code></p>
<p><strong>Client Secret:</strong> <code>${response.data.data.client_secret}</code></p>
<p class="text-warning"> این اطلاعات را در جای امنی ذخیره کنید!</p>
</div>
`,
icon: 'info'
})
}
}
await loadApplications()
cancelEdit()
} catch (error) {
console.error('خطا در ذخیره برنامه:', error)
const errorMessage = error.response?.data?.message || 'خطا در ذخیره برنامه'
showSnackbar(errorMessage, 'error')
} finally {
saving.value = false
}
}
const editApplication = (app: any) => {
editingApp.value = app
form.value = {
name: app.name,
description: app.description || '',
website: app.website || '',
redirectUri: app.redirectUri,
allowedScopes: app.allowedScopes || [],
rateLimit: app.rateLimit || 1000
}
showCreateDialog.value = true
}
const cancelEdit = () => {
editingApp.value = null
showCreateDialog.value = false
form.value = {
name: '',
description: '',
website: '',
redirectUri: '',
allowedScopes: [],
rateLimit: 1000
}
}
const deleteApplication = async (app: any) => {
const result = await Swal.fire({
title: 'حذف برنامه',
text: `آیا از حذف برنامه "${app.name}" اطمینان دارید؟`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'حذف',
cancelButtonText: 'انصراف'
})
if (result.isConfirmed) {
try {
await axios.delete(`/api/oauth/applications/${app.id}`)
showSnackbar('برنامه با موفقیت حذف شد', 'success')
await loadApplications()
} catch (error) {
console.error('خطا در حذف برنامه:', error)
const errorMessage = error.response?.data?.message || 'خطا در حذف برنامه'
showSnackbar(errorMessage, 'error')
}
}
}
const regenerateSecret = async (app: any) => {
const result = await Swal.fire({
title: 'بازسازی کلید',
text: 'آیا از بازسازی Client Secret اطمینان دارید؟ تمام توکن‌های موجود لغو خواهند شد.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'بازسازی',
cancelButtonText: 'انصراف'
})
if (result.isConfirmed) {
try {
const response = await axios.post(`/api/oauth/applications/${app.id}/regenerate-secret`)
Swal.fire({
title: 'کلید جدید',
html: `<p><strong>Client Secret جدید:</strong> <code>${response.data.data.client_secret}</code></p>`,
icon: 'success'
})
await loadApplications()
showSnackbar('کلید جدید با موفقیت ایجاد شد', 'success')
} catch (error) {
console.error('خطا در بازسازی کلید:', error)
const errorMessage = error.response?.data?.message || 'خطا در بازسازی کلید'
showSnackbar(errorMessage, 'error')
}
}
}
const revokeTokens = async (app: any) => {
const result = await Swal.fire({
title: 'لغو توکن‌ها',
text: 'آیا از لغو تمام توکن‌های این برنامه اطمینان دارید؟',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'لغو توکن‌ها',
cancelButtonText: 'انصراف'
})
if (result.isConfirmed) {
try {
const response = await axios.post(`/api/oauth/applications/${app.id}/revoke-tokens`)
showSnackbar(`${response.data.data.revokedCount} توکن لغو شد`, 'success')
} catch (error) {
console.error('خطا در لغو توکن‌ها:', error)
const errorMessage = error.response?.data?.message || 'خطا در لغو توکن‌ها'
showSnackbar(errorMessage, 'error')
}
}
}
const showStats = async (app: any) => {
try {
const response = await axios.get(`/api/oauth/applications/${app.id}/stats`)
const stats = response.data.data
Swal.fire({
title: `آمار استفاده - ${app.name}`,
html: `
<div class="text-right">
<p><strong>کل توکنها:</strong> ${stats.totalTokens}</p>
<p><strong>توکنهای فعال:</strong> ${stats.activeTokens}</p>
<p><strong>توکنهای منقضی شده:</strong> ${stats.expiredTokens}</p>
${stats.lastUsed ? `<p><strong>آخرین استفاده:</strong> ${new Date(stats.lastUsed).toLocaleDateString('fa-IR')}</p>` : ''}
</div>
`,
icon: 'info'
})
} catch (error) {
console.error('خطا در بارگذاری آمار:', error)
showSnackbar('خطا در بارگذاری آمار', 'error')
}
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
showSnackbar('متن در کلیپ‌بورد کپی شد', 'success')
} catch (error) {
console.error('خطا در کپی:', error)
showSnackbar('خطا در کپی کردن متن', 'error')
}
}
const showSnackbar = (text: string, color = 'success') => {
snackbar.value = {
show: true,
text: text,
color: color,
timeout: 3000
}
}
onMounted(() => {
loadApplications()
})
return {
applications,
loading,
saving,
showCreateDialog,
editingApp,
form,
formRules,
snackbar,
saveApplication,
editApplication,
cancelEdit,
deleteApplication,
regenerateSecret,
revokeTokens,
showStats,
copyToClipboard
}
}
})
</script>
<style scoped>
@import './oauth-styles.css';
</style>