Merge pull request 'front' (#1) from front into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -11,7 +11,8 @@ SMTP_USERNAME=email@example.com
|
||||
SMTP_FROM=email@example.com
|
||||
SMTP_PASSWORD=password
|
||||
|
||||
# OTP_CODE_EXPIRED_TIME = 5
|
||||
# OTP_CODE_EXPIRED_TIME=5
|
||||
# OTP_CODE_MOCKUP=0000
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
@@ -47,14 +47,3 @@ async def login_user(
|
||||
@router.post("/logout")
|
||||
async def logout_user(response: Response):
|
||||
response.delete_cookie(jwt_security.config.JWT_ACCESS_COOKIE_NAME)
|
||||
|
||||
|
||||
@router.get("/current")
|
||||
async def get_current_user(users_service: Annotated[UsersService, Depends(get_users_service)],
|
||||
payload: TokenPayload = Depends(jwt_security.access_token_required)):
|
||||
user = await users_service.get_user(int(payload.sub))
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return user
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Annotated, List
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from authx import TokenPayload
|
||||
|
||||
from app.api.dependencies import get_users_service
|
||||
from app.common.security import jwt_security
|
||||
@@ -12,7 +13,6 @@ router = APIRouter(
|
||||
tags=["Users"],
|
||||
)
|
||||
|
||||
# dependencies=[Depends(jwt_security.access_token_required)]
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def add_user(
|
||||
@@ -29,3 +29,11 @@ async def get_users(
|
||||
) -> List[UserReadSchema]:
|
||||
users = await users_service.get_users()
|
||||
return users
|
||||
|
||||
|
||||
@router.get("/current")
|
||||
async def get_current_user(
|
||||
users_service: Annotated[UsersService, Depends(get_users_service)],
|
||||
access_token_payload: TokenPayload = Depends(jwt_security.access_token_required),
|
||||
) -> UserReadSchema:
|
||||
return await users_service.get_user(int(access_token_payload.sub))
|
||||
|
||||
@@ -22,6 +22,7 @@ smtp_from = os.getenv("SMTP_FROM")
|
||||
smtp_password = os.getenv("SMTP_PASSWORD")
|
||||
|
||||
otp_code_expired_time = int(os.getenv("OTP_CODE_EXPIRED_TIME", "5")) # minutes
|
||||
otp_code_mockup = os.getenv("OTP_CODE_MOCKUP", None)
|
||||
|
||||
redis_host = os.getenv("REDIS_HOST")
|
||||
redis_port = int(os.getenv("REDIS_PORT"))
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
|
||||
import dotenv
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
BASE_DIR: str = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
@@ -10,8 +11,17 @@ if os.path.exists(os.path.join(BASE_DIR, ".env")):
|
||||
|
||||
from app.common.security import jwt_security
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
jwt_security.handle_errors(app)
|
||||
|
||||
main_router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -35,7 +35,7 @@ class AuthManager:
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
is_verified = await self._otp_service.verify_code(user.id, schema.otp_code)
|
||||
is_verified = await self._otp_service.verify_otp_code(user.id, schema.otp_code, delete_code=True)
|
||||
|
||||
if not is_verified:
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import random
|
||||
from typing import Type
|
||||
|
||||
import app.config
|
||||
from app.repositories import OTPRepository
|
||||
from app.schemas.otp_schemas import OTPReadSchema
|
||||
|
||||
@@ -11,14 +12,19 @@ class OTPService:
|
||||
|
||||
async def create_otp_code(self, user_id: int) -> OTPReadSchema:
|
||||
code = str(random.randint(1000, 9999))
|
||||
|
||||
if app.config.otp_code_mockup is not None:
|
||||
code = app.config.otp_code_mockup
|
||||
|
||||
return await self.otp_repo.add_one(user_id, code)
|
||||
|
||||
async def verify_code(self, user_id: int, code: str) -> bool:
|
||||
async def verify_otp_code(self, user_id: int, code: str, delete_code: bool = False) -> bool:
|
||||
user_otp_codes = await self.otp_repo.find_all(user_id=user_id)
|
||||
|
||||
for user_otp_code in user_otp_codes:
|
||||
if user_otp_code.code == code:
|
||||
await self.otp_repo.delete_one(user_otp_code.id)
|
||||
if delete_code:
|
||||
await self.otp_repo.delete_one(user_otp_code.id)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
0
backend/entrypoint.sh
Normal file → Executable file
0
backend/entrypoint.sh
Normal file → Executable file
36
front/.dockerignore
Normal file
36
front/.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.next
|
||||
**/.cache
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose.yml
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
**/build
|
||||
**/dist
|
||||
LICENSE
|
||||
README.md
|
||||
.nuxt
|
||||
24
front/.gitignore
vendored
Normal file
24
front/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
1
front/.nvmrc
Normal file
1
front/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v24.7.0
|
||||
52
front/.vscode/settings.json
vendored
Normal file
52
front/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimFinalNewlines": true,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
"."
|
||||
],
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
55
front/Dockerfile
Normal file
55
front/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
####################################################
|
||||
################### BASE IMAGE #####################
|
||||
####################################################
|
||||
|
||||
FROM node:24.7.0-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
####################################################
|
||||
############### INSTALL DEPENDENCIES ###############
|
||||
####################################################
|
||||
|
||||
FROM base AS deps
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci --omit=dev --prefer-offline --no-audit
|
||||
|
||||
####################################################
|
||||
#################### BUILD APP #####################
|
||||
####################################################
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
####################################################
|
||||
###################### FINAL #######################
|
||||
####################################################
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
# COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /app/.output ./.output
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nuxtuser -u 1001 && \
|
||||
chown -R nuxtuser:nodejs /app
|
||||
|
||||
USER nuxtuser
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
CMD ["node", ".output/server/index.mjs"]
|
||||
75
front/README.md
Normal file
75
front/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
15
front/app/app.config.ts
Normal file
15
front/app/app.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export default defineAppConfig({
|
||||
// https://ui.nuxt.com/getting-started/theme#design-system
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'emerald',
|
||||
neutral: 'slate',
|
||||
},
|
||||
button: {
|
||||
defaultVariants: {
|
||||
// Set default button color to neutral
|
||||
// color: 'neutral'
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
7
front/app/app.vue
Normal file
7
front/app/app.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<UApp>
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
2
front/app/assets/css/nuxtui.css
Normal file
2
front/app/assets/css/nuxtui.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
0
front/app/assets/scss/main.scss
Normal file
0
front/app/assets/scss/main.scss
Normal file
13
front/app/components/LayoutFooter/index.vue
Normal file
13
front/app/components/LayoutFooter/index.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<footer class="container mx-auto p-5">
|
||||
All rights reserved ©
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './style.scss';
|
||||
</style>
|
||||
0
front/app/components/LayoutFooter/style.scss
Normal file
0
front/app/components/LayoutFooter/style.scss
Normal file
29
front/app/components/LayoutHeader/index.vue
Normal file
29
front/app/components/LayoutHeader/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<header class="container mx-auto p-5 flex flex-row justify-end">
|
||||
<ul class="flex gap-5">
|
||||
<li>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
@click.prevent="logout()"
|
||||
>
|
||||
Выход
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const logout = () => {
|
||||
useCookie(AuthConfig.AccessTokenCookieName).value = null
|
||||
navigateTo(RoutePaths.Auth.Login)
|
||||
const toast = useToast()
|
||||
toast.add({ title: 'Выход', description: 'Вы вышли из системы', color: 'info' })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use './style.scss';
|
||||
</style>
|
||||
0
front/app/components/LayoutHeader/style.scss
Normal file
0
front/app/components/LayoutHeader/style.scss
Normal file
13
front/app/layouts/default.vue
Normal file
13
front/app/layouts/default.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen w-full flex flex-col">
|
||||
<LayoutHeader />
|
||||
<main class="flex-1 container mx-auto px-5 flex flex-col">
|
||||
<slot />
|
||||
</main>
|
||||
<LayoutFooter />
|
||||
</div>
|
||||
</template>
|
||||
11
front/app/layouts/noAuthenticated.vue
Normal file
11
front/app/layouts/noAuthenticated.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen w-full flex flex-col">
|
||||
<main class="flex-1 container mx-auto px-5 flex flex-col">
|
||||
<slot />
|
||||
</main>
|
||||
<LayoutFooter />
|
||||
</div>
|
||||
</template>
|
||||
11
front/app/middleware/auth.ts
Normal file
11
front/app/middleware/auth.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export default defineNuxtRouteMiddleware(async () => {
|
||||
console.log('Auth middleware')
|
||||
|
||||
const accessToken = useCookie(AuthConfig.AccessTokenCookieName)
|
||||
|
||||
const isAuthenticated = ref(accessToken.value != null)
|
||||
|
||||
if (!isAuthenticated.value) {
|
||||
return navigateTo(RoutePaths.Auth.Login)
|
||||
}
|
||||
})
|
||||
160
front/app/pages/auth/login.vue
Normal file
160
front/app/pages/auth/login.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<UForm
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
class="space-y-4 min-w-1/4"
|
||||
@submit="loginFormOnSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Пароль"
|
||||
name="password"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
class="w-full"
|
||||
type="password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit">
|
||||
Войти
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<UModal
|
||||
v-model:open="otpCodeModalOpen"
|
||||
:dismissible="false"
|
||||
:close="{
|
||||
color: 'primary',
|
||||
variant: 'outline',
|
||||
class: 'rounded-full'
|
||||
}"
|
||||
title="Введите код подтверждения"
|
||||
:description="`Код подтверждения выслан на почту ${state.email}`"
|
||||
>
|
||||
<template #body>
|
||||
<div class="w-full gap-5 flex flex-col justify-center items-center">
|
||||
<UPinInput
|
||||
id="pin-input"
|
||||
v-model="otpCode"
|
||||
otp
|
||||
mask
|
||||
:length="4"
|
||||
type="number"
|
||||
:autofocus="true"
|
||||
/>
|
||||
<span
|
||||
:v-if="otpCodeErrorText !== ''"
|
||||
>
|
||||
{{ otpCodeErrorText }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as v from 'valibot'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'no-authenticated',
|
||||
})
|
||||
|
||||
const schema = v.object({
|
||||
email: v.pipe(v.string(), v.email('Неверный формат email')),
|
||||
password: v.pipe(v.string()),
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const otpCode = ref([])
|
||||
const lastOtpCode = ref([])
|
||||
const otpCodeErrorText = ref('')
|
||||
const otpCodeModalOpen = ref(false)
|
||||
const isOtpLoading = ref(false)
|
||||
const isOtpVerified = ref(false)
|
||||
|
||||
// Watcher для отслеживания изменения пин-кода
|
||||
watch(otpCode, async (newValue) => {
|
||||
// Проверяем, что введены все 4 цифры
|
||||
if (newValue.join('').length === 4 && !isOtpVerified.value && newValue.join('') !== lastOtpCode.value.join('')) {
|
||||
lastOtpCode.value = otpCode.value
|
||||
await verifyOtpCode()
|
||||
}
|
||||
})
|
||||
|
||||
async function loginFormOnSubmit() {
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
try {
|
||||
await $api(
|
||||
'auth/login', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
})
|
||||
|
||||
otpCodeModalOpen.value = true
|
||||
}
|
||||
catch {
|
||||
toast.add({ title: 'Ошибка', description: 'Неверный email или пароль', color: 'warning' })
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyOtpCode() {
|
||||
if (isOtpLoading.value) return
|
||||
|
||||
isOtpLoading.value = true
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
try {
|
||||
const otpLoginResponse = await $api(
|
||||
'auth/otp', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...state,
|
||||
otp_code: otpCode.value.join(''),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
isOtpVerified.value = true
|
||||
|
||||
otpCodeErrorText.value = ''
|
||||
|
||||
toast.add({ title: 'Успешно', description: 'Вход выполнен', color: 'success' })
|
||||
|
||||
useCookie(AuthConfig.AccessTokenCookieName).value = otpLoginResponse.token
|
||||
|
||||
setTimeout(async () => {
|
||||
otpCodeModalOpen.value = false
|
||||
navigateTo(RoutePaths.Main)
|
||||
}, 500)
|
||||
}
|
||||
catch {
|
||||
otpCodeErrorText.value = 'Неверный код подтверждения'
|
||||
isOtpLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
11
front/app/pages/index.vue
Normal file
11
front/app/pages/index.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 flex justify-center items-center">
|
||||
<h2>Главная</h2>
|
||||
</div>
|
||||
</template>
|
||||
108
front/app/plugins/api.ts
Normal file
108
front/app/plugins/api.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { H3Error } from 'h3'
|
||||
|
||||
type IResponse = Response & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_data: any
|
||||
} | null
|
||||
|
||||
interface ApiErrorOptions {
|
||||
response: IResponse
|
||||
error: H3Error | Error
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик ошибок API
|
||||
*/
|
||||
function handleApiError({ response, error }: ApiErrorOptions) {
|
||||
if (!response) {
|
||||
console.error('Network error:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage = response._data?.message || error?.message || 'Unknown error'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
// console.error('Unauthorized access. Redirecting to login.')
|
||||
return navigateTo(RoutePaths.Auth.Login)
|
||||
|
||||
case 403:
|
||||
// console.error('Forbidden:', errorMessage)
|
||||
break
|
||||
|
||||
case 404:
|
||||
// console.error('Not found:', errorMessage)
|
||||
break
|
||||
|
||||
case 400:
|
||||
// console.error('Bad request', errorMessage)
|
||||
break
|
||||
|
||||
case 500:
|
||||
// console.error('Server error:', errorMessage)
|
||||
toast.add({ title: 'Ошибка', description: 'Произошла ошибка на сервере, попробуйте позже', color: 'error' })
|
||||
break
|
||||
|
||||
default:
|
||||
console.error(`Error ${response.status}:`, errorMessage)
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: response.status,
|
||||
statusMessage: errorMessage,
|
||||
fatal: false,
|
||||
})
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'api-plugin',
|
||||
setup(nuxtApp) {
|
||||
const config = nuxtApp.$config
|
||||
|
||||
let baseURL = '/'
|
||||
|
||||
baseURL = config.public.baseApiUrl
|
||||
|
||||
const api = $fetch.create({
|
||||
baseURL,
|
||||
|
||||
onRequest({ request, options }) {
|
||||
// Добавляем заголовок авторизации, если есть токен
|
||||
const tokenAuthenticated = useCookie(AuthConfig.AccessTokenCookieName)
|
||||
|
||||
if (tokenAuthenticated.value) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
Authorization: `Bearer ${tokenAuthenticated.value}`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any
|
||||
}
|
||||
|
||||
if (import.meta.dev) {
|
||||
console.log('[API Request]', request, options)
|
||||
}
|
||||
},
|
||||
|
||||
async onResponse({ response }) {
|
||||
if (import.meta.dev) {
|
||||
console.log('[API Response]', response.status, response._data)
|
||||
}
|
||||
},
|
||||
|
||||
async onResponseError({ response, error }): Promise<void> {
|
||||
handleApiError({
|
||||
response: response as IResponse,
|
||||
error: error as H3Error,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
provide: {
|
||||
api,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
3
front/app/types/auth.ts
Normal file
3
front/app/types/auth.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const AuthConfig = {
|
||||
AccessTokenCookieName: 'access_token_cookie',
|
||||
}
|
||||
6
front/app/types/routes.ts
Normal file
6
front/app/types/routes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const RoutePaths = {
|
||||
Main: '/',
|
||||
Auth: {
|
||||
Login: '/auth/login',
|
||||
},
|
||||
}
|
||||
5
front/compose.yml
Normal file
5
front/compose.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
front:
|
||||
build: .
|
||||
ports: [ 127.0.0.1:3000:3000 ]
|
||||
restart: unless-stopped
|
||||
42
front/eslint.config.mjs
Normal file
42
front/eslint.config.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import stylistic from '@stylistic/eslint-plugin'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import vueParser from 'vue-eslint-parser'
|
||||
// @ts-check
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt({
|
||||
files: ['**/*.{js,ts,jsx,tsx,vue,mjs}'],
|
||||
ignores: [
|
||||
'**/.nuxt/**',
|
||||
'**/.output/**',
|
||||
'**/assets/**',
|
||||
'**/node_modules/**',
|
||||
],
|
||||
languageOptions: {
|
||||
parser: vueParser,
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@stylistic': stylistic,
|
||||
vue,
|
||||
},
|
||||
rules: {
|
||||
...stylistic.configs.recommended.rules,
|
||||
...vue.configs['strongly-recommended'].rules,
|
||||
|
||||
'vue/max-attributes-per-line': ['error', {
|
||||
singleline: {
|
||||
max: 1,
|
||||
},
|
||||
multiline: {
|
||||
max: 1,
|
||||
},
|
||||
}],
|
||||
},
|
||||
})
|
||||
26
front/nuxt.config.ts
Normal file
26
front/nuxt.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
ssr: false,
|
||||
modules: [
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/ui',
|
||||
'@nuxt/image',
|
||||
],
|
||||
imports: {
|
||||
autoImport: true,
|
||||
dirs: [
|
||||
'./types',
|
||||
],
|
||||
},
|
||||
css: [
|
||||
'~/assets/css/nuxtui.css',
|
||||
'~/assets/scss/main.scss',
|
||||
],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
baseApiUrl: 'http://localhost:8000/api',
|
||||
},
|
||||
},
|
||||
})
|
||||
16911
front/package-lock.json
generated
Normal file
16911
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
front/package.json
Normal file
33
front/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"start": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/image": "1.11.0",
|
||||
"@nuxt/ui": "3.3.2",
|
||||
"nuxt": "^4.0.3",
|
||||
"typescript": "^5.6.3",
|
||||
"valibot": "^1.1.0",
|
||||
"vue": "^3.5.20",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint": "^1.9.0",
|
||||
"eslint": "^9.34.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"sass": "^1.91.0"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint:fix"
|
||||
]
|
||||
}
|
||||
BIN
front/public/favicon.ico
Normal file
BIN
front/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
front/public/robots.txt
Normal file
2
front/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
18
front/tsconfig.json
Normal file
18
front/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user