hesabixCore/webUI/src/components/OAuthManager.vue

577 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>