Skip to content

Commit

Permalink
Merge pull request #429 from BinaryStudioAcademy/task/OV-377-create-p…
Browse files Browse the repository at this point in the history
…review-page

OV-377: Create preview page and flow
  • Loading branch information
nikita-remeslov authored Sep 27, 2024
2 parents 90610ea + 34e1084 commit 264640c
Show file tree
Hide file tree
Showing 38 changed files with 488 additions and 13 deletions.
1 change: 1 addition & 0 deletions backend/src/bundles/public-video/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VideosApiPath, VideoValidationMessage } from 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const PublicVideosApiPath = {
ROOT: '/',
} as const;

export { PublicVideosApiPath };
42 changes: 42 additions & 0 deletions backend/src/bundles/public-video/public-video.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type PublicVideoService } from '~/bundles/public-video/public-video.service.js';
import {
type ApiHandlerOptions,
type ApiHandlerResponse,
BaseController,
} from '~/common/controller/controller.js';
import { ApiPath } from '~/common/enums/enums.js';
import { HTTPCode, HTTPMethod } from '~/common/http/http.js';
import { type Logger } from '~/common/logger/logger.js';

import { PublicVideosApiPath } from './enums/public-videos-api-path.enum.js';

class PublicVideoController extends BaseController {
private publicVideoService: PublicVideoService;

public constructor(logger: Logger, publicVideoService: PublicVideoService) {
super(logger, ApiPath.PUBLIC_VIDEO);

this.publicVideoService = publicVideoService;

this.addRoute({
path: PublicVideosApiPath.ROOT,
method: HTTPMethod.GET,
handler: (options) => this.findUrlByToken(options),
});
}

private async findUrlByToken(
options: ApiHandlerOptions,
): Promise<ApiHandlerResponse> {
const headers = options.headers as Record<string, { value: string }>;
const videoTokenHeader = headers['video_token']?.toString() ?? '';

return {
status: HTTPCode.OK,
payload:
await this.publicVideoService.findUrlByToken(videoTokenHeader),
};
}
}

export { PublicVideoController };
41 changes: 41 additions & 0 deletions backend/src/bundles/public-video/public-video.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type VideoRepository } from '~/bundles/videos/video.repository.js';
import { HTTPCode, HttpError } from '~/common/http/http.js';
import { tokenService } from '~/common/services/services.js';

import { VideoValidationMessage } from './enums/enums.js';

class PublicVideoService {
private videoRepository: VideoRepository;
public constructor(videoRepository: VideoRepository) {
this.videoRepository = videoRepository;
}

public async findUrlByToken(token: string): Promise<string> {
const id = await tokenService.getIdFromToken(token);

if (!id) {
this.throwVideoNotFoundError();
}

const video = await this.videoRepository.findById(id);

if (!video) {
this.throwVideoNotFoundError();
}

const { url } = video.toObject();
if (!url) {
this.throwVideoNotFoundError();
}
return url;
}

private throwVideoNotFoundError(): never {
throw new HttpError({
message: VideoValidationMessage.VIDEO_DOESNT_EXIST,
status: HTTPCode.NOT_FOUND,
});
}
}

export { PublicVideoService };
13 changes: 13 additions & 0 deletions backend/src/bundles/public-video/public-videos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { VideoModel } from '~/bundles/videos/video.model.js';
import { VideoRepository } from '~/bundles/videos/video.repository.js';
import { logger } from '~/common/logger/logger.js';
import { imageService } from '~/common/services/services.js';

import { PublicVideoController } from './public-video.controller.js';
import { PublicVideoService } from './public-video.service.js';

const videoRepository = new VideoRepository(VideoModel, imageService);
const videoService = new PublicVideoService(videoRepository);
const publicVideoController = new PublicVideoController(logger, videoService);

