feat: мультироль
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m6s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 26s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 9s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 2m6s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 26s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
This commit is contained in:
@@ -70,8 +70,8 @@ export const usersApi = {
|
||||
)
|
||||
return extractItems(payload)
|
||||
},
|
||||
setRole: (id: string | number, role: 'Student' | 'Teacher' | 'Admin') =>
|
||||
apiRequest<void>(`/users/${id}/role`, { method: 'PATCH', body: JSON.stringify(role) }),
|
||||
setRole: (id: string | number, roles: Array<'Student' | 'Teacher' | 'Admin'>) =>
|
||||
apiRequest<void>(`/users/${id}/role`, { method: 'PATCH', body: JSON.stringify(roles) }),
|
||||
setActive: (id: string | number, isActive: boolean) =>
|
||||
apiRequest<void>(`/users/${id}/active`, { method: 'PATCH', body: JSON.stringify(isActive) }),
|
||||
}
|
||||
|
||||
@@ -16,12 +16,26 @@ export function mapApiRole(role: string | undefined): UserRole {
|
||||
return 'student'
|
||||
}
|
||||
|
||||
function mapApiRoles(roles: string[] | undefined): UserRole[] {
|
||||
if (!roles?.length) return ['student']
|
||||
return Array.from(new Set(roles.map(mapApiRole)))
|
||||
}
|
||||
|
||||
function getDefaultActiveRole(roles: UserRole[]): UserRole {
|
||||
if (roles.includes('student')) return 'student'
|
||||
if (roles.includes('teacher')) return 'teacher'
|
||||
if (roles.includes('admin')) return 'admin'
|
||||
return 'student'
|
||||
}
|
||||
|
||||
export function mapApiUser(user: UserAuthDto | UserDto, stats?: UserStatsDto): User {
|
||||
const roles = mapApiRoles(user.roles)
|
||||
return {
|
||||
id: String(user.id),
|
||||
name: user.displayName || user.email || 'Пользователь UniVerse',
|
||||
email: user.email || '',
|
||||
role: mapApiRole(user.role),
|
||||
roles,
|
||||
activeRole: getDefaultActiveRole(roles),
|
||||
avatar: 'avatarUrl' in user ? user.avatarUrl ?? undefined : undefined,
|
||||
institute: 'ЮФУ',
|
||||
department: '',
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface UserAuthDto {
|
||||
id: number
|
||||
email: string
|
||||
displayName?: string | null
|
||||
role: ApiUserRole
|
||||
roles: ApiUserRole[]
|
||||
}
|
||||
|
||||
export interface UserDto extends UserAuthDto {
|
||||
|
||||
@@ -8,7 +8,7 @@ const auth = useAuthStore()
|
||||
const route = useRoute()
|
||||
|
||||
const navItems = computed(() => {
|
||||
const role = auth.user?.role ?? 'student'
|
||||
const role = auth.user?.activeRole ?? 'student'
|
||||
if (role === 'teacher') return [
|
||||
{ label: 'Дашборд', icon: 'chart-bar', to: '/teacher' },
|
||||
{ label: 'Лекции', icon: 'book-2', to: '/teacher/lectures' },
|
||||
|
||||
@@ -9,6 +9,7 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
interface NavItem { label: string; icon: string; to: string; roles: string[] }
|
||||
type AppRole = 'student' | 'teacher' | 'admin'
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ label: 'Главная', icon: 'home', to: '/', roles: ['student'] },
|
||||
@@ -30,9 +31,32 @@ const navItems: NavItem[] = [
|
||||
]
|
||||
|
||||
const visible = computed(() =>
|
||||
navItems.filter(n => auth.user && n.roles.includes(auth.user.role))
|
||||
navItems.filter(n => auth.user && n.roles.includes(auth.user.activeRole))
|
||||
)
|
||||
|
||||
const roleButtons = computed(() => {
|
||||
if (!auth.user) return []
|
||||
const labels: Record<AppRole, string> = {
|
||||
student: 'Студент',
|
||||
teacher: 'Преподаватель',
|
||||
admin: 'Администратор',
|
||||
}
|
||||
const targets: Record<AppRole, string> = {
|
||||
student: '/',
|
||||
teacher: '/teacher',
|
||||
admin: '/admin',
|
||||
}
|
||||
return auth.user.roles
|
||||
.filter(role => role !== auth.user?.activeRole)
|
||||
.map(role => ({ role, label: labels[role], to: targets[role] }))
|
||||
})
|
||||
|
||||
function switchToRole(role: AppRole, to: string) {
|
||||
if (auth.setActiveRole(role)) {
|
||||
router.push(to)
|
||||
}
|
||||
}
|
||||
|
||||
function isActive(to: string) {
|
||||
if (to === '/') return route.path === '/'
|
||||
return route.path.startsWith(to) && to !== '/'
|
||||
@@ -55,6 +79,16 @@ function isActive(to: string) {
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="role-switches" v-if="roleButtons.length">
|
||||
<button
|
||||
v-for="item in roleButtons"
|
||||
:key="item.role"
|
||||
class="role-switch-btn"
|
||||
@click="switchToRole(item.role, item.to)"
|
||||
>
|
||||
Перейти: {{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button class="logout-btn" @click="auth.logout().then(() => router.push('/login'))">
|
||||
<AppIcon class="logout-icon" icon="logout" :size="16" />
|
||||
Выйти
|
||||
@@ -110,7 +144,21 @@ function isActive(to: string) {
|
||||
box-shadow: 0 2px 8px rgba(34,197,94,0.12);
|
||||
}
|
||||
.nav-icon { flex-shrink: 0; color: currentColor; }
|
||||
.sidebar-footer { padding: 10px 18px 8px; }
|
||||
.sidebar-footer { padding: 10px 18px 8px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.role-switches { display: flex; flex-direction: column; gap: 6px; }
|
||||
.role-switch-btn {
|
||||
width: 100%;
|
||||
background: rgba(34,197,94,0.08);
|
||||
border: 1px solid rgba(34,197,94,0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-dark);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.role-switch-btn:hover { background: rgba(34,197,94,0.15); }
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
background: rgba(239,68,68,0.08);
|
||||
|
||||
@@ -21,13 +21,6 @@ const roleLabels: Record<string, string> = {
|
||||
|
||||
const unreadCount = computed(() => userStore.unreadCount())
|
||||
|
||||
function switchRole() {
|
||||
auth.switchRole()
|
||||
if (auth.user?.role === 'teacher') router.push('/teacher')
|
||||
else if (auth.user?.role === 'admin') router.push('/admin')
|
||||
else router.push('/')
|
||||
}
|
||||
|
||||
function openProfile() {
|
||||
router.push('/profile')
|
||||
}
|
||||
@@ -47,9 +40,9 @@ function openProfile() {
|
||||
<div class="topbar-right">
|
||||
<CoinChip v-if="auth.user" :amount="auth.user.coins" />
|
||||
|
||||
<button class="role-btn" @click="switchRole" v-if="auth.user">
|
||||
{{ roleLabels[auth.user.role] }} ↔
|
||||
</button>
|
||||
<span class="role-chip" v-if="auth.user">
|
||||
{{ roleLabels[auth.user.activeRole] }}
|
||||
</span>
|
||||
|
||||
<button class="notif-btn" @click="$router.push('/notifications')">
|
||||
<AppIcon icon="bell" :size="18" />
|
||||
@@ -115,19 +108,16 @@ function openProfile() {
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.role-btn {
|
||||
.role-chip {
|
||||
background: rgba(34,197,94,0.12);
|
||||
border: 1px solid rgba(34,197,94,0.3);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary-dark);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
color: var(--color-primary-dark);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.role-btn:hover { background: rgba(34,197,94,0.2); }
|
||||
.notif-btn {
|
||||
position: relative;
|
||||
background: none;
|
||||
@@ -173,6 +163,6 @@ function openProfile() {
|
||||
.topbar-center { display: none; }
|
||||
.brand-name { display: none; }
|
||||
.avatar-name { display: none; }
|
||||
.role-btn { display: none; }
|
||||
.role-chip { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,16 +39,19 @@ const router = createRouter({
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = useAuthStore()
|
||||
const resolveDefaultRoute = () => {
|
||||
if (auth.user?.activeRole === 'teacher') return '/teacher'
|
||||
if (auth.user?.activeRole === 'admin') return '/admin'
|
||||
return '/'
|
||||
}
|
||||
if (!auth.initialized && !to.meta.public) {
|
||||
await auth.initialize()
|
||||
}
|
||||
if (!to.meta.public && !auth.isAuthenticated) {
|
||||
return '/login'
|
||||
}
|
||||
if (to.meta.role && auth.user && auth.user.role !== to.meta.role) {
|
||||
if (auth.user.role === 'teacher') return '/teacher'
|
||||
if (auth.user.role === 'admin') return '/admin'
|
||||
return '/'
|
||||
if (to.meta.role && auth.user && !auth.user.roles.includes(to.meta.role as 'student' | 'teacher' | 'admin')) {
|
||||
return resolveDefaultRoute()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return true
|
||||
}
|
||||
|
||||
async function completeMicrosoftLogin(code: string, state: string | null) {
|
||||
async function completeMicrosoftLogin(code: string, _state: string | null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
@@ -136,8 +136,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
user.value = nextUser
|
||||
}
|
||||
|
||||
function switchRole() {
|
||||
error.value = 'Смена роли доступна только через backend.'
|
||||
function setActiveRole(role: User['activeRole']) {
|
||||
if (!user.value || !user.value.roles.includes(role)) return false
|
||||
user.value = { ...user.value, activeRole: role }
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -154,6 +156,6 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
logout,
|
||||
clearSession,
|
||||
setUser,
|
||||
switchRole,
|
||||
setActiveRole,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ export interface User {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
role: UserRole
|
||||
roles: UserRole[]
|
||||
activeRole: UserRole
|
||||
avatar?: string
|
||||
institute?: string
|
||||
department?: string
|
||||
|
||||
@@ -25,14 +25,20 @@ const columns = [
|
||||
|
||||
const roleLabels = { Student: 'Студент', Teacher: 'Преподаватель', Admin: 'Администратор' } as const
|
||||
const roleApi = { Студент: 'Student', Преподаватель: 'Teacher', Администратор: 'Admin' } as const
|
||||
type ApiUserRole = 'Student' | 'Teacher' | 'Admin'
|
||||
const roleSetSequence: ApiUserRole[][] = [
|
||||
['Student'],
|
||||
['Student', 'Teacher'],
|
||||
['Student', 'Teacher', 'Admin'],
|
||||
]
|
||||
|
||||
const rows = computed(() =>
|
||||
users.value.map(user => ({
|
||||
id: user.id,
|
||||
name: user.displayName || user.email,
|
||||
email: user.email,
|
||||
role: roleLabels[user.role],
|
||||
apiRole: user.role,
|
||||
role: user.roles.map(role => roleLabels[role]).join(', '),
|
||||
apiRoles: user.roles,
|
||||
institute: 'ЮФУ',
|
||||
activity: user.isActive ? 'Активен' : 'Заблокирован',
|
||||
isActive: user.isActive,
|
||||
@@ -56,14 +62,24 @@ async function fetchUsers() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(row: Record<string, any>) {
|
||||
await usersApi.setActive(row.id, !row.isActive)
|
||||
async function toggleActive(row: Record<string, unknown>) {
|
||||
const id = Number(row.id)
|
||||
const isActive = Boolean(row.isActive)
|
||||
await usersApi.setActive(id, !isActive)
|
||||
await fetchUsers()
|
||||
}
|
||||
|
||||
async function promoteRole(row: Record<string, any>) {
|
||||
const next = row.apiRole === 'Student' ? 'Teacher' : row.apiRole === 'Teacher' ? 'Admin' : 'Student'
|
||||
await usersApi.setRole(row.id, next)
|
||||
async function promoteRole(row: Record<string, unknown>) {
|
||||
const id = Number(row.id)
|
||||
const apiRoles = (Array.isArray(row.apiRoles) ? row.apiRoles : []) as ApiUserRole[]
|
||||
const currentKey = [...new Set(apiRoles)].sort().join(',')
|
||||
const currentIndex = roleSetSequence.findIndex(set => set.slice().sort().join(',') === currentKey)
|
||||
const next: ApiUserRole[] = (
|
||||
currentIndex >= 0
|
||||
? roleSetSequence[(currentIndex + 1) % roleSetSequence.length]
|
||||
: roleSetSequence[0]
|
||||
) ?? ['Student']
|
||||
await usersApi.setRole(id, next)
|
||||
await fetchUsers()
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ onMounted(async () => {
|
||||
else throw new Error('Microsoft не вернул токен авторизации.')
|
||||
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
const role = auth.user?.role
|
||||
const role = auth.user?.activeRole
|
||||
if (role === 'teacher') await router.replace('/teacher')
|
||||
else if (role === 'admin') await router.replace('/admin')
|
||||
else await router.replace('/')
|
||||
|
||||
Reference in New Issue
Block a user