feat: add settings seed action and remove login credentials
SuperAdmins can seed once from Settings, and the login page no longer exposes default credentials.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
// /src/api/seedApi.js
|
||||||
|
import axiosInstance from './axiosInstance';
|
||||||
|
|
||||||
|
export const seedApi = {
|
||||||
|
getStatus: () => axiosInstance.get('/seed/status'),
|
||||||
|
runSeed: () => axiosInstance.post('/seed', {}, { timeout: 60000 })
|
||||||
|
};
|
||||||
@@ -22,11 +22,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { usePermissionStore } from '@/stores/permissionStore';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
|
||||||
const menuItems = [
|
const baseMenuItems = [
|
||||||
{ label: 'داشبورد اصلی', icon: 'pi pi-home', to: '/' },
|
{ label: 'داشبورد اصلی', icon: 'pi pi-home', to: '/' },
|
||||||
{ label: 'مدیریت کاربران', icon: 'pi pi-users', to: '/users' },
|
{ label: 'مدیریت کاربران', icon: 'pi pi-users', to: '/users' },
|
||||||
{ label: 'مدیریت اساتید', icon: 'pi pi-id-card', to: '/professors' },
|
{ label: 'مدیریت اساتید', icon: 'pi pi-id-card', to: '/professors' },
|
||||||
@@ -40,6 +43,14 @@ const menuItems = [
|
|||||||
{ label: 'نقشها و دسترسیها', icon: 'pi pi-shield', to: '/roles' }
|
{ label: 'نقشها و دسترسیها', icon: 'pi pi-shield', to: '/roles' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const menuItems = computed(() => {
|
||||||
|
const items = [...baseMenuItems];
|
||||||
|
if (permissionStore.roleName === 'SuperAdmin') {
|
||||||
|
items.push({ label: 'تنظیمات', icon: 'pi pi-cog', to: '/settings' });
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
function isMenuActive(to) {
|
function isMenuActive(to) {
|
||||||
if (to === '/') {
|
if (to === '/') {
|
||||||
return route.path === '/';
|
return route.path === '/';
|
||||||
|
|||||||
+6
-1
@@ -41,7 +41,8 @@
|
|||||||
"notifications": "اطلاعیه ها",
|
"notifications": "اطلاعیه ها",
|
||||||
"contactInquiries": "درخواستهای تماس",
|
"contactInquiries": "درخواستهای تماس",
|
||||||
"certificates": "گواهی نامه ها",
|
"certificates": "گواهی نامه ها",
|
||||||
"logs": "گزارش فعالیتها"
|
"logs": "گزارش فعالیتها",
|
||||||
|
"settings": "تنظیمات"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"title": "مدیریت کاربران",
|
"title": "مدیریت کاربران",
|
||||||
@@ -135,5 +136,9 @@
|
|||||||
"subtitle": "مدیریت تراکنشها و شهریهها",
|
"subtitle": "مدیریت تراکنشها و شهریهها",
|
||||||
"addPayment": "ثبت پرداخت جدید",
|
"addPayment": "ثبت پرداخت جدید",
|
||||||
"paymentDetail": "جزئیات صورتحساب"
|
"paymentDetail": "جزئیات صورتحساب"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "تنظیمات سیستم",
|
||||||
|
"subtitle": "عملیات راهاندازی و پیکربندی اولیه پایگاه داده"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export function setupGuards(router) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (to.meta.superAdminOnly && permissionStore.roleName !== 'SuperAdmin') {
|
||||||
|
return next({ name: 'Forbidden' });
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ export const routes = [
|
|||||||
meta: { permission: 'logs:read' }
|
meta: { permission: 'logs:read' }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
name: 'Settings',
|
||||||
|
component: () => import('@/views/settings/SettingsView.vue'),
|
||||||
|
meta: { title: 'تنظیمات', superAdminOnly: true }
|
||||||
|
},
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
{
|
{
|
||||||
path: '403',
|
path: '403',
|
||||||
|
|||||||
@@ -10,19 +10,6 @@
|
|||||||
<p class="text-muted text-sm mt-2 mb-0">{{ $t('auth.welcome') }}</p>
|
<p class="text-muted text-sm mt-2 mb-0">{{ $t('auth.welcome') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Credentials Hint Box -->
|
|
||||||
<div class="surface-ground p-3 border-round border-1 border-color mb-4 text-xs">
|
|
||||||
<div class="font-bold text-primary mb-1 flex align-items-center gap-1">
|
|
||||||
<i class="pi pi-key"></i>
|
|
||||||
<span>اطلاعات ورود مدیر سیستم:</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-content-between align-items-center mt-1">
|
|
||||||
<span>نام کاربری: <code class="font-bold text-color">superadmin</code></span>
|
|
||||||
<span>رمز عبور: <code class="font-bold text-color">SuperAdminSecret123!</code></span>
|
|
||||||
</div>
|
|
||||||
<Button label="جاگذاری اطلاعات حساب مدیر" size="small" text class="w-full mt-2 text-xs p-1" @click="fillDemo" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="handleLogin" class="flex flex-column gap-3">
|
<form @submit.prevent="handleLogin" class="flex flex-column gap-3">
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label for="username" class="font-semibold text-sm text-color">{{ $t('auth.username') }}</label>
|
<label for="username" class="font-semibold text-sm text-color">{{ $t('auth.username') }}</label>
|
||||||
@@ -33,7 +20,6 @@
|
|||||||
v-model.trim="form.username"
|
v-model.trim="form.username"
|
||||||
class="w-full text-sm"
|
class="w-full text-sm"
|
||||||
:class="{ 'p-invalid': errors.username }"
|
:class="{ 'p-invalid': errors.username }"
|
||||||
placeholder="superadmin"
|
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
/>
|
/>
|
||||||
</IconField>
|
</IconField>
|
||||||
@@ -52,7 +38,6 @@
|
|||||||
class="w-full text-sm"
|
class="w-full text-sm"
|
||||||
inputClass="w-full text-sm"
|
inputClass="w-full text-sm"
|
||||||
:class="{ 'p-invalid': errors.password }"
|
:class="{ 'p-invalid': errors.password }"
|
||||||
placeholder="SuperAdminSecret123!"
|
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
</IconField>
|
</IconField>
|
||||||
@@ -90,8 +75,8 @@ const { showSuccess, showError } = useToast();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: 'superadmin',
|
username: '',
|
||||||
password: 'SuperAdminSecret123!'
|
password: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
@@ -101,11 +86,6 @@ const errors = reactive({
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const fillDemo = () => {
|
|
||||||
form.username = 'superadmin';
|
|
||||||
form.password = 'SuperAdminSecret123!';
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
let valid = true;
|
let valid = true;
|
||||||
errors.username = '';
|
errors.username = '';
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<!-- /src/views/settings/SettingsView.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="settings-view w-full max-w-3xl mx-auto">
|
||||||
|
<PageHeader
|
||||||
|
title="تنظیمات سیستم"
|
||||||
|
subtitle="عملیات راهاندازی و پیکربندی اولیه پایگاه داده"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="surface-card p-4 sm:p-5 border-round-xl border-1 border-color shadow-sm">
|
||||||
|
<div class="flex align-items-start gap-3 mb-4">
|
||||||
|
<div class="w-3rem h-3rem border-round-xl flex align-items-center justify-content-center flex-shrink-0 bg-primary-light text-primary">
|
||||||
|
<i class="pi pi-database text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h2 class="text-lg font-bold text-color m-0 mb-1">راهاندازی پایگاه داده</h2>
|
||||||
|
<p class="text-sm text-muted m-0 line-height-3">
|
||||||
|
نقشهای سیستمی و حساب مدیر ارشد را یکبار ایجاد میکند. پس از اجرا، این دکمه برای همیشه مخفی میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isStatusLoading" class="flex align-items-center gap-2 text-muted text-sm">
|
||||||
|
<i class="pi pi-spin pi-spinner"></i>
|
||||||
|
<span>در حال بررسی وضعیت راهاندازی…</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isLocked" class="flex align-items-center gap-2 p-3 border-round surface-ground border-1 border-color">
|
||||||
|
<i class="pi pi-lock text-primary"></i>
|
||||||
|
<div class="flex flex-column gap-1">
|
||||||
|
<span class="font-semibold text-sm text-color">راهاندازی اولیه انجام شده است</span>
|
||||||
|
<span v-if="lockedAtLabel" class="text-xs text-muted">{{ lockedAtLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-column sm:flex-row align-items-stretch sm:align-items-center gap-3">
|
||||||
|
<Button
|
||||||
|
label="اجرای راهاندازی پایگاه داده"
|
||||||
|
icon="pi pi-play"
|
||||||
|
class="font-bold"
|
||||||
|
:loading="isSeeding"
|
||||||
|
@click="confirmSeed"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-muted line-height-3">
|
||||||
|
این عملیات فقط یکبار قابل اجرا است.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
|
import { seedApi } from '@/api/seedApi';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
|
const isStatusLoading = ref(true);
|
||||||
|
const isSeeding = ref(false);
|
||||||
|
const isLocked = ref(false);
|
||||||
|
const lockedAt = ref(null);
|
||||||
|
|
||||||
|
const lockedAtLabel = computed(() => {
|
||||||
|
if (!lockedAt.value) return '';
|
||||||
|
const date = new Date(lockedAt.value);
|
||||||
|
if (Number.isNaN(date.getTime())) return '';
|
||||||
|
return `قفل شده در ${date.toLocaleString('fa-IR')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyStatus = (payload) => {
|
||||||
|
const status = payload?.data || payload || {};
|
||||||
|
isLocked.value = Boolean(status.locked);
|
||||||
|
lockedAt.value = status.lockedAt || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadStatus = async () => {
|
||||||
|
isStatusLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await seedApi.getStatus();
|
||||||
|
applyStatus(response);
|
||||||
|
} catch (err) {
|
||||||
|
showError(err);
|
||||||
|
} finally {
|
||||||
|
isStatusLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runSeed = async () => {
|
||||||
|
isSeeding.value = true;
|
||||||
|
try {
|
||||||
|
const response = await seedApi.runSeed();
|
||||||
|
applyStatus({ locked: true, lockedAt: new Date().toISOString(), ...(response?.data || response) });
|
||||||
|
isLocked.value = true;
|
||||||
|
showSuccess('راهاندازی پایگاه داده با موفقیت انجام شد.');
|
||||||
|
} catch (err) {
|
||||||
|
showError(err);
|
||||||
|
await loadStatus();
|
||||||
|
} finally {
|
||||||
|
isSeeding.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmSeed = () => {
|
||||||
|
confirm.require({
|
||||||
|
header: 'راهاندازی پایگاه داده',
|
||||||
|
message: 'نقشهای سیستمی و حساب مدیر ارشد ایجاد یا بهروز میشوند. این دکمه پس از اجرا دیگر نمایش داده نمیشود. ادامه میدهید؟',
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
acceptLabel: 'اجرا',
|
||||||
|
rejectLabel: 'انصراف',
|
||||||
|
acceptClass: 'p-button-primary',
|
||||||
|
accept: () => {
|
||||||
|
runSeed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadStatus);
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user