export { publicVideoController };
48 changes: 48 additions & 0 deletions backend/src/bundles/videos/video.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ class VideoController extends BaseController {
}>,
),
});
this.addRoute({
path: `${VideosApiPath.ID}${VideosApiPath.SHARE}`,
method: HTTPMethod.GET,
handler: (options) => {
return this.createVideoIdJWT(
options as ApiHandlerOptions<{
params: VideoGetOneRequestDto;
}>,
);
},
});
}

/**
Expand Down Expand Up @@ -186,6 +197,43 @@ class VideoController extends BaseController {
};
}

/**
* @swagger
* /videos/{id}/share:
* get:
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* description: The video id
* description: Create a JWT for the video id
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Successful operation
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
*/
private async createVideoIdJWT(
options: ApiHandlerOptions<{
params: VideoGetOneRequestDto;
}>,
): Promise<ApiHandlerResponse> {
return {
status: HTTPCode.OK,
payload: await this.videoService.getVideoIdToken(options.params.id),
};
}

/**
* @swagger
* /videos/:
Expand Down
7 changes: 6 additions & 1 deletion backend/src/bundles/videos/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type VideoRepository } from '~/bundles/videos/video.repository.js';
import { HTTPCode, HttpError } from '~/common/http/http.js';
import { type ImageService } from '~/common/services/image/image.service.js';
import { type RemotionService } from '~/common/services/remotion/remotion.service.js';
import { tokenService } from '~/common/services/services.js';
import { type Service } from '~/common/types/types.js';

import { VideoValidationMessage } from './enums/enums.js';
Expand All @@ -18,7 +19,6 @@ class VideoService implements Service {
private videoRepository: VideoRepository;
private remotionService: RemotionService;
private imageService: ImageService;

public constructor(
videoRepository: VideoRepository,
remotionService: RemotionService,
Expand Down Expand Up @@ -116,6 +116,11 @@ class VideoService implements Service {

return isVideoDeleted;
}

public async getVideoIdToken(id: string): Promise<string> {
const token = await tokenService.createToken(id, false);
return token.replaceAll('.', '~');
}
}

export { VideoService };
4 changes: 4 additions & 0 deletions backend/src/common/constants/white-routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const WHITE_ROUTES = [
path: `/api/v1${ApiPath.AUTH}${AuthApiPath.SIGN_UP}`,
method: HTTPMethod.POST,
},
{
path: `/api/v1${ApiPath.PUBLIC_VIDEO}/`,
method: HTTPMethod.GET,
},
{
path: /\/v1\/documentation\/.*/,
method: HTTPMethod.GET,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/common/controller/base-controller.package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ class BaseController implements Controller {
private mapRequest(
request: Parameters<ServerAppRouteParameters['handler']>[0],
): ApiHandlerOptions {
const { body, query, params, session, user } = request;
const { body, query, params, session, user, headers } = request;

return {
body,
query,
params,
session,
user,
headers,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type DefaultApiHandlerOptions = {
params?: unknown;
session?: unknown;
user?: unknown;
headers?: unknown;
};

type ApiHandlerOptions<
Expand All @@ -14,6 +15,7 @@ type ApiHandlerOptions<
params: T['params'];
session: T['session'];
user: T['user'];
headers: T['headers'];
};

export { type ApiHandlerOptions };
2 changes: 1 addition & 1 deletion backend/src/common/plugins/auth/auth-jwt.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const authenticateJWT = fp<Options>((fastify, { routesWhiteList }, done) => {

const [, token] = authHeader.split(' ');

const userId = await tokenService.getUserIdFromToken(token as string);
const userId = await tokenService.getIdFromToken(token as string);

if (!userId) {
throw new HttpError({
Expand Down
2 changes: 2 additions & 0 deletions backend/src/common/server-application/server-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { avatarVideoController } from '~/bundles/avatar-videos/avatar-videos.js'
import { avatarController } from '~/bundles/avatars/avatars.js';
import { chatController } from '~/bundles/chat/chat.js';
import { notificationController } from '~/bundles/notifications/notifications.js';
import { publicVideoController } from '~/bundles/public-video/public-videos.js';
import { speechController } from '~/bundles/speech/speech.js';
import { templateController } from '~/bundles/templates/templates.js';
import { userController } from '~/bundles/users/users.js';
Expand All @@ -25,6 +26,7 @@ const apiV1 = new BaseServerAppApi(
...chatController.routes,
...speechController.routes,
...avatarVideoController.routes,
...publicVideoController.routes,
...templateController.routes,
);

Expand Down
20 changes: 13 additions & 7 deletions backend/src/common/services/token/token.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ class TokenService {
this.expirationTime = expirationTime;
}

public async createToken(userId: string): Promise<string> {
return await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime(this.expirationTime)
.sign(this.secretKey);
public async createToken(
id: string,
expires: boolean = true,
): Promise<string> {
const jwt = new SignJWT({ id }).setProtectedHeader({ alg: 'HS256' });

if (expires) {
jwt.setExpirationTime(this.expirationTime);
}

return await jwt.sign(this.secretKey);
}

public async verifyToken(token: string): Promise<TokenPayload | null> {
Expand All @@ -26,9 +32,9 @@ class TokenService {
}
}

public async getUserIdFromToken(token: string): Promise<string | null> {
public async getIdFromToken(token: string): Promise<string | null> {
const payload = await this.verifyToken(token);
return (payload?.['userId'] as string) || null;
return (payload?.['id'] as string) ?? null;
}
}

Expand Down
6 changes: 6 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ VITE_APP_API_ORIGIN_URL=/api/v1
#
VITE_APP_PROXY_SERVER_URL=http://localhost:3001
VITE_APP_SOCKET_ORIGIN_URL=/socket.io

#
# DEPLOYMENT
#
VITE_APP_PUBLIC_URL=http://localhost:3000

9 changes: 8 additions & 1 deletion frontend/src/bundles/common/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { config } from '~/framework/config/config.js';
import { http } from '~/framework/http/http.js';
import { storage } from '~/framework/storage/storage.js';

import { PublicVideosApi } from './public-video-api/public-videos-api.js';
import { VideosApi } from './video-api/videos-api.js';

const videosApi = new VideosApi({
Expand All @@ -10,4 +11,10 @@ const videosApi = new VideosApi({
http,
});

export { videosApi };
const publicVideosApi = new PublicVideosApi({
baseUrl: config.ENV.API.ORIGIN_URL,
storage,
http,
});

export { publicVideosApi, videosApi };
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VideosApiPath } from 'shared';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ApiPath, ContentType } from '~/bundles/common/enums/enums.js';
import { type Http, HTTPMethod } from '~/framework/http/http.js';
import { BaseHttpApi } from '~/framework/http-api/http-api.js';
import { type Storage } from '~/framework/storage/storage.js';

import { VideosApiPath } from './enums/enums.js';

type Constructor = {
baseUrl: string;
http: Http;
storage: Storage;
};

class PublicVideosApi extends BaseHttpApi {
public constructor({ baseUrl, http, storage }: Constructor) {
super({ path: ApiPath.PUBLIC_VIDEO, baseUrl, http, storage });
}

public async getVideoUrlFromJWT(jwt: string): Promise<string> {
const headers = new Headers();
headers.append('video_token', jwt.replaceAll('~', '.'));

const options = {
method: HTTPMethod.GET,
contentType: ContentType.JSON,
hasAuth: true,
customHeaders: headers,
};

const response = await this.load(
this.getFullEndpoint(`${VideosApiPath.ROOT}`, {}),
options,
);

if (!response.ok) {
throw new Error(
`Failed to get video ID JWT: ${response.statusText}`,
);
}
return await response.text();
}
}

export { PublicVideosApi };
Loading

0 comments on commit 264640c

Please sign in to comment.