админ страница

This commit is contained in:
Мельников Данил 2025-05-28 21:17:18 +05:00
parent 3fbaaf1b45
commit 75a25f9d47
5 changed files with 997 additions and 55 deletions

View File

@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

912
WEB/src/pages/AdminPage.vue Normal file
View File

@ -0,0 +1,912 @@
<template>
<div class="q-pa-md">
<q-card>
<q-card-section>
<div class="text-h4 q-mb-md">
<q-icon name="admin_panel_settings" class="q-mr-sm" />
Управление данными
</div>
<!-- Навигационные вкладки -->
<q-tabs
v-model="activeTab"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="team" label="Команда" />
<q-tab name="members" label="Участники" />
<q-tab name="contests" label="Конкурсы" />
<q-tab name="projects" label="Проекты" />
<q-tab name="achievements" label="Достижения" />
</q-tabs>
</q-card-section>
<q-separator />
<q-card-section>
<q-tab-panels v-model="activeTab">
<!-- Управление командой -->
<q-tab-panel name="team">
<div class="text-h5 q-mb-md">Информация о команде</div>
<q-form @submit="onSubmitTeam" class="q-gutter-md">
<div class="row q-gutter-md">
<div class="col-12 col-md-6">
<q-input
filled
v-model="teamForm.name"
label="Название команды *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
</div>
<div class="col-12 col-md-5">
<q-file
filled
bottom-slots
v-model="teamForm.logo"
label="Логотип команды"
counter
accept=".jpg, .png, .jpeg, image/*"
>
<template v-slot:prepend>
<q-icon name="cloud_upload" @click.stop.prevent />
</template>
<template v-slot:append>
<q-icon name="close" @click.stop.prevent="teamForm.logo = null" class="cursor-pointer" />
</template>
</q-file>
</div>
</div>
<q-input
filled
type="textarea"
v-model="teamForm.description"
label="Краткое описание команды *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
rows="3"
/>
<q-input
filled
v-model="teamForm.giteaUrl"
label="URL профиля команды на Gitea"
hint="Например: https://gitea.example.com/team"
/>
<div>
<q-btn label="Сохранить изменения" type="submit" color="primary"/>
<q-btn label="Очистить" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</q-tab-panel>
<!-- Управление участниками -->
<q-tab-panel name="members">
<div class="row justify-between items-center q-mb-md">
<div class="text-h5">Участники команды</div>
<q-btn color="primary" label="Добавить участника" @click="showAddMemberDialog" />
</div>
<!-- Список участников -->
<q-list bordered separator>
<q-item v-for="member in members" :key="member.id" clickable v-ripple>
<q-item-section avatar>
<q-avatar>
<img :src="member.avatar || '/api/placeholder/40/40'" />
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ member.fullName }}</q-item-label>
<q-item-label caption>{{ member.role || 'Участник' }}</q-item-label>
<q-item-label caption>{{ member.email }}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="text-grey-8 q-gutter-xs">
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editMember(member)" />
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteMember(member.id)" />
</div>
</q-item-section>
</q-item>
</q-list>
</q-tab-panel>
<!-- Управление конкурсами -->
<q-tab-panel name="contests">
<div class="row justify-between items-center q-mb-md">
<div class="text-h5">Конкурсы</div>
<q-btn color="primary" label="Добавить конкурс" @click="showAddContestDialog" />
</div>
<!-- Список конкурсов -->
<div class="row q-gutter-md">
<q-card v-for="contest in contests" :key="contest.id" class="col-12 col-md-5">
<q-img
:src="contest.image || '/api/placeholder/300/200'"
style="height: 140px"
>
<div class="absolute-bottom">
<div class="text-h6">{{ contest.name }}</div>
</div>
</q-img>
<q-card-section>
<div class="text-caption text-grey">{{ contest.year }}</div>
<div class="text-body2 q-mt-sm">{{ contest.description?.substring(0, 100) }}...</div>
</q-card-section>
<q-card-actions>
<q-btn flat color="primary" label="Редактировать" @click="editContest(contest)" />
<q-btn flat color="negative" label="Удалить" @click="deleteContest(contest.id)" />
</q-card-actions>
</q-card>
</div>
</q-tab-panel>
<!-- Управление проектами -->
<q-tab-panel name="projects">
<div class="row justify-between items-center q-mb-md">
<div class="text-h5">Проекты</div>
<q-btn color="primary" label="Добавить проект" @click="showAddProjectDialog" />
</div>
<!-- Список проектов -->
<q-list bordered separator>
<q-item v-for="project in projects" :key="project.id" clickable v-ripple>
<q-item-section>
<q-item-label>{{ project.name }}</q-item-label>
<q-item-label caption>{{ project.description }}</q-item-label>
<q-item-label caption>
<q-chip v-for="tech in project.technologies" :key="tech" size="sm" color="primary" text-color="white">
{{ tech }}
</q-chip>
</q-item-label>
</q-item-section>
<q-item-section side>
<div class="text-grey-8 q-gutter-xs">
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editProject(project)" />
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteProject(project.id)" />
</div>
</q-item-section>
</q-item>
</q-list>
</q-tab-panel>
<!-- Управление достижениями -->
<q-tab-panel name="achievements">
<div class="row justify-between items-center q-mb-md">
<div class="text-h5">Достижения и курсы</div>
<q-btn color="primary" label="Добавить достижение" @click="showAddAchievementDialog" />
</div>
<!-- Список достижений -->
<q-list bordered separator>
<q-item v-for="achievement in achievements" :key="achievement.id" clickable v-ripple>
<q-item-section>
<q-item-label>{{ achievement.title }}</q-item-label>
<q-item-label caption>{{ achievement.description }}</q-item-label>
<q-item-label caption>{{ achievement.date }} {{ achievement.type }}</q-item-label>
</q-item-section>
<q-item-section side>
<div class="text-grey-8 q-gutter-xs">
<q-btn class="gt-xs" size="12px" flat dense round icon="edit" @click="editAchievement(achievement)" />
<q-btn class="gt-xs" size="12px" flat dense round icon="delete" color="negative" @click="deleteAchievement(achievement.id)" />
</div>
</q-item-section>
</q-item>
</q-list>
</q-tab-panel>
</q-tab-panels>
</q-card-section>
</q-card>
<!-- Dialog для добавления/редактирования участника -->
<q-dialog v-model="memberDialog">
<q-card style="min-width: 400px">
<q-card-section>
<div class="text-h6">{{ editingMember ? 'Редактировать участника' : 'Добавить участника' }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form @submit="onSubmitMember" class="q-gutter-md">
<q-input
filled
v-model="memberForm.fullName"
label="ФИО *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
<q-input
filled
v-model="memberForm.email"
label="Email *"
type="email"
lazy-rules
:rules="[
val => val && val.length > 0 || 'Поле обязательно',
val => /.+@.+\..+/.test(val) || 'Введите корректный email'
]"
/>
<q-input
filled
v-model="memberForm.role"
label="Роль в команде"
hint="Например: Frontend разработчик, Team Lead"
/>
<q-input
filled
type="textarea"
v-model="memberForm.bio"
label="Краткая информация"
rows="3"
/>
<q-file
filled
v-model="memberForm.avatar"
label="Фото профиля"
accept=".jpg, .png, .jpeg, image/*"
/>
<q-input
filled
v-model="memberForm.giteaUsername"
label="Username на Gitea"
/>
<div>
<q-btn label="Сохранить" type="submit" color="primary"/>
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
<!-- Dialog для добавления/редактирования конкурса -->
<q-dialog v-model="contestDialog">
<q-card style="min-width: 500px">
<q-card-section>
<div class="text-h6">{{ editingContest ? 'Редактировать конкурс' : 'Добавить конкурс' }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form @submit="onSubmitContest" class="q-gutter-md">
<q-input
filled
v-model="contestForm.name"
label="Название конкурса *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
<div class="row q-gutter-md">
<div class="col">
<q-input
filled
v-model="contestForm.year"
label="Год *"
type="number"
lazy-rules
:rules="[val => val && val > 2000 || 'Введите корректный год']"
/>
</div>
<div class="col">
<q-file
filled
v-model="contestForm.image"
label="Главное фото"
accept="image/*"
/>
</div>
</div>
<q-input
filled
type="textarea"
v-model="contestForm.description"
label="Описание конкурса *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
rows="4"
/>
<q-input
filled
v-model="contestForm.websiteUrl"
label="Ссылка на сайт конкурса"
type="url"
/>
<q-input
filled
v-model="contestForm.giteaRepo"
label="Репозиторий на Gitea"
hint="Например: team/contest-project-2024"
/>
<q-select
filled
v-model="contestForm.participants"
:options="members"
option-value="id"
option-label="fullName"
label="Участники от команды"
multiple
use-chips
stack-label
/>
<div>
<q-btn label="Сохранить" type="submit" color="primary"/>
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
<!-- Dialog для добавления/редактирования проекта -->
<q-dialog v-model="projectDialog">
<q-card style="min-width: 500px">
<q-card-section>
<div class="text-h6">{{ editingProject ? 'Редактировать проект' : 'Добавить проект' }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form @submit="onSubmitProject" class="q-gutter-md">
<q-input
filled
v-model="projectForm.name"
label="Название проекта *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
<q-input
filled
type="textarea"
v-model="projectForm.description"
label="Описание проекта *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
rows="3"
/>
<q-select
filled
v-model="projectForm.technologies"
:options="technologyOptions"
label="Технологии"
multiple
use-input
use-chips
@new-value="createValue"
stack-label
/>
<div class="row q-gutter-md">
<div class="col">
<q-input
filled
v-model="projectForm.giteaRepo"
label="Репозиторий на Gitea"
/>
</div>
<div class="col">
<q-input
filled
v-model="projectForm.demoUrl"
label="Ссылка на демо"
type="url"
/>
</div>
</div>
<q-select
filled
v-model="projectForm.author"
:options="members"
option-value="id"
option-label="fullName"
label="Автор проекта"
/>
<div>
<q-btn label="Сохранить" type="submit" color="primary"/>
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
<!-- Dialog для добавления/редактирования достижения -->
<q-dialog v-model="achievementDialog">
<q-card style="min-width: 500px">
<q-card-section>
<div class="text-h6">{{ editingAchievement ? 'Редактировать достижение' : 'Добавить достижение' }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form @submit="onSubmitAchievement" class="q-gutter-md">
<q-input
filled
v-model="achievementForm.title"
label="Название достижения/курса *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
<q-input
filled
type="textarea"
v-model="achievementForm.description"
label="Описание *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
rows="3"
/>
<div class="row q-gutter-md">
<div class="col">
<q-select
filled
v-model="achievementForm.type"
:options="['Достижение', 'Курс', 'Сертификат', 'Диплом']"
label="Тип *"
lazy-rules
:rules="[val => val && val.length > 0 || 'Поле обязательно']"
/>
</div>
<div class="col">
<q-input
filled
v-model="achievementForm.date"
label="Дата получения"
type="date"
/>
</div>
</div>
<q-file
filled
v-model="achievementForm.certificate"
label="Скан диплома/сертификата"
accept=".pdf,.jpg,.png,.jpeg"
/>
<q-select
filled
v-model="achievementForm.owner"
:options="members"
option-value="id"
option-label="fullName"
label="Владелец достижения *"
lazy-rules
:rules="[val => val !== null || 'Поле обязательно']"
/>
<div>
<q-btn label="Сохранить" type="submit" color="primary"/>
<q-btn label="Отмена" v-close-popup flat class="q-ml-sm" />
</div>
</q-form>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
name: 'AdminDataManagement',
setup() {
const activeTab = ref('team')
// Dialogs
const memberDialog = ref(false)
const contestDialog = ref(false)
const projectDialog = ref(false)
const achievementDialog = ref(false)
// Editing states
const editingMember = ref(false)
const editingContest = ref(false)
const editingProject = ref(false)
const editingAchievement = ref(false)
// Forms
const teamForm = reactive({
name: 'DevTeam',
description: 'Команда разработчиков, специализирующаяся на создании инновационных веб-решений',
logo: null,
giteaUrl: 'https://gitea.example.com/devteam'
})
const memberForm = reactive({
fullName: '',
email: '',
role: '',
bio: '',
avatar: null,
giteaUsername: ''
})
const contestForm = reactive({
name: '',
year: new Date().getFullYear(),
description: '',
image: null,
websiteUrl: '',
giteaRepo: '',
participants: []
})
const projectForm = reactive({
name: '',
description: '',
technologies: [],
giteaRepo: '',
demoUrl: '',
author: null
})
const achievementForm = reactive({
title: '',
description: '',
type: '',
date: '',
certificate: null,
owner: null
})
// Data
const members = ref([
{
id: 1,
fullName: 'Иван Иванов',
email: 'ivan@example.com',
role: 'Frontend Developer',
avatar: '/api/placeholder/40/40'
},
{
id: 2,
fullName: 'Мария Петрова',
email: 'maria@example.com',
role: 'Backend Developer',
avatar: '/api/placeholder/40/40'
}
])
const contests = ref([
{
id: 1,
name: 'Хакатон IT-решений 2024',
year: 2024,
description: 'Региональный хакатон по разработке IT-решений для бизнеса',
image: '/api/placeholder/300/200'
},
{
id: 2,
name: 'Web Development Challenge',
year: 2023,
description: 'Международный конкурс веб-разработки',
image: '/api/placeholder/300/200'
}
])
const projects = ref([
{
id: 1,
name: 'Система управления проектами',
description: 'Веб-приложение для управления проектами команды',
technologies: ['Vue.js', 'Node.js', 'PostgreSQL']
},
{
id: 2,
name: 'Мобильное приложение для заказа еды',
description: 'React Native приложение с интеграцией платежных систем',
technologies: ['React Native', 'Firebase', 'Stripe']
}
])
const achievements = ref([
{
id: 1,
title: 'Сертификат Vue.js Developer',
description: 'Успешное прохождение курса по Vue.js разработке',
type: 'Сертификат',
date: '2024-01-15'
},
{
id: 2,
title: '1 место в хакатоне',
description: 'Победа в региональном хакатоне IT-решений',
type: 'Достижение',
date: '2024-03-20'
}
])
const technologyOptions = ref([
'Vue.js', 'React', 'Angular', 'Node.js', 'Express', 'FastAPI',
'PostgreSQL', 'MongoDB', 'Docker', 'Kubernetes', 'AWS', 'Azure'
])
// Methods
const onSubmitTeam = () => {
console.log('Saving team info:', teamForm)
// Здесь будет API вызов для сохранения информации о команде
}
const showAddMemberDialog = () => {
editingMember.value = false
resetMemberForm()
memberDialog.value = true
}
const editMember = (member) => {
editingMember.value = true
Object.assign(memberForm, member)
memberDialog.value = true
}
const onSubmitMember = () => {
if (editingMember.value) {
console.log('Updating member:', memberForm)
// API вызов для обновления участника
} else {
console.log('Adding new member:', memberForm)
// API вызов для добавления участника
const newMember = { ...memberForm, id: Date.now() }
members.value.push(newMember)
}
memberDialog.value = false
resetMemberForm()
}
const deleteMember = (id) => {
members.value = members.value.filter(m => m.id !== id)
console.log('Deleting member:', id)
}
const resetMemberForm = () => {
Object.assign(memberForm, {
fullName: '',
email: '',
role: '',
bio: '',
avatar: null,
giteaUsername: ''
})
}
const showAddContestDialog = () => {
editingContest.value = false
resetContestForm()
contestDialog.value = true
}
const editContest = (contest) => {
editingContest.value = true
Object.assign(contestForm, contest)
contestDialog.value = true
}
const onSubmitContest = () => {
if (editingContest.value) {
console.log('Updating contest:', contestForm)
} else {
console.log('Adding new contest:', contestForm)
const newContest = { ...contestForm, id: Date.now() }
contests.value.push(newContest)
}
contestDialog.value = false
resetContestForm()
}
const deleteContest = (id) => {
contests.value = contests.value.filter(c => c.id !== id)
console.log('Deleting contest:', id)
}
const resetContestForm = () => {
Object.assign(contestForm, {
name: '',
year: new Date().getFullYear(),
description: '',
image: null,
websiteUrl: '',
giteaRepo: '',
participants: []
})
}
const showAddProjectDialog = () => {
editingProject.value = false
resetProjectForm()
projectDialog.value = true
}
const editProject = (project) => {
editingProject.value = true
Object.assign(projectForm, project)
projectDialog.value = true
}
const onSubmitProject = () => {
if (editingProject.value) {
console.log('Updating project:', projectForm)
} else {
console.log('Adding new project:', projectForm)
const newProject = { ...projectForm, id: Date.now() }
projects.value.push(newProject)
}
projectDialog.value = false
resetProjectForm()
}
const deleteProject = (id) => {
projects.value = projects.value.filter(p => p.id !== id)
console.log('Deleting project:', id)
}
const resetProjectForm = () => {
Object.assign(projectForm, {
name: '',
description: '',
technologies: [],
giteaRepo: '',
demoUrl: '',
author: null
})
}
const showAddAchievementDialog = () => {
editingAchievement.value = false
resetAchievementForm()
achievementDialog.value = true
}
const editAchievement = (achievement) => {
editingAchievement.value = true
Object.assign(achievementForm, achievement)
achievementDialog.value = true
}
const onSubmitAchievement = () => {
if (editingAchievement.value) {
console.log('Updating achievement:', achievementForm)
} else {
console.log('Adding new achievement:', achievementForm)
const newAchievement = { ...achievementForm, id: Date.now() }
achievements.value.push(newAchievement)
}
achievementDialog.value = false
resetAchievementForm()
}
const deleteAchievement = (id) => {
achievements.value = achievements.value.filter(a => a.id !== id)
console.log('Deleting achievement:', id)
}
const resetAchievementForm = () => {
Object.assign(achievementForm, {
title: '',
description: '',
type: '',
date: '',
certificate: null,
owner: null
})
}
const createValue = (val, done) => {
if (val.length > 0) {
if (!technologyOptions.value.includes(val)) {
technologyOptions.value.push(val)
}
done(val, 'toggle')
}
}
return {
activeTab,
// Dialogs
memberDialog,
contestDialog,
projectDialog,
achievementDialog,
// Editing states
editingMember,
editingContest,
editingProject,
editingAchievement,
// Forms
teamForm,
memberForm,
contestForm,
projectForm,
achievementForm,
// Data
members,
contests,
projects,
achievements,
technologyOptions,
// Methods
onSubmitTeam,
showAddMemberDialog,
editMember,
onSubmitMember,
deleteMember,
showAddContestDialog,
editContest,
onSubmitContest,
deleteContest,
showAddProjectDialog,
editProject,
onSubmitProject,
deleteProject,
showAddAchievementDialog,
editAchievement,
onSubmitAchievement,
deleteAchievement,
createValue
}
}
}
</script>
<style scoped>
.q-tab-panel {
padding: 16px 0;
}
.q-card {
box-shadow: 0 1px 5px rgba(0,0,0,0.2), 0 2px 2px rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12);
}
.q-item-section.side {
padding-left: 16px;
}
.text-h4, .text-h5, .text-h6 {
font-weight: 500;
}
.q-chip {
margin-bottom: 4px;
}
@media (max-width: 600px) {
.gt-xs {
display: none;
}
}
</style>

View File

@ -1,15 +1,17 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { isAuthenticated as checkAuth, isAdmin as checkAdmin } from '../utils/auth.js'
const router = useRouter()
// Реактивное вычисление статуса авторизации
const isAuthenticated = computed(() => !!localStorage.getItem('access_token'))
const isAuthenticated = computed(() => checkAuth())
const isAdmin = computed(() => checkAdmin())
const goToLogin = () => router.push('/login')
const goToAdmin = () => router.push('/admin')
const logout = () => {
localStorage.removeItem('access_token')
router.go(0) // Принудительное обновление страницы для обновления состояния
@ -22,6 +24,15 @@ const logout = () => {
<div v-if="isAuthenticated" class="q-gutter-sm">
<q-btn label="Редактировать профиль" color="secondary" />
<q-btn
v-if="isAdmin"
label="Управление данными"
color="accent"
@click="goToAdmin"
icon="admin_panel_settings"
/>
<q-btn label="Выход" color="negative" @click="logout" />
</div>
@ -43,4 +54,4 @@ const logout = () => {
min-height: 100vh;
gap: 20px;
}
</style>
</style>

View File

@ -1,13 +1,23 @@
import {createRouter, createWebHistory} from 'vue-router'
import LoginPage from "../pages/LoginPage.vue"
import HomePage from "../pages/HomePage.vue"
import AdminPage from "../pages/AdminPage.vue"
import { isAuthenticated, getUserRole } from "../utils/auth.js"
const routes = [
{
path: '/',
component: HomePage
},
{path: '/login', component: LoginPage}
{
path: '/login',
component: LoginPage
},
{
path: '/admin',
component: AdminPage,
meta: { requiresAuth: true, requiresAdmin: true }
}
]
const router = createRouter({
@ -16,13 +26,30 @@ const router = createRouter({
})
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('access_token')
const authenticated = isAuthenticated()
if (to.path === '/login' && isAuthenticated) {
next('/') // теперь редирект на главную страницу
} else {
next()
// Если переходим на страницу логина и уже авторизованы
if (to.path === '/login' && authenticated) {
next('/')
return
}
// Проверка доступа к админ-панели
if (to.meta.requiresAdmin) {
if (!authenticated) {
next('/login')
return
}
const userRole = getUserRole()
if (userRole !== 'admin') {
// Редирект на главную, если не админ
next('/')
return
}
}
next()
})
export default router
export default router

35
WEB/src/utils/auth.js Normal file
View File

@ -0,0 +1,35 @@
export const decodeToken = (token) => {
try {
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
)
return JSON.parse(jsonPayload)
} catch (error) {
console.error('Ошибка декодирования токена:', error)
return null
}
}
export const getUserRole = () => {
const token = localStorage.getItem('access_token')
if (!token) return null
const decoded = decodeToken(token)
return decoded?.role || null
}
export const isAuthenticated = () => {
return !!localStorage.getItem('access_token')
}
export const isAdmin = () => {
return getUserRole() === 'admin'
}