diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/README.md b/README.md index ad2e8ab..d3ce33c 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# VideoStramApp +# VideoStreamApp + +Full-stack [app tutorial](https://blog.logrocket.com/full-stack-app-tutorial-nestjs-react/) with NestJS and React + +[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs) + +## Features + +- NestJS + Mongoose +- MongoDB +- React|Vue as frontend + +## Run Locally + +Clone the project + +```bash +git clone https://github.com/zolotarev/VideoStreamApp.git +``` + +Go to the project directory + +```bash +cd VideoStreamApp +``` + +Install dependencies + +```bash +(cd backend && yarn install) & (cd frontend && yarn install) +``` + +Start for development + +```bash +(cd backend && yarn && yarn dev) & (cd frontend && yarn && yarn dev) +``` + +Start for production + +```bash +(cd frontend && yarn build) +(cd backend && yarn build && yarn start) +``` + +## License + +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..9e21b13 --- /dev/null +++ b/backend/.env @@ -0,0 +1,2 @@ +PORT=3002 +MONGO=mongodb://localhost:27017/Stream diff --git a/backend/.gitignore b/backend/.gitignore index 22f55ad..18a849c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -32,4 +32,5 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json +/public/ diff --git a/backend/package.json b/backend/package.json index 941ce18..ee16449 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,44 +21,45 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/common": "^8.0.0", - "@nestjs/core": "^8.0.0", - "@nestjs/platform-express": "^8.0.0", + "@nestjs/common": "^8.2.6", + "@nestjs/core": "^8.2.6", + "@nestjs/platform-express": "^8.2.6", "@nestjs/serve-static": "^2.2.2", "bcrypt": "^5.0.1", + "dotenv": "^16.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.5.3" }, "devDependencies": { - "@nestjs/cli": "^8.0.0", + "@nestjs/cli": "^8.2.0", "@nestjs/jwt": "^8.0.0", "@nestjs/mongoose": "^9.0.2", - "@nestjs/schematics": "^8.0.0", - "@nestjs/testing": "^8.0.0", + "@nestjs/schematics": "^8.0.5", + "@nestjs/testing": "^8.2.6", "@types/bcrypt": "^5.0.0", "@types/express": "^4.17.13", - "@types/jest": "27.0.2", + "@types/jest": "27.4.0", "@types/multer": "^1.4.7", - "@types/node": "^16.0.0", + "@types/node": "^17.0.16", "@types/supertest": "^2.0.11", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^8.0.1", + "@typescript-eslint/eslint-plugin": "^5.11.0", + "@typescript-eslint/parser": "^5.11.0", + "eslint": "^8.8.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", - "jest": "^27.2.5", - "mongoose": "^6.1.7", + "jest": "^27.5.1", + "mongoose": "^6.2.1", "passport-jwt": "^4.0.0", - "prettier": "^2.3.2", - "source-map-support": "^0.5.20", - "supertest": "^6.1.3", - "ts-jest": "^27.0.3", - "ts-loader": "^9.2.3", - "ts-node": "^10.0.0", - "tsconfig-paths": "^3.10.1", - "typescript": "^4.3.5" + "prettier": "^2.5.1", + "source-map-support": "^0.5.21", + "supertest": "^6.2.2", + "ts-jest": "^27.1.3", + "ts-loader": "^9.2.6", + "ts-node": "^10.5.0", + "tsconfig-paths": "^3.12.0", + "typescript": "^4.5.5" }, "jest": { "moduleFileExtensions": [ diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/backend/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/backend/src/app.middleware.ts b/backend/src/app.middleware.ts index c879950..77b36b0 100644 --- a/backend/src/app.middleware.ts +++ b/backend/src/app.middleware.ts @@ -1,37 +1,41 @@ - import { JwtService } from '@nestjs/jwt'; -import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common'; +import { + Injectable, + NestMiddleware, + HttpException, + HttpStatus, +} from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { UserService } from './service/user.service'; interface UserRequest extends Request { - user: any + user: any; } @Injectable() export class isAuthenticated implements NestMiddleware { - constructor(private readonly jwt: JwtService, private readonly userService: UserService) { } - async use(req: UserRequest, res: Response, next: NextFunction) { - try{ - - if ( - req.headers.authorization && - req.headers.authorization.startsWith('Bearer') - ) { - const token = req.headers.authorization.split(' ')[1]; - const decoded = await this.jwt.verify(token); - const user = await this.userService.getOne(decoded.email) - if (user) { - req.user = user - next() - } else { - throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED) - - } - } else { - throw new HttpException('No token found', HttpStatus.NOT_FOUND) - - } - }catch { - throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED) - } + constructor( + private readonly jwt: JwtService, + private readonly userService: UserService, + ) {} + async use(req: UserRequest, res: Response, next: NextFunction) { + try { + if ( + req.headers.authorization && + req.headers.authorization.startsWith('Bearer') + ) { + const token = req.headers.authorization.split(' ')[1]; + const decoded = await this.jwt.verify(token); + const user = await this.userService.getOne(decoded.email); + if (user) { + req.user = user; + next(); + } else { + throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); + } + } else { + throw new HttpException('No token found', HttpStatus.NOT_FOUND); + } + } catch { + throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); } -} \ No newline at end of file + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 65abd68..cf54283 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,6 +1,4 @@ import { Module, RequestMethod, MiddlewareConsumer } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { MongooseModule } from '@nestjs/mongoose'; import { MulterModule } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; @@ -16,42 +14,42 @@ import { UserController } from './controller/user.controller'; import { Video, VideoSchema } from './model/video.schema'; import { User, UserSchema } from './model/user.schema'; import { isAuthenticated } from './app.middleware'; +import dotenv from 'dotenv'; +dotenv.config(); @Module({ imports: [ - MongooseModule.forRoot('mongodb://localhost:27017/Stream'), + MongooseModule.forRoot(process.env.MONGO), MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), MongooseModule.forFeature([{ name: Video.name, schema: VideoSchema }]), - MulterModule.register({ - storage: diskStorage({ - destination: './public', - filename: (req, file, cb) => { - const ext = file.mimetype.split('/')[1]; - cb(null, `${uuidv4()}-${Date.now()}.${ext}`); - }, - }) - }), - JwtModule.register({ + MulterModule.register({ + storage: diskStorage({ + destination: './public', + filename: (req, file, cb) => { + const ext = file.mimetype.split('/')[1]; + cb(null, `${uuidv4()}-${Date.now()}.${ext}`); + }, + }), + }), + JwtModule.register({ secret, signOptions: { expiresIn: '2h' }, }), ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', 'public'), + exclude: ['/api/v1*'], }), ], - -controllers: [AppController, VideoController, UserController], -providers: [AppService, VideoService, UserService], -}) + controllers: [VideoController, UserController], + providers: [VideoService, UserService], +}) export class AppModule { configure(consumer: MiddlewareConsumer) { consumer .apply(isAuthenticated) - .exclude( - { path: 'api/v1/video/:id', method: RequestMethod.GET } - ) + .exclude({ path: 'api/v1/video/:id', method: RequestMethod.GET }) .forRoutes(VideoController); } } diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/backend/src/controller/user.controller.ts b/backend/src/controller/user.controller.ts index 8dcd8c4..825b7b9 100644 --- a/backend/src/controller/user.controller.ts +++ b/backend/src/controller/user.controller.ts @@ -1,27 +1,27 @@ -import { Body, Controller, Delete, Get, HttpStatus, Param, Post, UploadedFiles, Put, Req, Res } from "@nestjs/common"; -import { User } from "../model/user.schema"; -import { UserService } from "../service/user.service"; +import { Body, Controller, HttpStatus, Post, Res } from '@nestjs/common'; +import { User } from '../model/user.schema'; +import { UserService } from '../service/user.service'; import { JwtService } from '@nestjs/jwt'; - -@Controller('/api/v1/user') +@Controller('/user') export class UserController { - constructor(private readonly userServerice: UserService, - private jwtService: JwtService - ) { } + constructor( + private readonly userService: UserService, + private jwtService: JwtService, + ) {} - @Post('/signup') - async Signup(@Res() response, @Body() user: User) { - console.log(user) - const newUSer = await this.userServerice.signup(user); - return response.status(HttpStatus.CREATED).json({ - newUSer - }) - } + @Post('/signup') + async SignUp(@Res() response, @Body() user: User) { + console.log(user); + const newUSer = await this.userService.signUp(user); + return response.status(HttpStatus.CREATED).json({ + newUSer, + }); + } - @Post('/signin') - async SignIn(@Res() response, @Body() user: User) { - const token = await this.userServerice.signin(user, this.jwtService); - return response.status(HttpStatus.OK).json(token) - } -} \ No newline at end of file + @Post('/signin') + async SignIn(@Res() response, @Body() user: User) { + const token = await this.userService.signIn(user, this.jwtService); + return response.status(HttpStatus.OK).json(token); + } +} diff --git a/backend/src/controller/video.controller.ts b/backend/src/controller/video.controller.ts index 73636f3..1903ad1 100644 --- a/backend/src/controller/video.controller.ts +++ b/backend/src/controller/video.controller.ts @@ -1,45 +1,72 @@ -import { Body, Controller, Delete, Get, HttpStatus, Param, Post, UseInterceptors, UploadedFiles, Put, Req, Res, Query } from "@nestjs/common"; -import { Video } from "../model/video.schema" -import { VideoService } from "../service/video.service"; -import { FileFieldsInterceptor, FilesInterceptor } from "@nestjs/platform-express"; +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Param, + Post, + UseInterceptors, + UploadedFiles, + Put, + Req, + Res, + Query, +} from '@nestjs/common'; +import { Video } from '../model/video.schema'; +import { VideoService } from '../service/video.service'; +import { FileFieldsInterceptor } from '@nestjs/platform-express'; -@Controller('/api/v1/video') +@Controller('/video') export class VideoController { - constructor(private readonly videoService: VideoService) { } + constructor(private readonly videoService: VideoService) {} - @Post() - @UseInterceptors(FileFieldsInterceptor([ - { name: 'video', maxCount: 1 }, - { name: 'cover', maxCount: 1 }, - ])) - async createBook(@Res() response, @Req() request, @Body() video: Video, @UploadedFiles() files: { video?: Express.Multer.File[], cover?: Express.Multer.File[] }) { - const requestBody = { createdBy: request.user, title: video.title, video: files.video[0].filename, coverImage: files.cover[0].filename } - const newVideo = await this.videoService.createVideo(requestBody); - return response.status(HttpStatus.CREATED).json({ - newVideo - }) - } + @Post() + @UseInterceptors( + FileFieldsInterceptor([ + { name: 'video', maxCount: 1 }, + { name: 'cover', maxCount: 1 }, + ]), + ) + async createBook( + @Res() response, + @Req() request, + @Body() video: Video, + @UploadedFiles() + files: { video?: Express.Multer.File[]; cover?: Express.Multer.File[] }, + ) { + const requestBody = { + createdBy: request.user, + title: video.title, + video: files.video[0].filename, + coverImage: files.cover[0].filename, + }; + const newVideo = await this.videoService.createVideo(requestBody); + return response.status(HttpStatus.CREATED).json({ + newVideo, + }); + } - @Get() - async read(@Query() id): Promise { - return await this.videoService.readVideo(id); - } + @Get() + async read(@Query('id') id: string): Promise