-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from 10 commits
b868514
49af338
06e0c10
945fac9
c19a3fa
01bf93f
e7cc793
5e9044d
ba2031e
423cabc
5c7e932
480e38f
556ed1d
907bdaf
c2ebd64
32b55b7
7d1c536
3ae5db1
d932a5a
89fd45d
b012825
b8e9f78
34e1084
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
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 }; |
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.getVideoIdFromToken(token); | ||||||
|
||||||
if (!id) { | ||||||
this.throwVideoNotFoundError(); | ||||||
} | ||||||
|
||||||
const video = await this.videoRepository.findById(id); | ||||||
|
||||||
if (!video) { | ||||||
this.throwVideoNotFoundError(); | ||||||
} | ||||||
|
||||||
const url = video.toObject().url; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
if (!url) { | ||||||
this.throwVideoNotFoundError(); | ||||||
} | ||||||
return url; | ||||||
} | ||||||
|
||||||
private throwVideoNotFoundError(): never { | ||||||
throw new HttpError({ | ||||||
message: VideoValidationMessage.VIDEO_DOESNT_EXIST, | ||||||
status: HTTPCode.NOT_FOUND, | ||||||
}); | ||||||
} | ||||||
} | ||||||
|
||||||
export { PublicVideoService }; |
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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,6 +109,17 @@ class VideoController extends BaseController { | |
}>, | ||
), | ||
}); | ||
this.addRoute({ | ||
path: `${VideosApiPath.ID}/share`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
}>, | ||
); | ||
}, | ||
}); | ||
} | ||
|
||
/** | ||
|
@@ -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/: | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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('.', '~'); | ||||||
} | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets move it to video service There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use createToken method and set expiration time optional? |
||||||
public async verifyToken(token: string): Promise<TokenPayload | null> { | ||||||
try { | ||||||
const { payload } = await jwtVerify(token, this.secretKey); | ||||||
|
@@ -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; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
} | ||||||
|
||||||
export { TokenService }; |
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 }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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}`, {}), | ||
|
@@ -81,6 +97,24 @@ 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`, {}), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
{ | ||
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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need this nullish coalescing here? You already check it on line 32