Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OV-377: Create preview page and flow #429

Merged
merged 23 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b868514
OV-377: + add preview page with video player
stefano-lacorazza Sep 23, 2024
49af338
OV-377: + add copy url button to homepage
stefano-lacorazza Sep 23, 2024
06e0c10
OV-377: + create JWT with video id and return it to frontend
stefano-lacorazza Sep 24, 2024
945fac9
OV-377: + copy url to clipboard
stefano-lacorazza Sep 24, 2024
c19a3fa
OV-377: + create dynamic preview url
stefano-lacorazza Sep 24, 2024
01bf93f
OV-377: + create public-video controller and service
stefano-lacorazza Sep 25, 2024
e7cc793
Merge branch 'next' into task/OV-377-create-preview-page
stefano-lacorazza Sep 25, 2024
5e9044d
OV-377: + white route public-video
stefano-lacorazza Sep 25, 2024
ba2031e
OV-377: + style preview page
stefano-lacorazza Sep 25, 2024
423cabc
OV-377: * prettier
stefano-lacorazza Sep 25, 2024
5c7e932
OV-377: * Refactor token service to use a unified method for creating…
stefano-lacorazza Sep 26, 2024
480e38f
OV-377: + added dynamically created sharing URL
stefano-lacorazza Sep 26, 2024
556ed1d
OV-377: - use config to create video url
stefano-lacorazza Sep 26, 2024
907bdaf
Merge branch 'next' into task/OV-377-create-preview-page
stefano-lacorazza Sep 26, 2024
c2ebd64
Merge branch 'next' into task/OV-377-create-preview-page
stefano-lacorazza Sep 26, 2024
32b55b7
OV-377: + create env variable for deployment URL andfix magic numbers
stefano-lacorazza Sep 26, 2024
7d1c536
OV-377: * prettier
stefano-lacorazza Sep 26, 2024
3ae5db1
OV-377: * changed baseurl to access env PUBLIC_URL
stefano-lacorazza Sep 26, 2024
d932a5a
OV-377: * changed baseurl to access env PUBLIC_URL
stefano-lacorazza Sep 26, 2024
89fd45d
OV-377: * prettier
stefano-lacorazza Sep 26, 2024
b012825
Merge branch 'next' into task/OV-377-create-preview-page
stefano-lacorazza Sep 27, 2024
b8e9f78
OV-377: - prettier
stefano-lacorazza Sep 27, 2024
34e1084
OV-377: * change .env variable
stefano-lacorazza Sep 27, 2024
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
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 };
41 changes: 41 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,41 @@
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 };
43 changes: 43 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,43 @@
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.getVideoIdFromToken(token);

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

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

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

const url = video.toObject().url;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const url = video.toObject().url;
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 };
12 changes: 12 additions & 0 deletions backend/src/bundles/public-video/public-videos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { VideoModel } from '~/bundles/videos/video.model.js';
import { VideoRepository } from '~/bundles/videos/video.repository.js';
import { logger } from '~/common/logger/logger.js';

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

const videoRepository = new VideoRepository(VideoModel);
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}/share`,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this path to the enum

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
6 changes: 5 additions & 1 deletion backend/src/bundles/videos/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { VideoEntity } from '~/bundles/videos/video.entity.js';
import { type VideoRepository } from '~/bundles/videos/video.repository.js';
import { HTTPCode, HttpError } from '~/common/http/http.js';
import { type FileService } from '~/common/services/file/file.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 @@ -15,7 +16,6 @@ import {
class VideoService implements Service {
private videoRepository: VideoRepository;
private fileService: FileService;

public constructor(
videoRepository: VideoRepository,
fileService: FileService,
Expand Down Expand Up @@ -100,6 +100,10 @@ class VideoService implements Service {

return isVideoDeleted;
}

public async getVideoIdToken(id: string): Promise<string> {
return await tokenService.createVideoIdToken(id);
}
}

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 };
3 changes: 3 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 { userController } from '~/bundles/users/users.js';
import { videoController } from '~/bundles/videos/videos.js';
Expand All @@ -24,6 +25,8 @@ const apiV1 = new BaseServerAppApi(
...chatController.routes,
...speechController.routes,
...avatarVideoController.routes,
...publicVideoController.routes,

);

const serverApp = new BaseServerApp({
Expand Down
12 changes: 12 additions & 0 deletions backend/src/common/services/token/token.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ class TokenService {
.sign(this.secretKey);
}

public async createVideoIdToken(videoId: string): Promise<string> {
const jwt = await new SignJWT({ videoId })
.setProtectedHeader({ alg: 'HS256' })
.sign(this.secretKey);
return jwt.replaceAll('.', '~');
}

public async verifyToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.secretKey);
Expand All @@ -30,6 +37,11 @@ class TokenService {
const payload = await this.verifyToken(token);
return (payload?.['userId'] as string) || null;
}

public async getVideoIdFromToken(token: string): Promise<string | null> {
const payload = await this.verifyToken(token);
return (payload?.['videoId'] as string) || null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return (payload?.['videoId'] as string) || null;
return (payload?.['videoId'] as string) ?? null;

}
}

export { TokenService };
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,43 @@
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 };
33 changes: 33 additions & 0 deletions frontend/src/bundles/common/api/video-api/videos-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,22 @@ class VideosApi extends BaseHttpApi {
return await response.json<VideoGetAllItemResponseDto>();
}

public async getVideo(
payload: UpdateVideoRequestDto,
id: string,
): Promise<VideoGetAllItemResponseDto> {
const response = await this.load(
this.getFullEndpoint(VideosApiPath.ID, { id }),
{
method: HTTPMethod.GET,
contentType: ContentType.JSON,
hasAuth: true,
},
);

return await response.json<VideoGetAllItemResponseDto>();
}

public async deleteVideo(id: string): Promise<void> {
const response = await this.load(
this.getFullEndpoint(`${VideosApiPath.ROOT}${id}`, {}),
Expand All @@ -81,6 +97,23 @@ class VideosApi extends BaseHttpApi {

await response.json<boolean>();
}

public async getVideoIdJWT(id: string): Promise<string> {

const response = await this.load(
this.getFullEndpoint(`${VideosApiPath.ROOT}${id}/share`, {}),
{
method: HTTPMethod.GET,
contentType: ContentType.JSON,
hasAuth: true,
},
);

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

export { VideosApi };
Loading
Loading