feat: первое подключение фронтенда
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 54s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 27s
🚀 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 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 54s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 27s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 6s
This commit is contained in:
1 parent
71e7d84e0f
commit
779b6aba77
21 files changed
+942
-365
No files matched your search
+137
-68
@@ -1,90 +1,159 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { User, UserRole } from '@/types'
|
||||
import { computed, ref } from 'vue'
|
||||
import { authApi } from '@/api'
|
||||
import { mapApiUser } from '@/api/mappers'
|
||||
import { buildApiUrl, setApiAccessToken } from '@/api/client'
|
||||
import type { AuthResponse } from '@/api/types'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const defaultUsers: Record<UserRole, User> = {
|
||||
student: {
|
||||
id: 'stu-1',
|
||||
name: 'Алексей Морозов',
|
||||
email: 'a.morozov@sfedu.ru',
|
||||
role: 'student',
|
||||
institute: 'ИКТИБ',
|
||||
department: 'Программная инженерия',
|
||||
year: 3,
|
||||
direction: 'Программная инженерия',
|
||||
coins: 340,
|
||||
level: 3,
|
||||
xp: 120,
|
||||
lecturesAttended: 12,
|
||||
hoursLearned: 18.5,
|
||||
achievements: ['1', '2', '3'],
|
||||
},
|
||||
teacher: {
|
||||
id: 't-1',
|
||||
name: 'Михаил Сергеевич Волков',
|
||||
email: 'm.volkov@sfedu.ru',
|
||||
role: 'teacher',
|
||||
institute: 'ИКТИБ',
|
||||
department: 'каф. Информатики',
|
||||
year: 0,
|
||||
direction: 'Информатика и вычислительная техника',
|
||||
coins: 90,
|
||||
level: 4,
|
||||
xp: 240,
|
||||
lecturesAttended: 24,
|
||||
hoursLearned: 56,
|
||||
achievements: ['1', '2', '3', '4'],
|
||||
},
|
||||
admin: {
|
||||
id: 'adm-1',
|
||||
name: 'Виктор Алексеев',
|
||||
email: 'admin@sfedu.ru',
|
||||
role: 'admin',
|
||||
institute: 'ЮФУ',
|
||||
department: 'Администрация',
|
||||
year: 0,
|
||||
direction: 'Цифровое развитие',
|
||||
coins: 0,
|
||||
level: 5,
|
||||
xp: 500,
|
||||
lecturesAttended: 0,
|
||||
hoursLearned: 0,
|
||||
achievements: [],
|
||||
},
|
||||
const TOKEN_STORAGE_KEY = 'universe.accessToken'
|
||||
|
||||
function applyAuthResponse(response: AuthResponse) {
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, response.accessToken)
|
||||
setApiAccessToken(response.accessToken)
|
||||
return mapApiUser(response.user)
|
||||
}
|
||||
|
||||
function getAuthReturnUrl() {
|
||||
return import.meta.env.VITE_AUTH_RETURN_URL || '/auth/callback'
|
||||
}
|
||||
|
||||
function getAbsoluteAuthReturnUrl() {
|
||||
return new URL(getAuthReturnUrl(), window.location.origin).toString()
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref<User | null>(null)
|
||||
const isAuthenticated = ref(false)
|
||||
const loading = ref(false)
|
||||
const initialized = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const accessToken = ref<string | null>(localStorage.getItem(TOKEN_STORAGE_KEY))
|
||||
|
||||
async function login(role: UserRole = 'student', shouldFail = false) {
|
||||
if (accessToken.value) setApiAccessToken(accessToken.value)
|
||||
|
||||
const isAuthenticated = computed(() => Boolean(user.value && accessToken.value))
|
||||
|
||||
async function hydrateFromResponse(response: AuthResponse) {
|
||||
accessToken.value = response.accessToken
|
||||
user.value = applyAuthResponse(response)
|
||||
error.value = null
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
if (initialized.value) return isAuthenticated.value
|
||||
loading.value = true
|
||||
error.value = null
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
if (shouldFail) {
|
||||
loading.value = false
|
||||
error.value = 'Не удалось подтвердить доступ через ЮФУ. Попробуйте еще раз.'
|
||||
|
||||
try {
|
||||
const refreshed = await authApi.refresh()
|
||||
await hydrateFromResponse(refreshed)
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
return true
|
||||
} catch (refreshError) {
|
||||
if (accessToken.value) {
|
||||
try {
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
return true
|
||||
} catch {
|
||||
// Fall through to local cleanup below.
|
||||
}
|
||||
}
|
||||
clearSession()
|
||||
error.value = refreshError instanceof Error ? refreshError.message : null
|
||||
return false
|
||||
} finally {
|
||||
initialized.value = true
|
||||
loading.value = false
|
||||
}
|
||||
user.value = { ...defaultUsers[role] }
|
||||
isAuthenticated.value = true
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function startMicrosoftLogin() {
|
||||
window.location.assign(buildApiUrl('/auth/login/microsoft', { returnUrl: getAuthReturnUrl() }))
|
||||
return true
|
||||
}
|
||||
|
||||
function logout() {
|
||||
async function completeMicrosoftLogin(code: string, state: string | null) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const redirectUri = getAbsoluteAuthReturnUrl()
|
||||
const response = await authApi.loginMicrosoft(code, redirectUri)
|
||||
await hydrateFromResponse(response)
|
||||
initialized.value = true
|
||||
return true
|
||||
} catch (err) {
|
||||
clearSession()
|
||||
error.value = err instanceof Error ? err.message : 'Ошибка авторизации через Microsoft.'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function completeTokenLogin(token: string) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
accessToken.value = token
|
||||
localStorage.setItem(TOKEN_STORAGE_KEY, token)
|
||||
setApiAccessToken(token)
|
||||
const me = await authApi.me()
|
||||
user.value = mapApiUser(me)
|
||||
initialized.value = true
|
||||
return true
|
||||
} catch (err) {
|
||||
clearSession()
|
||||
error.value = err instanceof Error ? err.message : 'Не удалось получить пользователя после входа.'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
loading.value = true
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch {
|
||||
// Local cleanup is still correct if the server session is already gone.
|
||||
} finally {
|
||||
clearSession()
|
||||
initialized.value = true
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
user.value = null
|
||||
isAuthenticated.value = false
|
||||
accessToken.value = null
|
||||
localStorage.removeItem(TOKEN_STORAGE_KEY)
|
||||
setApiAccessToken(null)
|
||||
}
|
||||
|
||||
function switchRole(role?: UserRole) {
|
||||
if (!user.value) return
|
||||
const roles: UserRole[] = ['student', 'teacher', 'admin']
|
||||
const nextRole = (role ?? roles[(roles.indexOf(user.value.role) + 1) % roles.length]) as UserRole
|
||||
user.value = { ...defaultUsers[nextRole] }
|
||||
function setUser(nextUser: User) {
|
||||
user.value = nextUser
|
||||
}
|
||||
|
||||
return { user, isAuthenticated, loading, error, login, logout, switchRole }
|
||||
function switchRole() {
|
||||
error.value = 'Смена роли доступна только через backend.'
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
accessToken,
|
||||
isAuthenticated,
|
||||
loading,
|
||||
initialized,
|
||||
error,
|
||||
initialize,
|
||||
startMicrosoftLogin,
|
||||
completeMicrosoftLogin,
|
||||
completeTokenLogin,
|
||||
logout,
|
||||
clearSession,
|
||||
setUser,
|
||||
switchRole,
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user