This commit is contained in:
Андрей Дувакин 2025-05-31 11:50:03 +05:00
commit 673f20188e
17 changed files with 511 additions and 71 deletions

View File

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db from app.database.session import get_db
@ -55,3 +55,22 @@ async def delete_profile(
): ):
profiles_service = ProfilesService(db) profiles_service = ProfilesService(db)
return await profiles_service.delete(profile_id, user) return await profiles_service.delete(profile_id, user)
@router.get(
'/user/{user_id}/',
response_model=Optional[ProfileEntity],
summary='Get profile by user ID',
description='Retrieve profile data by user ID',
)
async def get_profile_by_user_id(
user_id: int,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
profiles_service = ProfilesService(db)
profile = await profiles_service.get_by_user_id(user_id)
return profile

View File

@ -102,6 +102,17 @@ class ProfilesService:
return self.model_to_entity(result) return self.model_to_entity(result)
async def get_by_user_id(self, user_id: int) -> Optional[ProfileEntity]:
user = await self.users_repository.get_by_id(user_id)
if user is None:
raise HTTPException(status_code=404, detail='User not found')
profile_model = await self.profiles_repository.get_by_id(user.profile_id)
if not profile_model:
raise HTTPException(status_code=404, detail='Profile not found')
return self.model_to_entity(profile_model)
@staticmethod @staticmethod
def model_to_entity(profile_model: Profile) -> ProfileEntity: def model_to_entity(profile_model: Profile) -> ProfileEntity:
return ProfileEntity( return ProfileEntity(

View File

@ -33,8 +33,8 @@ class TeamsService:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Team not found")
team_model.title = team.title team_model.title = team.title
team_model.description = team_model.description team_model.description = team.description
team_model.git_url = team_model.git_url team_model.git_url = team.git_url
await self.teams_repository.update(team_model) await self.teams_repository.update(team_model)

View File

@ -6,10 +6,13 @@ const loginUser = async (loginData) => {
const response = await axios.post(`${CONFIG.BASE_URL}/auth/`, loginData, { const response = await axios.post(`${CONFIG.BASE_URL}/auth/`, loginData, {
withCredentials: true, withCredentials: true,
}); });
return response.data.access_token;
const { access_token, user_id } = response.data;
return { access_token, user_id };
} catch (error) { } catch (error) {
if (error.status === 401) { if (error.response?.status === 401) {
throw new Error("Неверное имя пользователя или пароль") throw new Error("Неверное имя пользователя или пароль");
} }
throw new Error(error.message); throw new Error(error.message);

View File

@ -0,0 +1,18 @@
import axios from "axios";
import CONFIG from "@/core/config.js";
const fetchContests = async () => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/contests`, {
withCredentials: true,
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Нет доступа к конкурсам (401)");
}
throw new Error(error.message);
}
};
export default fetchContests;

View File

@ -0,0 +1,17 @@
import axios from 'axios'
import CONFIG from '../../core/config.js'
const getUserProfile = async (user_id, token) => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/profiles/${user_id}/`, {
headers: {
Authorization: `Bearer ${token}`
}
})
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || 'Ошибка получения профиля')
}
}
export default getUserProfile

View File

