Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tracker-task/
### **Стек**

- [Next.js](https://nextjs.org/docs) - для создания серверного рендеринга и статической генерации страниц
- [Next.js с Нуля - полный курс для начинающих (2025)](https://www.youtube.com/watch?v=KZb53sf-PEg)
- [TypeScript](https://www.typescriptlang.org/docs/) - для статической типизации
- [TypeScript ФУНДАМЕНТАЛЬНЫЙ КУРС от А до Я. Вся теория + практика](https://www.youtube.com/watch?v=LWtHl__oEWc&t=3720s)
- [TanStack Query](https://tanstack.com/query/latest/docs/framework/react/overview) - для управления серверным состоянием и кэшированием данных
Expand Down Expand Up @@ -95,8 +96,6 @@ tracker-task/
- Обязателен CI пайплайн
- [CI CD наглядные примеры](https://www.youtube.com/watch?v=ANj7qUgzNq4&pp=ygUNdWxiaSB0diBjaSBjZA%3D%3D)

===

## [**Backend**](https://itbooster.ru/roadmap/4/)

### **Стек**
Expand All @@ -120,6 +119,8 @@ tracker-task/
- [BullMQ](https://docs.bullmq.io/readme-1) - для управления очередями задач
- [BullMQ](https://itbooster.ru/roadmap/4/?nodeId=08380761-24eb-42b8-9f9b-522bc5f6ee8b)
- [OpenAI Node.js](https://github.com/openai/openai-node#readme) - для интеграции с OpenAI API
- [Passport.js](https://www.passportjs.org/) - для аутентификации
- [Интеграция Passport.js с NestJS](https://docs.nestjs.com/recipes/passport) - интеграция Passport.js с NestJS

### **Инструменты**

Expand Down Expand Up @@ -149,7 +150,7 @@ tracker-task/

## **Общее**

- **[CI_CD]**(https://itbooster.ru/roadmap/2/?nodeId=4318d274-56ee-4161-ab9f-4ed89f574870) - для автоматизации сборки, тестирования и деплоя приложения
- **[CI_CD](https://itbooster.ru/roadmap/2/?nodeId=4318d274-56ee-4161-ab9f-4ed89f574870)** - для автоматизации сборки, тестирования и деплоя приложения
- Обязательно на каждый пул реквест прогоняем линтеры, тесты, билд проекта, typecheck через ts.
- Релизы через отведение ветки + нажатие кнопки release в github.
- Пуш в dev/trunk/master - запрещен напрямую, только через ПР с код ревью
Expand Down
4 changes: 4 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Конфигурация cookie
COOKIE_DOMAIN=домен_куки
COOKIES_TTL=время_жизни_куки

# Переменные среды для приложения API
JWT_SECRET=секретный_ключ_jwt
JWT_ACCESS_TOKEN_TTL=время_жизни_токена_доступа
Expand Down
5 changes: 5 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.11",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.11",
"@nestjs/swagger": "^11.2.6",
"@prisma/adapter-pg": "^7.4.0",
Expand All @@ -35,6 +36,8 @@
"dotenv": "^17.3.1",
"escape-html": "^1.0.3",
"nestjs-zod": "^5.1.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.18.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
Expand All @@ -50,6 +53,8 @@
"@types/cookie-parser": "^1.4.10",
"@types/express": "^4.17.25",
"@types/node": "^24.10.13",
"@types/passport": "^1.0.17",
"@types/passport-jwt": "^4.0.1",
"@types/pg": "^8.16.0",
"@types/supertest": "^6.0.3",
"@vitest/coverage-v8": "^4.0.18",
Expand Down
65 changes: 64 additions & 1 deletion apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
} from '@nestjs/common'
import type { Request, Response } from 'express'

import { AuthService } from './auth.service'
Expand All @@ -11,9 +20,13 @@ import {
ApiBadRequestResponse,
ApiNotFoundResponse,
ApiUnauthorizedResponse,
ApiBearerAuth,
} from '@nestjs/swagger'
import { LoginRequestDto } from './dto/login.dto'
import { AuthResponse } from './dto/auth.dto'
import { Authorization } from './decorators/authorization.decorator'
import { User } from 'generated/prisma/client'
import { Authorized } from './decorators/authorized.decorator'

@ApiTags('Auth')
@Controller('auth')
Expand Down Expand Up @@ -123,4 +136,54 @@ export class AuthController {
async logout(@Res({ passthrough: true }) res: Response) {
return this.authService.logout(res)
}

@ApiOperation({
summary: 'Получение информации о текущем пользователе',
description: 'Возвращает информацию о текущем аутентифицированном пользователе',
})
@ApiOkResponse({
description: 'Информация о пользователе успешно получена',
schema: {
example: {
id: '123e4567-e89b-12d3-a456-426614174000',
email: 'user@example.com',
name: 'John Doe',
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
},
},
})
@ApiUnauthorizedResponse({
description: 'Пользователь не авторизован',
schema: {
example: {
statusCode: 401,
error: 'Unauthorized',
message: 'Пользователь не авторизован',
},
},
})
@ApiNotFoundResponse({
description: 'Пользователь не найден',
schema: {
example: {
statusCode: 404,
error: 'Not Found',
message: 'Пользователь не найден',
},
},
})
@Authorization()
@ApiBearerAuth()
@Get('me')
@HttpCode(HttpStatus.OK)
async me(@Authorized() userInfo: User) {
return {
id: userInfo.id,
email: userInfo.email,
name: userInfo.name,
createdAt: userInfo.createdAt,
updatedAt: userInfo.updatedAt,
}
}
}
21 changes: 12 additions & 9 deletions apps/api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getJwtConfig } from './config/jwt.config';
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { JwtModule } from '@nestjs/jwt'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { getJwtConfig } from './config/jwt.config'
import { JwtStrategy } from 'src/strategies/jwt.strategy'
import { PassportModule } from '@nestjs/passport'

@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: getJwtConfig,
inject: [ConfigService],
})
}),
],
controllers: [AuthController],
providers: [AuthService],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
16 changes: 15 additions & 1 deletion apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
},
})

return this.auth(res, user.id)

Check warning on line 61 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}

async login(res: Response, dto: LoginRequestDto) {
Expand All @@ -78,13 +78,13 @@
throw new NotFoundException('Пользователь не найден')
}

const isPasswordValid = await verify(user.password, password)

Check warning on line 81 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`

if (!isPasswordValid) {
throw new NotFoundException('Пользователь не найден')
}

return this.auth(res, user.id)

Check warning on line 87 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}

async refresh(req: Request, res: Response) {
Expand All @@ -110,7 +110,7 @@
throw new NotFoundException('Пользователь не найден')
}

return this.auth(res, user.id)

Check warning on line 113 in apps/api/src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / ci

Unsafe argument of type error typed assigned to a parameter of type `string`
}
}

Expand All @@ -120,6 +120,20 @@
return { message: 'Пользователь успешно вышел', success: true }
}

async validateUser(userId: string) {
const user = await this.prismaService.user.findUnique({
where: {
id: userId,
},
})

if (!user) {
throw new NotFoundException('Пользователь не найден')
}

return user
}

private auth(res: Response, userId: string) {
const { accessToken, refreshToken } = this.generateTokens(userId)

Expand Down Expand Up @@ -150,7 +164,7 @@
res.cookie('refreshToken', value, {
httpOnly: true,
secure: !isDev(this.configService),
sameSite: isDev(this.configService) ? 'none' : 'lax',
sameSite: isDev(this.configService) ? 'lax' : 'strict',
domain: this.COOKIE_DOMAIN,
expires,
})
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/auth/decorators/authorization.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { applyDecorators, UseGuards } from '@nestjs/common'
import { JwtGuard } from 'src/guards/auth.guard'

export function Authorization() {
return applyDecorators(UseGuards(JwtGuard))
}
17 changes: 17 additions & 0 deletions apps/api/src/auth/decorators/authorized.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common'
import type { Request } from 'express'
import { User } from 'generated/prisma/client'

export const Authorized = createParamDecorator(
(data: keyof User, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest() as Request

const user = request.user as User

if (!user) {
return null
}

return data ? user[data] : user
},
)
3 changes: 3 additions & 0 deletions apps/api/src/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AuthGuard } from '@nestjs/passport'

export class JwtGuard extends AuthGuard('jwt') {}
5 changes: 4 additions & 1 deletion apps/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { setupSwagger } from './utils/swagger.util'

async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.enableCors()
app.enableCors({
origin: true,
credentials: true,
})

app.use(cookieParser())

Expand Down
25 changes: 25 additions & 0 deletions apps/api/src/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { AuthService } from 'src/auth/auth.service'
import { JwtPayload } from 'src/auth/interfaces/jwt.interface'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>('JWT_SECRET'),
algorithms: ['HS256'],
})
}

async validate(payload: JwtPayload) {
return await this.authService.validateUser(payload.id)
}
}
3 changes: 3 additions & 0 deletions apps/api/src/utils/swagger.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ export function setupSwagger(app: INestApplication) {
SwaggerModule.setup('api/docs', app, cleanupOpenApiDoc(document), {
jsonDocumentUrl: '/swagger.json',
yamlDocumentUrl: '/swagger.yaml',
swaggerOptions: {
withCredentials: true,
},
})
}
Loading
Loading