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:
2026-08-14 23:23:06 +03:30
parent 35dfd4a603
commit 833584b1b4
7 changed files with 160 additions and 24 deletions
+7
View File
@@ -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 })
};
+12 -1
View File
@@ -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
View File
@@ -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": "عملیات راه‌اندازی و پیکربندی اولیه پایگاه داده"
} }
} }
+4
View File
@@ -51,6 +51,10 @@ export function setupGuards(router) {
} }
} }
if (to.meta.superAdminOnly && permissionStore.roleName !== 'SuperAdmin') {
return next({ name: 'Forbidden' });
}
next(); next();
}); });
} }
+7
View File
@@ -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',
+2 -22
View File
@@ -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 = '';
+122
View File
@@ -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>