diff --git a/backend/base/settings.py b/backend/base/settings.py
index 1e7ce41..c59cf32 100644
--- a/backend/base/settings.py
+++ b/backend/base/settings.py
@@ -53,6 +53,12 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
+REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',
+ 'rest_framework.authentication.SessionAuthentication',
+ ],
+}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
diff --git a/frontend/app/(urls)/account/layout.tsx b/frontend/app/(urls)/account/layout.tsx
new file mode 100644
index 0000000..954e8b0
--- /dev/null
+++ b/frontend/app/(urls)/account/layout.tsx
@@ -0,0 +1,57 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import AccountSidebar from '@/components/AccountSidebar'
+import Loader from '@/components/ui/Loader'
+import { RiUser3Line } from 'react-icons/ri'
+import { CgNotes } from 'react-icons/cg'
+import { FaStar } from 'react-icons/fa6'
+import useUserStore from '@/app/store/userStore'
+
+export default function AccountLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const [isLoading, setIsLoading] = useState(true)
+ const router = useRouter()
+ const { isAuthenticated, user } = useUserStore()
+
+ useEffect(() => {
+ if (!isAuthenticated || !user) {
+ router.replace('/login')
+ return
+ }
+
+ const timer = setTimeout(() => {
+ setIsLoading(false)
+ }, 300)
+
+ return () => clearTimeout(timer)
+ }, [isAuthenticated, user, router])
+
+ if (!isAuthenticated || !user || isLoading) {
+ return
+ }
+
+ const userNavigation = [
+ { name: 'Профиль', href: '/account', icon: RiUser3Line },
+ { name: 'Мои маршруты', href: '/account/routes', icon: CgNotes },
+ ]
+
+ return (
+
+ )
+}
diff --git a/frontend/app/(urls)/login/ClientView.tsx b/frontend/app/(urls)/login/ClientView.tsx
new file mode 100644
index 0000000..d0dbd5b
--- /dev/null
+++ b/frontend/app/(urls)/login/ClientView.tsx
@@ -0,0 +1,114 @@
+import React from 'react'
+import { useForm } from '@/app/hooks/useForm'
+import Button from '@/components/ui/Button'
+// import LoginButton from '@/app/components/ui/LoginButton'
+import { HiOutlineEye, HiOutlineEyeOff } from 'react-icons/hi'
+import showToast from '@/components/ui/Toast'
+import { useRouter } from 'next/navigation'
+import { signIn } from 'next-auth/react'
+// import PasswordRecovery from '@/app/components/ui/PasswordRecovery'
+
+const validationRules = {
+ email: { required: true },
+ password: { required: true, minLength: 8 },
+}
+
+const ClientView = () => {
+ const router = useRouter()
+
+ const {
+ values,
+ isVisible,
+ handleChange,
+ handleSubmit,
+ togglePasswordVisibility,
+ } = useForm(
+ {
+ email: '',
+ password: '',
+ },
+ validationRules,
+ async (values) => {
+ try {
+ const result = await signIn('credentials', {
+ email: values.email,
+ password: values.password,
+ redirect: false,
+ })
+
+ if (result?.error) {
+ showToast({ type: 'error', message: result.error })
+ return
+ }
+
+ showToast({ type: 'success', message: 'Авторизация успешна!' })
+ router.push('/account')
+ } catch {
+ showToast({ type: 'error', message: 'Ошибка при входе в аккаунт' })
+ }
+ }
+ )
+
+ return (
+ <>
+
+
+
+
+ {/* */}
+ >
+ )
+}
+
+export default ClientView
diff --git a/frontend/app/(urls)/login/page.tsx b/frontend/app/(urls)/login/page.tsx
index 2868bf2..2d16176 100644
--- a/frontend/app/(urls)/login/page.tsx
+++ b/frontend/app/(urls)/login/page.tsx
@@ -1,7 +1,60 @@
-import React from 'react'
+'use client'
-const page = () => {
- return page
+import React, { useState, useEffect } from 'react'
+import Link from 'next/link'
+import { useRouter } from 'next/navigation'
+import Loader from '@/components/ui/Loader'
+import useUserStore from '@/app/store/userStore'
+import ClientView from './ClientView'
+
+const LoginPage = () => {
+ const router = useRouter()
+ const { isAuthenticated } = useUserStore()
+ const [isClient, setIsClient] = useState(true)
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ // проверяем логин
+ if (isAuthenticated) {
+ // распределяем
+ if (isClient) {
+ router.replace('/account')
+ }
+ return
+ }
+
+ const timer = setTimeout(() => {
+ setIsLoading(false)
+ }, 300)
+
+ return () => clearTimeout(timer)
+ }, [isAuthenticated, router, isClient])
+
+ if (isLoading) {
+ return
+ }
+
+ return (
+
+
+
+
Рады видеть Вас снова!
+
+ Пожалуйста, авторизуйтесь, чтобы продолжить
+
+
+
+
+
+
+ Впервые у нас?{' '}
+
+ Зарегистрироваться
+
+
+
+
+ )
}
-export default page
+export default LoginPage
diff --git a/frontend/app/(urls)/register/admin/page.tsx b/frontend/app/(urls)/register/admin/page.tsx
deleted file mode 100644
index 2868bf2..0000000
--- a/frontend/app/(urls)/register/admin/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react'
-
-const page = () => {
- return page
-}
-
-export default page
diff --git a/frontend/app/(urls)/register/page.tsx b/frontend/app/(urls)/register/page.tsx
index 4df568e..cd77db6 100644
--- a/frontend/app/(urls)/register/page.tsx
+++ b/frontend/app/(urls)/register/page.tsx
@@ -15,9 +15,7 @@ const RegisterPage = () => {
// проверяем логин
if (isAuthenticated) {
// распределяем
- if (!isClient) {
- router.replace('/admin')
- } else {
+ if (isClient) {
router.replace('/account')
}
return
diff --git a/frontend/app/types/index.ts b/frontend/app/types/index.ts
index 4697a2d..1a805a1 100644
--- a/frontend/app/types/index.ts
+++ b/frontend/app/types/index.ts
@@ -1,4 +1,5 @@
import { StaticImageData } from 'next/image'
+import { IconType } from 'react-icons'
export interface TextInputProps {
value: string
@@ -121,3 +122,14 @@ export interface PhoneInputProps {
className?: string
operatorsInfo?: boolean
}
+
+export interface NavigationItem {
+ name: string
+ href: string
+ icon: IconType
+}
+
+export interface AccountSidebarProps {
+ user: User
+ navigation: NavigationItem[]
+}
diff --git a/frontend/components/AccountSidebar.tsx b/frontend/components/AccountSidebar.tsx
new file mode 100644
index 0000000..02067e8
--- /dev/null
+++ b/frontend/components/AccountSidebar.tsx
@@ -0,0 +1,110 @@
+'use client'
+
+import React from 'react'
+import Link from 'next/link'
+import Image from 'next/image'
+import { usePathname } from 'next/navigation'
+import { AccountSidebarProps } from '@/app/types'
+import Logout from './Logout'
+import noPhoto from '../public/images/noPhoto.png'
+
+const AccountSidebar: React.FC = ({
+ user,
+ navigation,
+}) => {
+ const pathname = usePathname()
+
+ if (!user) {
+ return null
+ }
+ const navigationItems = navigation
+
+ const getAccountTypeStyles = (accountType: string) => {
+ switch (accountType.toLowerCase()) {
+ case 'free':
+ return 'bg-gray-400 text-white'
+ case 'pro':
+ return 'bg-orage/70 text-white'
+ case 'premium':
+ return 'bg-blue-300'
+ default:
+ return 'bg-gray-200'
+ }
+ }
+
+ return (
+
+
+
+ {user.image ? (
+
+ ) : (
+
+ )}
+
+
+
{user.name || 'Пользователь'}
+
+ ID: {user.uuid || 'Not available'}
+
+ {user.account_type && (
+
+ {user.account_type.charAt(0).toUpperCase() +
+ user.account_type.slice(1)}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default AccountSidebar
diff --git a/frontend/components/Logout.tsx b/frontend/components/Logout.tsx
new file mode 100644
index 0000000..dece582
--- /dev/null
+++ b/frontend/components/Logout.tsx
@@ -0,0 +1,43 @@
+import React from 'react'
+import { useRouter } from 'next/navigation'
+import { signOut } from 'next-auth/react'
+import Button from './ui/Button'
+import { IoExitOutline } from 'react-icons/io5'
+import useUserStore from '@/app/store/userStore'
+
+const Logout = () => {
+ const API_URL = process.env.NEXT_PUBLIC_API_URL
+ const router = useRouter()
+
+ const handleLogout = async () => {
+ try {
+ // бекенд чистит куки
+ await fetch(`${API_URL}/logout`, {
+ method: 'POST',
+ credentials: 'include',
+ })
+
+ // чистим стор
+ useUserStore.getState().logout()
+
+ // если логин был гугловый
+ await signOut({ redirect: false })
+
+ // редирект
+ router.push('/')
+ router.refresh() // обновляем страницу чтобы обновить стейты
+ } catch (error) {
+ console.error('Logout error:', error)
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default Logout
diff --git a/frontend/components/ui/Loader.tsx b/frontend/components/ui/Loader.tsx
new file mode 100644
index 0000000..9efb07a
--- /dev/null
+++ b/frontend/components/ui/Loader.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+
+const Loader = () => {
+ return (
+
+
+ {/* пульсирующие круги */}
+
+
+
+ {/* бегущий блик */}
+
+
+
+ )
+}
+
+export default Loader
diff --git a/frontend/middlewares/middleware.ts b/frontend/middlewares/middleware.ts
new file mode 100644
index 0000000..1813b46
--- /dev/null
+++ b/frontend/middlewares/middleware.ts
@@ -0,0 +1,36 @@
+import { NextResponse } from 'next/server'
+import type { NextRequest } from 'next/server'
+import { jwtDecode } from 'jwt-decode'
+
+interface JWTPayload {
+ exp?: number
+}
+
+export function middleware(request: NextRequest) {
+ const accessToken = request.cookies.get('accessToken')?.value
+ const refreshToken = request.cookies.get('refreshToken')?.value
+ const isAuthPage = request.nextUrl.pathname.startsWith('/account')
+
+ if (isAuthPage && (!accessToken || !refreshToken)) {
+ return NextResponse.redirect(new URL('/login', request.url))
+ }
+
+ if (isAuthPage && accessToken) {
+ try {
+ const decoded = jwtDecode(accessToken)
+
+ if (decoded.exp && decoded.exp < Date.now() / 1000) {
+ return NextResponse.redirect(new URL('/login', request.url))
+ }
+ } catch (error) {
+ console.error('Token decode error:', error)
+ return NextResponse.redirect(new URL('/login', request.url))
+ }
+ }
+
+ return NextResponse.next()
+}
+
+export const config = {
+ matcher: ['/account/:path*'],
+}
diff --git a/frontend/public/images/noPhoto.png b/frontend/public/images/noPhoto.png
new file mode 100644
index 0000000..4f80d37
Binary files /dev/null and b/frontend/public/images/noPhoto.png differ