@ -0,0 +1,18 @@
import axios from "axios";
import CONFIG from "@/core/config.js";
const fetchProjects = async () => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/projects`, {
withCredentials: true,
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Нет доступа к проектам (401)");
}
throw new Error(error.message);
}
};
export default fetchProjects;

View File

@ -0,0 +1,24 @@
import axios from "axios";
import CONFIG from "@/core/config.js";
const fetchTeams = async () => {
try {
const token = localStorage.getItem("access_token");
const response = await axios.get(`${CONFIG.BASE_URL}/teams`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Нет доступа к командам (401)");
} else if (error.response?.status === 403) {
throw new Error("Доступ запрещён (403)");
}
throw new Error(error.message);
}
};
export default fetchTeams;

View File

@ -0,0 +1,17 @@
import axios from 'axios'
import CONFIG from '@/core/config.js'
const updateTeam = async (team) => {
try {
const response = await axios.put(
`${CONFIG.BASE_URL}/teams/${team.id}`,
team,
{ withCredentials: true }
)
return response.data
} catch (error) {
throw new Error(error.response?.data?.detail || error.message)
}
}
export default updateTeam

View File

@ -0,0 +1,18 @@
import axios from "axios";
import CONFIG from "@/core/config.js";
const fetchUsers = async () => {
try {
const response = await axios.get(`${CONFIG.BASE_URL}/users`, {
withCredentials: true,
});
return response.data;
} catch (error) {
if (error.response?.status === 401) {
throw new Error("Нет доступа к пользователям (401)");
}
throw new Error(error.message);
}
};
export default fetchUsers;

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>

View File

@ -1,8 +1,33 @@
import { createApp } from 'vue' import {createApp} from 'vue'
import App from './App.vue' import App from './App.vue'
import { Quasar, Notify, Dialog, Loading, QInput, QBtn, QForm, QCard, QCardSection } from 'quasar'
import router from './router' import router from './router'
import {
Quasar,
Notify,
Dialog,
Loading,
QInput,
QBtn,
QForm,
QCard,
QCardSection,
QLayout,
QPageContainer,
QPage,
QTabs,
QTab,
QTabPanels,
QTabPanel,
QHeader,
QTable,
QSeparator,
QCardActions,
QDialog,
QIcon
} from 'quasar'
import '@quasar/extras/material-icons/material-icons.css' import '@quasar/extras/material-icons/material-icons.css'
@ -11,8 +36,13 @@ import 'quasar/src/css/index.sass'
const app = createApp(App) const app = createApp(App)
app.use(Quasar, { app.use(Quasar, {
plugins: { Notify, Dialog, Loading }, // уведомления, диалоги, загрузка plugins: {Notify, Dialog, Loading},
components: { QInput, QBtn, QForm, QCard, QCardSection } // обязательно указать используемые компоненты components: {
QInput, QBtn, QForm, QCard, QCardSection,
QLayout, QPageContainer, QPage,
QTabs, QTab, QTabPanels, QTabPanel, QHeader,QTable,
QSeparator, QCardActions, QDialog, QIcon
}
}) })
app.use(router) app.use(router)

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

@ -0,0 +1,271 @@
<template>
<q-layout view="lHh Lpr lFf" class="bg-violet-strong text-dark">
<q-header elevated class="bg-transparent">
<q-tabs
v-model="tab"
class="text-white"
active-color="white"
indicator-color="white"
align="justify"
dense
animated
@update:model-value="loadData"
>
<q-tab name="users" label="Пользователи" />
<q-tab name="teams" label="Команды" />
<q-tab name="projects" label="Проекты" />
<q-tab name="contests" label="Конкурсы" />
</q-tabs>
</q-header>
<q-page-container class="q-pa-md">
<q-tab-panels v-model="tab" animated transition-prev="slide-right" transition-next="slide-left">
<!-- Пользователи -->
<q-tab-panel name="users">
<div class="violet-card q-pa-md">
<q-table
title="Пользователи"
:rows="users"
:columns="userColumns"
row-key="id"
@row-click="openEdit('users', $event)"
:loading="loadingUsers"
dense
flat
/>
</div>
</q-tab-panel>
<!-- Команды -->
<q-tab-panel name="teams">
<div class="violet-card q-pa-md">
<q-table
title="Команды"
:rows="teams"
:columns="teamColumns"
row-key="id"
@row-click="openEdit('teams', $event)"
:loading="loadingTeams"
dense
flat
/>
</div>
</q-tab-panel>
<!-- Проекты -->
<q-tab-panel name="projects">
<div class="violet-card q-pa-md">
<q-table
title="Проекты"
:rows="projects"
:columns="projectColumns"
row-key="id"
@row-click="openEdit('projects', $event)"
:loading="loadingProjects"
dense
flat
/>
</div>
</q-tab-panel>
<!-- Конкурсы -->
<q-tab-panel name="contests">
<div class="violet-card q-pa-md">
<q-table
title="Конкурсы"
:rows="contests"
:columns="contestColumns"
row-key="id"
@row-click="openEdit('contests', $event)"
:loading="loadingContests"
dense
flat
/>
</div>
</q-tab-panel>
</q-tab-panels>
</q-page-container>
<!-- Модальное окно редактирования -->
<q-dialog v-model="dialogVisible" persistent>
<q-card style="min-width: 350px; max-width: 700px;">
<q-card-section>
<div class="text-h6">Редактирование {{ dialogType }}</div>
</q-card-section>
<q-separator />
<q-card-section>
<pre>{{ dialogData }}</pre>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Закрыть" color="primary" @click="closeDialog" />
</q-card-actions>
</q-card>
</q-dialog>
</q-layout>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
// Импорты API
import fetchTeams from '@/api/teams/getTeams.js'
import fetchUsers from '@/api/users/getUsers.js'
import fetchProjects from '@/api/projects/getProjects.js'
import fetchContests from '@/api/contests/getContests.js'
import updateTeam from '@/api/teams/updateTeam.js'
// Активная вкладка
const tab = ref('users')
// Данные
const users = ref([])
const teams = ref([])
const projects = ref([])
const contests = ref([])
// Загрузка
const loadingUsers = ref(false)
const loadingTeams = ref(false)
const loadingProjects = ref(false)
const loadingContests = ref(false)
// Колонки
const userColumns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true },
{ name: 'name', label: 'Имя', field: 'name', sortable: true },
{ name: 'email', label: 'Email', field: 'email', sortable: true },
]
const teamColumns = [
{ name: 'title', label: 'Название команды', field: 'title', sortable: true },
{ name: 'description', label: 'Описание', field: 'description', sortable: true },
{ name: 'logo', label: 'Логотип', field: 'logo', sortable: true },
{ name: 'git_url', label: 'Git URL', field: 'git_url', sortable: true },
]
const projectColumns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true },
{ name: 'projectName', label: 'Проект', field: 'projectName', sortable: true },
]
const contestColumns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true },
{ name: 'contestName', label: 'Конкурс', field: 'contestName', sortable: true },
]
// Модалка
const dialogVisible = ref(false)
const dialogData = ref({})
const dialogType = ref('')
// Функция открытия редактирования
function openEdit(type, row) {
dialogType.value = type
// Клонируем объект, чтобы не менять данные в таблице до сохранения
dialogData.value = JSON.parse(JSON.stringify(row))
dialogVisible.value = true
}
// Функция сохранения изменений
async function saveChanges() {
if (dialogType.value === 'teams') {
try {
await updateTeam(dialogData.value)
// Обновляем локальные данные команды
const index = teams.value.findIndex(t => t.id === dialogData.value.id)
if (index !== -1) {
teams.value[index] = JSON.parse(JSON.stringify(dialogData.value))
}
closeDialog()
} catch (error) {
console.error('Ошибка при сохранении:', error.message)
// Можно добавить уведомление об ошибке
}
}
}
// Закрыть модалку
function closeDialog() {
dialogVisible.value = false
}
// Загрузка данных
async function loadData(name) {
switch (name) {
case 'users':
loadingUsers.value = true
try {
users.value = await fetchUsers() || []
} catch (error) {
users.value = []
console.error(error.message)
} finally {
loadingUsers.value = false
}
break
case 'teams':
loadingTeams.value = true
try {
teams.value = await fetchTeams() || []
} catch (error) {
teams.value = []
console.error(error.message)
} finally {
loadingTeams.value = false
}
break
case 'projects':
loadingProjects.value = true
try {
projects.value = await fetchProjects() || []
} catch (error) {
projects.value = []
console.error(error.message)
} finally {
loadingProjects.value = false
}
break
case 'contests':
loadingContests.value = true
try {
contests.value = await fetchContests() || []
} catch (error) {
contests.value = []
console.error(error.message)
} finally {
loadingContests.value = false
}
break
}
}
// Начальная загрузка
onMounted(() => loadData(tab.value))
// Загрузка при смене вкладки
watch(tab, (newTab) => {
loadData(newTab)
})
</script>
<style scoped>
.bg-violet-strong {
background: linear-gradient(135deg, #4f046f 0%, #7c3aed 100%);
min-height: 100vh;
}
.violet-card {
border-radius: 22px;
background: #ede9fe;
box-shadow: 0 6px 32px rgba(124, 58, 237, 0.13), 0 1.5px 6px rgba(124, 58, 237, 0.10);
}
</style>

View File

@ -2,7 +2,11 @@
import { ref, onMounted, onBeforeUnmount } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Notify } from 'quasar' import { Notify } from 'quasar'
import loginUser from "../api/auth/loginRequest.js" import axios from 'axios'
import CONFIG from '../core/config.js'
import loginUser from '../api/auth/loginRequest.js'
const router = useRouter() const router = useRouter()
const login = ref('') const login = ref('')
@ -11,14 +15,12 @@ const loading = ref(false)
const marqueeTrack = ref(null) const marqueeTrack = ref(null)
const marqueeText = ref(null) const marqueeText = ref(null)
const animationDuration = ref(7) // секунд const animationDuration = ref(7)
// Динамически рассчитываем длительность анимации в зависимости от ширины текста
const updateAnimation = () => { const updateAnimation = () => {
if (marqueeTrack.value && marqueeText.value) { if (marqueeTrack.value && marqueeText.value) {
const textWidth = marqueeText.value.offsetWidth const textWidth = marqueeText.value.offsetWidth
const containerWidth = marqueeTrack.value.offsetWidth / 2 const containerWidth = marqueeTrack.value.offsetWidth / 2
// Скорость: 100px/sec (можно подстроить)
const speed = 100 const speed = 100
animationDuration.value = (textWidth + containerWidth) / speed animationDuration.value = (textWidth + containerWidth) / speed
marqueeTrack.value.style.setProperty('--marquee-width', `${textWidth}px`) marqueeTrack.value.style.setProperty('--marquee-width', `${textWidth}px`)
@ -27,7 +29,7 @@ const updateAnimation = () => {
} }
onMounted(() => { onMounted(() => {
setTimeout(updateAnimation, 100) // Дать DOM отрисоваться setTimeout(updateAnimation, 100)
window.addEventListener('resize', updateAnimation) window.addEventListener('resize', updateAnimation)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@ -37,12 +39,21 @@ onBeforeUnmount(() => {
const authorisation = async () => { const authorisation = async () => {
loading.value = true loading.value = true
try { try {
const accessToken = await loginUser({ const { access_token, user_id } = await loginUser({
login: login.value, login: login.value,
password: password.value password: password.value
}) })
localStorage.setItem('access_token', accessToken) localStorage.setItem('access_token', access_token)
localStorage.setItem('user_id', user_id)
const profileResponse = await axios.get(`${CONFIG.BASE_URL}/profiles/user/${user_id}/`, {
headers: { Authorization: `Bearer ${access_token}` }
})
const roleId = profileResponse.data.role_id
Notify.create({ Notify.create({
type: 'positive', type: 'positive',
@ -50,7 +61,21 @@ const authorisation = async () => {
icon: 'check_circle' icon: 'check_circle'
}) })
console.log('Role ID:', roleId)
if (roleId === 1) {
console.log('Переход на /admin')
router.push('/admin')
} else {
console.log('Переход на /')
router.push('/') router.push('/')
}
if (roleId === 1) {
router.push('/admin')
} else {
router.push('/')
}
} catch (error) { } catch (error) {
Notify.create({ Notify.create({
type: 'negative', type: 'negative',
@ -63,6 +88,7 @@ const authorisation = async () => {
} }
</script> </script>
<template> <template>
<div class="fullscreen flex flex-center bg-violet-strong"> <div class="fullscreen flex flex-center bg-violet-strong">
<q-card class="q-pa-xl shadow-violet card-animate violet-card" style="width: 370px; max-width: 92vw;"> <q-card class="q-pa-xl shadow-violet card-animate violet-card" style="width: 370px; max-width: 92vw;">

View File

@ -1,13 +1,12 @@
import {createRouter, createWebHistory} from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import LoginPage from "../pages/LoginPage.vue" import LoginPage from "../pages/LoginPage.vue"
import HomePage from "../pages/HomePage.vue" import HomePage from "../pages/HomePage.vue"
import AdminPage from "../pages/AdminPage.vue"
const routes = [ const routes = [
{ { path: '/', component: HomePage },
path: '/', { path: '/login', component: LoginPage },
component: HomePage { path: '/admin', component: AdminPage }
},
{path: '/login', component: LoginPage}
] ]
const router = createRouter({ const router = createRouter({
@ -17,9 +16,16 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('access_token') const isAuthenticated = !!localStorage.getItem('access_token')
const userId = localStorage.getItem('user_id')
if (to.path === '/login' && isAuthenticated) { if (to.path === '/login' && isAuthenticated) {
next('/') // теперь редирект на главную страницу next('/')
} else if (to.path === '/admin') {
if (isAuthenticated && userId === '1') {
next()
} else {
next('/')
}
} else { } else {
next() next()
} }

View File

@ -1,7 +1,12 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
}) })