From 7675dd42848486ccf93ab626ed90985adf653ef5 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:41:19 +0200 Subject: [PATCH 01/19] Allow public access to video API endpoints --- .../src/main/java/com/csed/knowtopia/config/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/main/java/com/csed/knowtopia/config/SecurityConfig.java b/backend/src/main/java/com/csed/knowtopia/config/SecurityConfig.java index f49d641..308e5e5 100644 --- a/backend/src/main/java/com/csed/knowtopia/config/SecurityConfig.java +++ b/backend/src/main/java/com/csed/knowtopia/config/SecurityConfig.java @@ -41,6 +41,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/auth/**").permitAll() // Allow all requests to /api/auth/** .requestMatchers("/api/courses/search/**").permitAll() // Allow all requests to /api/courses/search/** .requestMatchers("/api/courses/preview/**").permitAll() // Allow all requests to /api/courses/search/** + .requestMatchers("/api/videos/**").permitAll() // Protected endpoints .requestMatchers("/api/admin/**").hasRole("ADMIN") // Allow only users with role ADMIN to access /api/admin/** From 694d55eaab869e43f52a61bdfe542a6744bb0bb7 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:41:29 +0200 Subject: [PATCH 02/19] Add ObjectMapper configuration for JSON processing --- .../csed/knowtopia/config/ObjectMapperConfig.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java diff --git a/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java b/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java new file mode 100644 index 0000000..b7f6ec1 --- /dev/null +++ b/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java @@ -0,0 +1,15 @@ +package com.csed.knowtopia.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ObjectMapperConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} From a2015cb518849fe7432cad9c96528951dd22fac7 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:41:51 +0200 Subject: [PATCH 03/19] Refactor file and video upload methods to use MultipartFile directly and update validation logic --- .../controller/CourseStorageController.java | 7 +++++-- .../knowtopia/validator/FileValidator.java | 2 +- .../CourseStorageControllerTest.java | 19 ++++++------------- .../CourseStorageIntegrationTest.java | 11 +++-------- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java b/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java index 2c326a6..0beeb34 100644 --- a/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java +++ b/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java @@ -15,6 +15,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @@ -30,7 +31,8 @@ public class CourseStorageController { content = @Content(schema = @Schema(implementation = FileResponseDTO.class))) @ApiResponse(responseCode = "400", description = "Invalid file or request") @PostMapping("/files") - public ResponseEntity uploadFile(@Valid @ModelAttribute FileUploadDTO fileDTO) throws IOException { + public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) throws IOException { + FileUploadDTO fileDTO = new FileUploadDTO(file, null); return ResponseEntity.ok(storageService.storeFile(fileDTO)); } @@ -55,7 +57,8 @@ public ResponseEntity deleteFile(@PathVariable long id) { content = @Content(schema = @Schema(implementation = VideoResponseDTO.class))) @ApiResponse(responseCode = "400", description = "Invalid video or request") @PostMapping("/videos") - public ResponseEntity uploadVideo(@Valid @ModelAttribute VideoUploadDTO videoDTO) throws IOException { + public ResponseEntity uploadVideo(@RequestParam("file") MultipartFile video) throws IOException { + VideoUploadDTO videoDTO = new VideoUploadDTO(video, null); return ResponseEntity.ok(storageService.storeVideo(videoDTO)); } diff --git a/backend/src/main/java/com/csed/knowtopia/validator/FileValidator.java b/backend/src/main/java/com/csed/knowtopia/validator/FileValidator.java index 7ad5de2..e026fd0 100644 --- a/backend/src/main/java/com/csed/knowtopia/validator/FileValidator.java +++ b/backend/src/main/java/com/csed/knowtopia/validator/FileValidator.java @@ -24,7 +24,7 @@ public void validateVideo(MultipartFile file) { } private void validateFile(MultipartFile file, List allowedTypes, long maxSize) { - if (file.isEmpty()) { + if (file == null || file.isEmpty()) { throw new FileValidationException("File is empty"); } if (file.getSize() > maxSize) { diff --git a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java index 27cd37b..edcd947 100644 --- a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java +++ b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java @@ -88,13 +88,11 @@ void uploadFile_Success() throws Exception { when(storageService.storeFile(any(FileUploadDTO.class))).thenReturn(fileResponseDTO); mockMvc.perform(multipart("/api/files") - .file(testFile) - .param("description", "Test file")) + .file(testFile)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.filename").value("test.pdf")) - .andExpect(jsonPath("$.url").value("/uploads/test.pdf")) - .andExpect(jsonPath("$.description").value("Test file")); + .andExpect(jsonPath("$.url").value("/uploads/test.pdf")); verify(storageService).storeFile(any(FileUploadDTO.class)); } @@ -102,8 +100,7 @@ void uploadFile_Success() throws Exception { @Test @WithMockUser(roles = "USER") void uploadFile_NoFile_BadRequest() throws Exception { - mockMvc.perform(multipart("/api/files") - .param("description", "Test file")) + mockMvc.perform(multipart("/api/files")) .andExpect(status().isBadRequest()); } @@ -138,22 +135,18 @@ void uploadVideo_Success() throws Exception { when(storageService.storeVideo(any(VideoUploadDTO.class))).thenReturn(videoResponseDTO); mockMvc.perform(multipart("/api/videos") - .file(testVideo) - .param("description", "Test video")) + .file(testVideo)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.filename").value("test.mp4")) - .andExpect(jsonPath("$.url").value("http://cloudinary.com/test.mp4")) - .andExpect(jsonPath("$.description").value("Test video")); - + .andExpect(jsonPath("$.url").value("http://cloudinary.com/test.mp4")); verify(storageService).storeVideo(any(VideoUploadDTO.class)); } @Test @WithMockUser(roles = "USER") void uploadVideo_NoFile_BadRequest() throws Exception { - mockMvc.perform(multipart("/api/videos") - .param("description", "Test video")) + mockMvc.perform(multipart("/api/videos")) .andExpect(status().isBadRequest()); } diff --git a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java index 05144d4..501ba95 100644 --- a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java +++ b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java @@ -79,8 +79,7 @@ void testFileUpload() throws Exception { ); MvcResult result = mockMvc.perform(multipart("/api/files") - .file(file) - .param("description", "Test file description")) + .file(file)) .andExpect(status().isOk()) .andReturn(); @@ -93,7 +92,6 @@ void testFileUpload() throws Exception { assertNotNull(response.getId()); assertEquals("test.txt", response.getFilename()); assertNotNull(response.getUrl()); - assertEquals("Test file description", response.getDescription()); // Verify file exists in filesystem assertTrue(Files.exists(Paths.get(response.getUrl()))); @@ -133,8 +131,7 @@ void testVideoUpload() throws Exception { ); MvcResult result = mockMvc.perform(multipart("/api/videos") - .file(multipartFile) - .param("description", "Test video description")) + .file(multipartFile)) .andExpect(status().isOk()) .andReturn(); @@ -148,7 +145,6 @@ void testVideoUpload() throws Exception { assertEquals("test-video.mp4", response.getFilename()); assertNotNull(response.getUrl()); assertTrue(response.getUrl().startsWith("http")); // Cloudinary URL - assertEquals("Test video description", response.getDescription()); } @Test @@ -216,8 +212,7 @@ void testUploadInvalidFile() throws Exception { ); mockMvc.perform(multipart("/api/files") - .file(file) - .param("description", "Invalid file")) + .file(file)) .andExpect(status().isBadRequest()); } From c2f493447e6947aa902b046051ff460e41cff785 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:42:12 +0200 Subject: [PATCH 04/19] Update Cloudinary configuration and enable multipart file uploads with size limits --- backend/src/main/resources/application.properties | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 6ac064f..7729ca1 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -28,4 +28,9 @@ spring.security.oauth2.client.registration.google.client-secret=GOCSPX-Uda9XEuVp # Cloudinary Configuration cloudinary.cloud-name=dfwyg3lbr cloudinary.api-key=764171541677637 -cloudinary.api-secret=nzfm9vGh-bBCfyKCSQkKwnZ26aE \ No newline at end of file +cloudinary.api-secret=nzfm9vGh-bBCfyKCSQkKwnZ26aE + +spring.servlet.multipart.max-file-size=100MB +spring.servlet.multipart.max-request-size=100MB +spring.servlet.multipart.enabled=true +spring.servlet.multipart.resolve-lazily=true \ No newline at end of file From e7da354b44052b8b69c80442ae1d483e84b881ea Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:42:47 +0200 Subject: [PATCH 05/19] Add cloudinary-video-player dependency to frontend --- frontend/package-lock.json | 851 ++++++++++++++++++++++++++++++++++++- frontend/package.json | 1 + 2 files changed, 848 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc73f45..0724c0d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@mdi/font": "^7.4.47", "axios": "^1.7.8", + "cloudinary-video-player": "^2.2.0", "core-js": "^3.8.3", "vue": "^3.2.13", "vue-router": "^4.0.3", @@ -1727,7 +1728,6 @@ "version": "7.26.0", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz", "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1783,6 +1783,22 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudinary/transformation-builder-sdk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@cloudinary/transformation-builder-sdk/-/transformation-builder-sdk-1.16.1.tgz", + "integrity": "sha512-Mh1qYMkoDxSAzbt0qY9NJaZrdH/vFBcrpeVWmbTXbPVDZHLaaLyJ2+RDFGger5lycbrehPLoNp2hh22BvhkvbQ==", + "dependencies": { + "@cloudinary/url-gen": "^1.7.0" + } + }, + "node_modules/@cloudinary/url-gen": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@cloudinary/url-gen/-/url-gen-1.21.0.tgz", + "integrity": "sha512-ctYcCzX3G3vcgnESTU2ET3K1XsBiXcEnBddCGV0QbR3fJhLLrIShjSMEwZoepgh4LAFOHJu9DzvLFr+E8R7c7g==", + "dependencies": { + "@cloudinary/transformation-builder-sdk": "^1.15.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -1853,11 +1869,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-5.1.0.tgz", + "integrity": "sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==", + "dependencies": { + "@hapi/boom": "9.x.x" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@hapi/topo": { @@ -2361,6 +2395,51 @@ "@types/node": "*" } }, + "node_modules/@videojs/http-streaming": { + "version": "3.16.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.2.tgz", + "integrity": "sha512-fvt4ko7FknxiT9FnjyNQt6q2px+awrkM+Orv7IB/4gldvj94u4fowGfmNHynnvNTPgPkdxHklGmFLGfclYw8HA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "aes-decrypter": "^4.0.2", + "global": "^4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "7.1.0", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.19.0" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz", + "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/@videojs/xhr": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz", + "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, "node_modules/@vue/babel-helper-vue-jsx-merge-props": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", @@ -3237,6 +3316,14 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -3321,6 +3408,17 @@ "node": ">= 10.0.0" } }, + "node_modules/aes-decrypter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz", + "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", @@ -3725,6 +3823,42 @@ "dev": true, "license": "MIT" }, + "node_modules/bcp-47": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-1.0.8.tgz", + "integrity": "sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-1.0.3.tgz", + "integrity": "sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-normalize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-1.1.1.tgz", + "integrity": "sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==", + "dependencies": { + "bcp-47": "^1.0.0", + "bcp-47-match": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz", @@ -3977,6 +4111,11 @@ "node": ">=6" } }, + "node_modules/can-autoplay": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/can-autoplay/-/can-autoplay-3.0.2.tgz", + "integrity": "sha512-Ih6wc7yJB4TylS/mLyAW0Dj5Nh3Gftq/g966TcxgvpNCOzlbqTs85srAq7mwIspo4w8gnLCVVGroyCHfh6l9aA==" + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -4220,6 +4359,64 @@ "node": ">=6" } }, + "node_modules/cloudinary-video-analytics": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/cloudinary-video-analytics/-/cloudinary-video-analytics-1.7.1.tgz", + "integrity": "sha512-LIQjztFX8zakYT0fHaka1NTRDeEZPWbx65tqn6tK4t+2BMvrTjL6cmdYaJ8QpsiDJT7mwWtFnIMlLEkLmdHdsg==", + "dependencies": { + "is-mobile": "^4.0.0", + "uuid": "9.0.0" + } + }, + "node_modules/cloudinary-video-analytics/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/cloudinary-video-player": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cloudinary-video-player/-/cloudinary-video-player-2.2.0.tgz", + "integrity": "sha512-9YHaEHpJhzW+OkySOchkHHWJuSDd3Xa8SCEnXrcs8EjBsyp6jOAJpBRqHfxREF4bmk1M8CZLNSfuxJWWlKQABw==", + "dependencies": { + "@cloudinary/url-gen": "^1.20.0", + "cloudinary-video-analytics": "1.7.1", + "cloudinary-video-player-profiles": "1.1.0", + "lodash": "^4.17.21", + "srt-parser-2": "^1.2.3", + "uuid": "^10.0.0", + "video.js": "^8.17.1", + "videojs-contrib-ads": "^7.5.2", + "videojs-contrib-dash": "^5.1.1", + "videojs-ima": "^2.3.0", + "videojs-per-source-behaviors": "^3.0.1", + "webfontloader": "^1.6.28" + } + }, + "node_modules/cloudinary-video-player-profiles": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cloudinary-video-player-profiles/-/cloudinary-video-player-profiles-1.1.0.tgz", + "integrity": "sha512-vBpoDmDEq6+iViASIrsbRPNhiykwnnbfW4kdiYyrbdU6FFzogTun+6jmlbw/FR6XG2FEn4s69OOFRlu/qsl01Q==" + }, + "node_modules/cloudinary-video-player/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/codem-isoboxer": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.9.tgz", + "integrity": "sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA==" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -4843,6 +5040,33 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/dashjs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-4.7.4.tgz", + "integrity": "sha512-+hldo25QPP3H/NOwqUrvt4uKdMse60/Gsz9AUAnoYfhga8qHWq4nWiojUosOiigbigkDTCAn9ORcvUaKCvmfCA==", + "dependencies": { + "bcp-47-match": "^1.0.3", + "bcp-47-normalize": "^1.1.1", + "codem-isoboxer": "0.3.9", + "es6-promise": "^4.2.8", + "fast-deep-equal": "2.0.1", + "html-entities": "^1.2.1", + "imsc": "^1.1.5", + "localforage": "^1.7.1", + "path-browserify": "^1.0.1", + "ua-parser-js": "^1.0.37" + } + }, + "node_modules/dashjs/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + }, + "node_modules/dashjs/node_modules/html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==" + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/debounce/-/debounce-1.2.1.tgz", @@ -5195,6 +5419,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", @@ -5425,6 +5654,11 @@ "dev": true, "license": "MIT" }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", @@ -6136,6 +6370,11 @@ "dev": true, "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6581,6 +6820,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", @@ -7063,6 +7311,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz", @@ -7080,6 +7333,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/imsc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", + "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==", + "dependencies": { + "sax": "1.2.1" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7090,6 +7351,11 @@ "node": ">=0.8.19" } }, + "node_modules/individual": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz", + "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", @@ -7119,6 +7385,28 @@ "node": ">= 10" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7168,6 +7456,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-2.2.1.tgz", @@ -7214,6 +7511,11 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", @@ -7237,6 +7539,11 @@ "node": ">=8" } }, + "node_modules/is-mobile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", @@ -7490,6 +7797,11 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keycode": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -7555,6 +7867,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-2.1.0.tgz", @@ -7610,6 +7930,14 @@ "json5": "lib/cli.js" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", @@ -7627,7 +7955,6 @@ "version": "4.17.21", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -7866,6 +8193,16 @@ "yallist": "^3.0.2" } }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, "node_modules/magic-string": { "version": "0.30.14", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.14.tgz", @@ -8026,6 +8363,14 @@ "node": ">=6" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.2", "resolved": "https://registry.npmmirror.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", @@ -8174,6 +8519,20 @@ "dev": true, "license": "MIT" }, + "node_modules/mpd-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz", + "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.0.0", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.0.tgz", @@ -8205,6 +8564,22 @@ "multicast-dns": "cli.js" } }, + "node_modules/mux.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz", + "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", @@ -8743,6 +9118,11 @@ "tslib": "^2.0.3" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", @@ -8816,6 +9196,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -9500,6 +9891,14 @@ "renderkid": "^3.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9830,7 +10229,6 @@ "version": "0.14.1", "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -10049,6 +10447,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rust-result": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz", + "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==", + "dependencies": { + "individual": "^2.0.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10070,6 +10476,14 @@ ], "license": "MIT" }, + "node_modules/safe-json-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz", + "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==", + "dependencies": { + "rust-result": "^1.0.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10077,6 +10491,11 @@ "dev": true, "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, "node_modules/schema-utils": { "version": "2.7.1", "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz", @@ -10551,6 +10970,17 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/srt-parser-2": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/srt-parser-2/-/srt-parser-2-1.2.3.tgz", + "integrity": "sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A==", + "bin": { + "srt-parser-2": "bin/index.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz", @@ -11061,6 +11491,31 @@ "node": ">= 0.6" } }, + "node_modules/ua-parser-js": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz", @@ -11173,6 +11628,11 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11235,6 +11695,384 @@ "node": ">= 0.8" } }, + "node_modules/video.js": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.21.0.tgz", + "integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "^3.16.2", + "@videojs/vhs-utils": "^4.1.1", + "@videojs/xhr": "2.7.0", + "aes-decrypter": "^4.0.2", + "global": "4.4.0", + "m3u8-parser": "^7.2.0", + "mpd-parser": "^1.3.1", + "mux.js": "^7.0.1", + "videojs-contrib-quality-levels": "4.1.0", + "videojs-font": "4.2.0", + "videojs-vtt.js": "0.15.5" + } + }, + "node_modules/videojs-contrib-ads": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/videojs-contrib-ads/-/videojs-contrib-ads-7.5.2.tgz", + "integrity": "sha512-hrLnWwAVL0CJJPFNuWR0jV+SpW/TWQx7nQkZxMVn2CWZZGMZe2fowtUfjf8U9gozTfM09wKBvHn1mtNg9m3VPg==", + "dependencies": { + "global": "^4.3.2" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^8.11.6" + } + }, + "node_modules/videojs-contrib-dash": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/videojs-contrib-dash/-/videojs-contrib-dash-5.1.1.tgz", + "integrity": "sha512-MI0kPHuQ3KH9Mc2mLVLqvFKCoEyTfXzHc02fm8pqMk8v7LXrJKnIv9xfugBccRF7vZHDZISftedD/CmEJfvvrA==", + "dependencies": { + "dashjs": "^4.2.0", + "global": "^4.3.2", + "video.js": "^5.18.0 || ^6 || ^7" + } + }, + "node_modules/videojs-contrib-dash/node_modules/@videojs/http-streaming": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.16.3.tgz", + "integrity": "sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^6 || ^7" + } + }, + "node_modules/videojs-contrib-dash/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-contrib-dash/node_modules/@videojs/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "node_modules/videojs-contrib-dash/node_modules/aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/videojs-contrib-dash/node_modules/m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, + "node_modules/videojs-contrib-dash/node_modules/mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, + "node_modules/videojs-contrib-dash/node_modules/mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-contrib-dash/node_modules/video.js": { + "version": "7.21.6", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.6.tgz", + "integrity": "sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" + } + }, + "node_modules/videojs-contrib-dash/node_modules/videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" + }, + "node_modules/videojs-contrib-quality-levels": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", + "dependencies": { + "global": "^4.4.0" + }, + "engines": { + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "video.js": "^8" + } + }, + "node_modules/videojs-font": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz", + "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==" + }, + "node_modules/videojs-ima": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/videojs-ima/-/videojs-ima-2.3.0.tgz", + "integrity": "sha512-8r0BZGT+WCTO6PePyKZHikV79Ojqh4yLMx4+DmPyXeRcKUVsQ7Va0R7Ok8GRcA8Zy3l1PM6jzLrD/W1rwKhZ8g==", + "dependencies": { + "@hapi/cryptiles": "^5.1.0", + "can-autoplay": "^3.0.2", + "extend": ">=3.0.2", + "videojs-contrib-ads": "^6.9.0" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "video.js": "^5.19.2 || ^6 || ^7 || ^8" + } + }, + "node_modules/videojs-ima/node_modules/@videojs/http-streaming": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.16.3.tgz", + "integrity": "sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "3.0.5", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "^0.22.1", + "mux.js": "6.0.1", + "video.js": "^6 || ^7" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "video.js": "^6 || ^7" + } + }, + "node_modules/videojs-ima/node_modules/@videojs/http-streaming/node_modules/video.js": { + "version": "7.21.6", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.6.tgz", + "integrity": "sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" + } + }, + "node_modules/videojs-ima/node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-ima/node_modules/@videojs/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "global": "~4.4.0", + "is-function": "^1.0.1" + } + }, + "node_modules/videojs-ima/node_modules/aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/videojs-ima/node_modules/m3u8-parser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.8.0.tgz", + "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0" + } + }, + "node_modules/videojs-ima/node_modules/mpd-parser": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.22.1.tgz", + "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "@xmldom/xmldom": "^0.8.3", + "global": "^4.4.0" + }, + "bin": { + "mpd-to-m3u8-json": "bin/parse.js" + } + }, + "node_modules/videojs-ima/node_modules/mux.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz", + "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "global": "^4.4.0" + }, + "bin": { + "muxjs-transmux": "bin/transmux.js" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-ima/node_modules/videojs-contrib-ads": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/videojs-contrib-ads/-/videojs-contrib-ads-6.9.0.tgz", + "integrity": "sha512-nzKz+jhCGMTYffSNVYrmp9p70s05v6jUMOY3Z7DpVk3iFrWK4Zi/BIkokDWrMoHpKjdmCdKzfJVBT+CrUj6Spw==", + "dependencies": { + "global": "^4.3.2", + "video.js": "^6 || ^7" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/videojs-ima/node_modules/videojs-contrib-ads/node_modules/video.js": { + "version": "7.21.6", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-7.21.6.tgz", + "integrity": "sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/http-streaming": "2.16.3", + "@videojs/vhs-utils": "^3.0.4", + "@videojs/xhr": "2.6.0", + "aes-decrypter": "3.1.3", + "global": "^4.4.0", + "keycode": "^2.2.0", + "m3u8-parser": "4.8.0", + "mpd-parser": "0.22.1", + "mux.js": "6.0.1", + "safe-json-parse": "4.0.0", + "videojs-font": "3.2.0", + "videojs-vtt.js": "^0.15.5" + } + }, + "node_modules/videojs-ima/node_modules/videojs-font": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", + "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" + }, + "node_modules/videojs-per-source-behaviors": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/videojs-per-source-behaviors/-/videojs-per-source-behaviors-3.0.1.tgz", + "integrity": "sha512-M/s4qSP2OlQkAbElYstPdcAeLHXXtHlY1HNNQB2Cpa0c+oJFpLW595EOIXQ7B18agUv+POvLxuXVEGmq/SQOeA==", + "dependencies": { + "global": "^4.4.0", + "video.js": "^7 || ^8" + }, + "engines": { + "node": ">=4.4.0" + } + }, + "node_modules/videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "dependencies": { + "global": "^4.3.1" + } + }, "node_modules/vue": { "version": "3.5.13", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.13.tgz", @@ -11485,6 +12323,11 @@ "defaults": "^1.0.3" } }, + "node_modules/webfontloader": { + "version": "1.6.28", + "resolved": "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz", + "integrity": "sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6ac3ea3..a5ccd0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@mdi/font": "^7.4.47", "axios": "^1.7.8", + "cloudinary-video-player": "^2.2.0", "core-js": "^3.8.3", "vue": "^3.2.13", "vue-router": "^4.0.3", From 5ce15325da11ca21f6a2030acf841b67ef7caa60 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:43:25 +0200 Subject: [PATCH 06/19] Add video upload and playback functionality with Cloudinary integration --- frontend/src/components/VideoComponent.vue | 54 +++++++++ frontend/src/pages/UploadAndPlayPage.vue | 124 +++++++++++++++++++++ frontend/src/router/index.js | 6 + frontend/src/services/api.js | 2 +- frontend/src/services/videoService.js | 10 ++ 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/VideoComponent.vue create mode 100644 frontend/src/pages/UploadAndPlayPage.vue create mode 100644 frontend/src/services/videoService.js diff --git a/frontend/src/components/VideoComponent.vue b/frontend/src/components/VideoComponent.vue new file mode 100644 index 0000000..aac320f --- /dev/null +++ b/frontend/src/components/VideoComponent.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/pages/UploadAndPlayPage.vue b/frontend/src/pages/UploadAndPlayPage.vue new file mode 100644 index 0000000..ec9fb2f --- /dev/null +++ b/frontend/src/pages/UploadAndPlayPage.vue @@ -0,0 +1,124 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 633ea61..3933450 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -31,6 +31,12 @@ const routes = [ component: () => import("../pages/CourseDetailPage.vue"), meta: { requiresAuth: true }, }, + { + path: "/upload_and_play", + name: "UploadAndPlay", + component: () => import("../pages/UploadAndPlayPage.vue"), + meta: { requiresAuth: true }, + }, { path: "/search_results", name: "SearchResults", diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 99595e8..d8049c4 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -5,7 +5,7 @@ import { loadToken } from "./storage"; const api = axios.create({ baseURL: API_BASE_URL, headers: { - "Content-Type": "application/json", + // Remove default Content-Type as it will be set automatically }, }); diff --git a/frontend/src/services/videoService.js b/frontend/src/services/videoService.js new file mode 100644 index 0000000..bfae1b8 --- /dev/null +++ b/frontend/src/services/videoService.js @@ -0,0 +1,10 @@ +import { postData, fetchData } from './api'; + +export const postVideo = async (video_data) => { + console.log(video_data); + return await postData('/videos', video_data); +}; + +export const getVideo = async (id) => { + return await fetchData(`/videos/${id}`); +}; From 97a1583df34dd248f460f444bafa4c70721d9638 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 18 Dec 2024 07:52:00 +0200 Subject: [PATCH 07/19] Remove objectmapping as it caused failed json parsing --- .../csed/knowtopia/config/ObjectMapperConfig.java | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java diff --git a/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java b/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java deleted file mode 100644 index b7f6ec1..0000000 --- a/backend/src/main/java/com/csed/knowtopia/config/ObjectMapperConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.csed.knowtopia.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.modelmapper.ModelMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class ObjectMapperConfig { - - @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } -} From 83f2f8afc3abe5bb8e5194de0581569c3f5ae240 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 04:55:28 +0200 Subject: [PATCH 08/19] Add nullable constraint and length limit to Video URL field --- backend/src/main/java/com/csed/knowtopia/entity/Video.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/java/com/csed/knowtopia/entity/Video.java b/backend/src/main/java/com/csed/knowtopia/entity/Video.java index 9980120..b420658 100644 --- a/backend/src/main/java/com/csed/knowtopia/entity/Video.java +++ b/backend/src/main/java/com/csed/knowtopia/entity/Video.java @@ -17,7 +17,10 @@ public class Video { private Long id; private String filename; + + @Column(nullable = false, length = 2083) private String url; + private String description; @Column(updatable = false) From 85ef529dd10a2a5a6bf9fa63e6c26c40ae12446e Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 04:55:37 +0200 Subject: [PATCH 09/19] Refactor Lesson entity to replace videoLink with a Video entity reference --- .../main/java/com/csed/knowtopia/entity/Lesson.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/csed/knowtopia/entity/Lesson.java b/backend/src/main/java/com/csed/knowtopia/entity/Lesson.java index bf27194..3a566ba 100644 --- a/backend/src/main/java/com/csed/knowtopia/entity/Lesson.java +++ b/backend/src/main/java/com/csed/knowtopia/entity/Lesson.java @@ -30,9 +30,10 @@ public class Lesson { @JoinColumn(name = "module_id", nullable = false) private Module module; - @NotEmpty - @Column(nullable = false, length = 2083) - private String videoLink; + @OneToOne + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "video_id") + private Video video; @PastOrPresent @Column(name = "creation_date") @@ -55,8 +56,8 @@ public class Lesson { public Module getModule() { return module; } public void setModule(Module module) { this.module = module; } - public String getVideoLink() { return videoLink; } - public void setVideoLink(String videoLink) { this.videoLink = videoLink; } + public Video getVideo() { return video; } + public void setVideo(Video video) { this.video = video; } public LocalDate getCreationDate() { return creationDate; } public void setCreationDate(LocalDate creationDate){ this.creationDate = creationDate; } From e2c0fbf8b5f1236b945acd391593413d38ce0a73 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 04:55:46 +0200 Subject: [PATCH 10/19] Remove videoLink update from LessonService during lesson update --- .../src/main/java/com/csed/knowtopia/service/LessonService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/main/java/com/csed/knowtopia/service/LessonService.java b/backend/src/main/java/com/csed/knowtopia/service/LessonService.java index 7b0c369..5fa8515 100644 --- a/backend/src/main/java/com/csed/knowtopia/service/LessonService.java +++ b/backend/src/main/java/com/csed/knowtopia/service/LessonService.java @@ -35,7 +35,6 @@ private Lesson updateEntries(Lesson oldLesson, Lesson newLesson) { if (newLesson.getTitle() != null) oldLesson.setTitle(newLesson.getTitle()); if (newLesson.getDescription() != null) oldLesson.setDescription(newLesson.getDescription()); if (newLesson.getOrderIndex() != null) oldLesson.setOrderIndex(newLesson.getOrderIndex()); - if (newLesson.getVideoLink() != null) oldLesson.setVideoLink(newLesson.getVideoLink()); return oldLesson; } From 15c61f7be0b2aeab490e8f01b6b3dd2baf5add33 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 04:55:59 +0200 Subject: [PATCH 11/19] Update ModuleService to use Video entity URL instead of videoLink --- .../src/main/java/com/csed/knowtopia/service/ModuleService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/csed/knowtopia/service/ModuleService.java b/backend/src/main/java/com/csed/knowtopia/service/ModuleService.java index a76d825..8fab063 100644 --- a/backend/src/main/java/com/csed/knowtopia/service/ModuleService.java +++ b/backend/src/main/java/com/csed/knowtopia/service/ModuleService.java @@ -91,7 +91,7 @@ public CourseModuleDTO getCourseModules(Long courseId) { .id(lesson.getId()) .title(lesson.getTitle()) .orderIndex(lesson.getOrderIndex()) - .videoLink(lesson.getVideoLink()) + .videoLink(lesson.getVideo().getUrl()) .description(lesson.getDescription()) .creationDate(lesson.getCreationDate()) .moduleId(lesson.getModule().getId()) From a07eab3babb0754c80a437d3190a3f3f7972d2c8 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:30:17 +0200 Subject: [PATCH 12/19] Implement video upload for specific lessons in CourseStorageController --- .../controller/CourseStorageController.java | 47 ++++++++++--------- .../knowtopia/dto/request/VideoUploadDTO.java | 4 ++ 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java b/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java index 0beeb34..d934d14 100644 --- a/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java +++ b/backend/src/main/java/com/csed/knowtopia/controller/CourseStorageController.java @@ -1,10 +1,14 @@ package com.csed.knowtopia.controller; +import com.csed.knowtopia.dto.LessonDTO; import com.csed.knowtopia.dto.request.FileUploadDTO; import com.csed.knowtopia.dto.request.VideoUploadDTO; import com.csed.knowtopia.dto.response.FileResponseDTO; import com.csed.knowtopia.dto.response.VideoResponseDTO; +import com.csed.knowtopia.exception.UnauthorizedException; import com.csed.knowtopia.service.CourseStorageService; +import com.csed.knowtopia.service.LessonService; +import com.csed.knowtopia.service.CustomUserPrincipal; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -14,8 +18,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.security.core.AuthenticationException; +import org.springframework.http.HttpStatus; import java.io.IOException; @@ -25,6 +32,7 @@ @Tag(name = "Course Storage", description = "APIs for managing course files and videos") public class CourseStorageController { private final CourseStorageService storageService; + private final LessonService lessonService; @Operation(summary = "Upload a file", description = "Upload a file for a course with optional description") @ApiResponse(responseCode = "200", description = "File uploaded successfully", @@ -52,29 +60,26 @@ public ResponseEntity deleteFile(@PathVariable long id) { return ResponseEntity.ok(storageService.removeFile(id)); } - @Operation(summary = "Upload a video", description = "Upload a video for a course with optional description") + @Operation(summary = "Upload a video to a lesson", description = "Upload a video for a specific lesson") @ApiResponse(responseCode = "200", description = "Video uploaded successfully", - content = @Content(schema = @Schema(implementation = VideoResponseDTO.class))) + content = @Content(schema = @Schema(implementation = LessonDTO.class))) @ApiResponse(responseCode = "400", description = "Invalid video or request") - @PostMapping("/videos") - public ResponseEntity uploadVideo(@RequestParam("file") MultipartFile video) throws IOException { - VideoUploadDTO videoDTO = new VideoUploadDTO(video, null); - return ResponseEntity.ok(storageService.storeVideo(videoDTO)); + @ApiResponse(responseCode = "401", description = "Unauthorized") + @ApiResponse(responseCode = "404", description = "Lesson not found") + @PostMapping("/lessons/{lessonId}/video") + public ResponseEntity uploadLessonVideo( + @PathVariable Long lessonId, + @RequestParam("file") MultipartFile video, + @AuthenticationPrincipal CustomUserPrincipal principal + ) throws IOException { + if (principal == null) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("Authentication required"); + } + VideoUploadDTO videoDTO = new VideoUploadDTO(video, lessonId, null); + return ResponseEntity.ok(lessonService.updateLessonVideo(videoDTO, principal.getUser())); } - @Operation(summary = "Get video metadata", description = "Retrieve metadata for a specific video") - @ApiResponse(responseCode = "200", description = "Video metadata retrieved successfully") - @ApiResponse(responseCode = "404", description = "Video not found") - @GetMapping("/videos/{id}") - public ResponseEntity getVideoMetadata(@PathVariable long id) { - return ResponseEntity.ok(storageService.retrieveVideoMetadata(id)); - } - - @Operation(summary = "Delete a video", description = "Delete a specific video by ID") - @ApiResponse(responseCode = "200", description = "Video deleted successfully") - @ApiResponse(responseCode = "404", description = "Video not found") - @DeleteMapping("/videos/{id}") - public ResponseEntity deleteVideo(@PathVariable long id) { - return ResponseEntity.ok(storageService.removeVideo(id)); - } + } diff --git a/backend/src/main/java/com/csed/knowtopia/dto/request/VideoUploadDTO.java b/backend/src/main/java/com/csed/knowtopia/dto/request/VideoUploadDTO.java index cf76c10..4f850e9 100644 --- a/backend/src/main/java/com/csed/knowtopia/dto/request/VideoUploadDTO.java +++ b/backend/src/main/java/com/csed/knowtopia/dto/request/VideoUploadDTO.java @@ -17,6 +17,10 @@ public class VideoUploadDTO { @NotNull(message = "File is required") private MultipartFile file; + @NotNull(message = "LessonId is required") + @Schema(description = "Id of the lesson to which the video belongs") + private Long lessonId; + @Schema(description = "Optional description of the video") @Nullable private String description; From 78b463b1a97efb0b8e37daedcf23746744f0fe7f Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:30:25 +0200 Subject: [PATCH 13/19] Add ForbiddenException and handler to GlobalExceptionHandler --- .../knowtopia/exception/ForbiddenException.java | 7 +++++++ .../handler/GlobalExceptionHandler.java | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/csed/knowtopia/exception/ForbiddenException.java diff --git a/backend/src/main/java/com/csed/knowtopia/exception/ForbiddenException.java b/backend/src/main/java/com/csed/knowtopia/exception/ForbiddenException.java new file mode 100644 index 0000000..b13aa5a --- /dev/null +++ b/backend/src/main/java/com/csed/knowtopia/exception/ForbiddenException.java @@ -0,0 +1,7 @@ +package com.csed.knowtopia.exception; + +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/csed/knowtopia/handler/GlobalExceptionHandler.java b/backend/src/main/java/com/csed/knowtopia/handler/GlobalExceptionHandler.java index 4e1b486..28a044e 100644 --- a/backend/src/main/java/com/csed/knowtopia/handler/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/csed/knowtopia/handler/GlobalExceptionHandler.java @@ -1,7 +1,6 @@ package com.csed.knowtopia.handler; -import com.csed.knowtopia.exception.ErrorResponse; -import com.csed.knowtopia.exception.UnauthorizedException; +import com.csed.knowtopia.exception.*; import jakarta.persistence.EntityNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,9 +9,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import com.csed.knowtopia.exception.FileValidationException; -import com.csed.knowtopia.exception.ResourceNotFoundException; - import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -60,4 +56,15 @@ public ResponseEntity handleUnauthorizedException(UnauthorizedExc ); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse); } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException ex) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + ex.getMessage(), + null, + LocalDateTime.now() + ); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse); + } } \ No newline at end of file From 54a2c657d90780c85d27cc17f022d84a50baa668 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:30:35 +0200 Subject: [PATCH 14/19] Refactor storeVideo method to return Video entity instead of VideoResponseDTO --- .../csed/knowtopia/service/CourseStorageService.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/csed/knowtopia/service/CourseStorageService.java b/backend/src/main/java/com/csed/knowtopia/service/CourseStorageService.java index ef47e2c..1f20d46 100644 --- a/backend/src/main/java/com/csed/knowtopia/service/CourseStorageService.java +++ b/backend/src/main/java/com/csed/knowtopia/service/CourseStorageService.java @@ -46,7 +46,7 @@ public FileResponseDTO storeFile(FileUploadDTO fileDTO) throws IOException { .build(); } - public VideoResponseDTO storeVideo(VideoUploadDTO videoDTO) throws IOException { + public Video storeVideo(VideoUploadDTO videoDTO) throws IOException { fileValidationService.validateVideo(videoDTO.getFile()); String originalFilename = videoDTO.getFile().getOriginalFilename(); @@ -60,13 +60,7 @@ public VideoResponseDTO storeVideo(VideoUploadDTO videoDTO) throws IOException { video.setUrl((String) uploadResult.get("url")); video.setDescription(videoDTO.getDescription()); - video = videoRepository.save(video); - return VideoResponseDTO.builder() - .id(video.getId()) - .filename(video.getFilename()) - .url(video.getUrl()) - .description(video.getDescription()) - .build(); + return videoRepository.save(video); } public FileResponseDTO retrieveFileMetadata(long id) { From b99c2d9212fff6c3d66f6d73c9b9fbc966d1dcae Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:30:54 +0200 Subject: [PATCH 15/19] Implement video update functionality in LessonService with proper authorization checks --- .../csed/knowtopia/service/LessonService.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/com/csed/knowtopia/service/LessonService.java b/backend/src/main/java/com/csed/knowtopia/service/LessonService.java index 5fa8515..e89b23d 100644 --- a/backend/src/main/java/com/csed/knowtopia/service/LessonService.java +++ b/backend/src/main/java/com/csed/knowtopia/service/LessonService.java @@ -1,29 +1,39 @@ package com.csed.knowtopia.service; import com.csed.knowtopia.dto.LessonDTO; +import com.csed.knowtopia.dto.request.VideoUploadDTO; import com.csed.knowtopia.entity.Course; import com.csed.knowtopia.entity.Lesson; import com.csed.knowtopia.entity.Module; import com.csed.knowtopia.entity.User; +import com.csed.knowtopia.entity.Video; +import com.csed.knowtopia.exception.ForbiddenException; import com.csed.knowtopia.exception.UnauthorizedException; import com.csed.knowtopia.repository.LessonRepository; import com.csed.knowtopia.repository.ModuleRepository; +import com.csed.knowtopia.repository.VideoRepository; import jakarta.persistence.EntityNotFoundException; import org.modelmapper.ModelMapper; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.io.IOException; @Service public class LessonService { private final ModelMapper modelMapper; private final ModuleRepository moduleRepository; private final LessonRepository lessonRepository; + private final CourseStorageService courseStorageService; + private final VideoRepository videoRepository; - public LessonService(LessonRepository lessonRepository, ModelMapper modelMapper, ModuleRepository moduleRepository) { + public LessonService(LessonRepository lessonRepository, ModelMapper modelMapper, + ModuleRepository moduleRepository, CourseStorageService courseStorageService, VideoRepository videoRepository) { this.modelMapper = modelMapper; this.moduleRepository = moduleRepository; this.lessonRepository = lessonRepository; + this.courseStorageService = courseStorageService; + this.videoRepository = videoRepository; } @@ -85,7 +95,40 @@ public LessonDTO deleteLesson(Long lessonId, User user) { User courseInstructor = course.getInstructor(); if (!user.getId().equals(courseInstructor.getId())) throw new UnauthorizedException("Unauthorized"); + // Delete old video if exists + if (lesson.getVideo() != null) { + courseStorageService.removeVideo(lesson.getVideo().getId()); + videoRepository.delete(lesson.getVideo()); + } lessonRepository.delete(lesson); return modelMapper.map(lesson, LessonDTO.class); } + + public LessonDTO updateLessonVideo(VideoUploadDTO videoDTO, User user) throws IOException { + Lesson lesson = lessonRepository.findById(videoDTO.getLessonId()) + .orElseThrow(() -> new EntityNotFoundException("Lesson not found")); + + Module module = lesson.getModule(); + if (module == null) throw new EntityNotFoundException("Module not found"); + + Course course = module.getCourse(); + if (course == null) throw new EntityNotFoundException("Course not found"); + + User instructor = course.getInstructor(); + if (!user.getId().equals(instructor.getId())) + throw new ForbiddenException("Unauthorized to update lesson video"); + + // Delete old video if exists + if (lesson.getVideo() != null) { + courseStorageService.removeVideo(lesson.getVideo().getId()); + } + + // Store new video + Video video = courseStorageService.storeVideo(videoDTO); + lesson.setVideo(video); + + LessonDTO lessonDTO = modelMapper.map(lessonRepository.save(lesson), LessonDTO.class); + lessonDTO.setVideoLink(video.getUrl()); + return lessonDTO; + } } \ No newline at end of file From d377b639941cdcf49fabcb632cb43e24a62ad1b6 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:32:08 +0200 Subject: [PATCH 16/19] Refactor video upload tests with lesson ID handling and user setup --- .../CourseStorageControllerTest.java | 156 ++++++++++++------ .../CourseStorageIntegrationTest.java | 108 ++++++++---- .../service/CourseStorageServiceTest.java | 3 +- 3 files changed, 181 insertions(+), 86 deletions(-) diff --git a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java index 28b6d21..857a0f7 100644 --- a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java +++ b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageControllerTest.java @@ -4,26 +4,44 @@ import com.csed.knowtopia.dto.request.VideoUploadDTO; import com.csed.knowtopia.dto.response.FileResponseDTO; import com.csed.knowtopia.dto.response.VideoResponseDTO; +import com.csed.knowtopia.entity.User; +import com.csed.knowtopia.entity.Video; +import com.csed.knowtopia.exception.ForbiddenException; import com.csed.knowtopia.exception.ResourceNotFoundException; +import com.csed.knowtopia.exception.UnauthorizedException; import com.csed.knowtopia.service.CourseStorageService; +import com.csed.knowtopia.service.CustomUserPrincipal; +import com.csed.knowtopia.service.LessonService; +import com.csed.knowtopia.dto.LessonDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import java.util.Collections; + import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @SpringBootTest @AutoConfigureMockMvc @@ -37,10 +55,24 @@ public class CourseStorageControllerTest { @MockBean private CourseStorageService storageService; + @MockBean + private LessonService lessonService; + private MockMultipartFile testFile; private MockMultipartFile testVideo; private FileResponseDTO fileResponseDTO; private VideoResponseDTO videoResponseDTO; + private Video returnedVideo; + private LessonDTO lessonDTO; + + private User testUser; + private CustomUserPrincipal userPrincipal; + + @Mock + private SecurityContext mockSecurityContext; + + private User studentUser; + private CustomUserPrincipal studentPrincipal; @BeforeEach void setUp() { @@ -73,12 +105,59 @@ void setUp() { .description("Test file") .build(); + returnedVideo = new Video(); + returnedVideo.setId(1L); + returnedVideo.setFilename("test.mp4"); + returnedVideo.setUrl("http://cloudinary.com/test.mp4"); + returnedVideo.setDescription("Test video"); + videoResponseDTO = VideoResponseDTO.builder() .id(1L) .filename("test.mp4") .url("http://cloudinary.com/test.mp4") .description("Test video") .build(); + + lessonDTO = new LessonDTO(); + lessonDTO.setId(1L); + lessonDTO.setTitle("Test Lesson"); + lessonDTO.setDescription("Test Description"); + + // Setup test user + testUser = new User(); + testUser.setId(1L); + testUser.setUsername("testInstructor"); + testUser.setEmail("instructor@test.com"); + testUser.setRole("USER"); + testUser.setPassword("password"); + + userPrincipal = new CustomUserPrincipal( + testUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + + // Setup student user (non-instructor) + studentUser = new User(); + studentUser.setId(2L); + studentUser.setUsername("testStudent"); + studentUser.setEmail("student@test.com"); + studentUser.setRole("USER"); + studentUser.setPassword("password"); + + studentPrincipal = new CustomUserPrincipal( + studentUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + + // Update test with authentication + when(mockSecurityContext.getAuthentication()).thenReturn( + new UsernamePasswordAuthenticationToken( + userPrincipal, + null, + userPrincipal.getAuthorities() + ) + ); + SecurityContextHolder.setContext(mockSecurityContext); } @Test @@ -130,75 +209,46 @@ void deleteFile_Success() throws Exception { @Test @WithMockUser(roles = "USER") - void uploadVideo_Success() throws Exception { - when(storageService.storeVideo(any(VideoUploadDTO.class))).thenReturn(videoResponseDTO); + void uploadLessonVideo_Success() throws Exception { + when(lessonService.updateLessonVideo(any(VideoUploadDTO.class), eq(testUser))) + .thenReturn(lessonDTO); - mockMvc.perform(multipart("/api/videos") - .file(testVideo)) + mockMvc.perform(multipart("/api/lessons/1/video") + .file(testVideo) + .with(user(userPrincipal))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.filename").value("test.mp4")) - .andExpect(jsonPath("$.url").value("http://cloudinary.com/test.mp4")); - verify(storageService).storeVideo(any(VideoUploadDTO.class)); + .andExpect(jsonPath("$.title").value("Test Lesson")); + + verify(lessonService).updateLessonVideo(any(VideoUploadDTO.class), eq(testUser)); } @Test @WithMockUser(roles = "USER") void uploadVideo_NoFile_BadRequest() throws Exception { - mockMvc.perform(multipart("/api/videos")) + mockMvc.perform(multipart("/api/lessons/1/video")) .andExpect(status().isBadRequest()); } @Test @WithMockUser(roles = "USER") - void getVideoMetadata_Success() throws Exception { - when(storageService.retrieveVideoMetadata(1L)).thenReturn(videoResponseDTO); - - mockMvc.perform(get("/api/videos/1")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(1)) - .andExpect(jsonPath("$.filename").value("test.mp4")); - - verify(storageService).retrieveVideoMetadata(1L); + void uploadLessonVideo_Unauthorized_NonInstructor() throws Exception { + when(lessonService.updateLessonVideo(any(VideoUploadDTO.class), eq(studentUser))) + .thenThrow(new ForbiddenException("Unauthorized to update lesson video")); + + mockMvc.perform(multipart("/api/lessons/1/video") + .file(testVideo) + .with(user(studentPrincipal))) + .andExpect(status().isForbidden()) + .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Unauthorized to update lesson video")); } @Test - @WithMockUser(roles = "USER") - void deleteVideo_Success() throws Exception { - when(storageService.removeVideo(1L)).thenReturn("Video deleted successfully"); - - mockMvc.perform(delete("/api/videos/1")) - .andExpect(status().isOk()) - .andExpect(content().string("Video deleted successfully")); - - verify(storageService).removeVideo(1L); - } - - @Test - @WithMockUser(roles = "USER") - void getFileMetadata_NotFound() throws Exception { - when(storageService.retrieveFileMetadata(999L)) - .thenThrow(new ResourceNotFoundException("File not found")); - - mockMvc.perform(get("/api/files/999")) - .andExpect(status().isNotFound()) - .andExpect(content().string("File not found")); - } - - @Test - @WithMockUser(roles = "USER") - void getVideoMetadata_NotFound() throws Exception { - when(storageService.retrieveVideoMetadata(999L)) - .thenThrow(new ResourceNotFoundException("Video not found")); - - mockMvc.perform(get("/api/videos/999")) - .andExpect(status().isNotFound()) - .andExpect(content().string("Video not found")); - } - - @Test - void unauthorizedAccess() throws Exception { - mockMvc.perform(get("/api/files/1")) + @WithAnonymousUser + void uploadLessonVideo_Unauthorized_NoUser() throws Exception { + mockMvc.perform(multipart("/api/lessons/1/video") + .file(testVideo)) .andExpect(status().isUnauthorized()); } + } diff --git a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java index 501ba95..0fc8cf8 100644 --- a/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java +++ b/backend/src/test/java/com/csed/knowtopia/controller/CourseStorageIntegrationTest.java @@ -1,14 +1,21 @@ package com.csed.knowtopia.controller; +import com.csed.knowtopia.dto.LessonDTO; import com.csed.knowtopia.dto.response.FileResponseDTO; import com.csed.knowtopia.dto.response.VideoResponseDTO; +import com.csed.knowtopia.entity.*; +import com.csed.knowtopia.entity.Module; +import com.csed.knowtopia.repository.*; + import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -46,6 +53,23 @@ public class CourseStorageIntegrationTest { private Long testFileId; private Long testVideoId; + private User testInstructor; + private Course testCourse; + private Module testModule; + private Lesson testLesson; + + @Autowired + private UserRepository userRepository; + + @Autowired + private CourseRepository courseRepository; + + @Autowired + private ModuleRepository moduleRepository; + + @Autowired + private LessonRepository lessonRepository; + @BeforeAll void setup() throws Exception { // Setup MockMvc with security configuration @@ -56,6 +80,40 @@ void setup() throws Exception { uploadPath = Paths.get(testUploadDir).toAbsolutePath().normalize(); Files.createDirectories(uploadPath); + + // Create test instructor + testInstructor = new User(); + testInstructor.setUsername("testInstructor"); + testInstructor.setEmail("instructor@test.com"); + testInstructor.setRole("USER"); + testInstructor.setPassword("password"); + testInstructor = userRepository.save(testInstructor); + + // Create test student + User testStudent = new User(); + testStudent.setUsername("testStudent"); + testStudent.setEmail("student@test.com"); + testStudent.setRole("USER"); + testStudent.setPassword("password"); + userRepository.save(testStudent); + + // Create test course + testCourse = new Course(); + testCourse.setTitle("Test Course"); + testCourse.setInstructor(testInstructor); + testCourse = courseRepository.save(testCourse); + + // Create test module + testModule = new Module(); + testModule.setTitle("Test Module"); + testModule.setCourse(testCourse); + testModule = moduleRepository.save(testModule); + + // Create test lesson + testLesson = new Lesson(); + testLesson.setTitle("Test Lesson"); + testLesson.setModule(testModule); + testLesson = lessonRepository.save(testLesson); } @AfterAll @@ -116,7 +174,7 @@ void testGetFileMetadata() throws Exception { } @Test - @WithMockUser(roles = "USER") + @WithUserDetails(value = "testInstructor", setupBefore = TestExecutionEvent.TEST_EXECUTION) @Order(3) void testVideoUpload() throws Exception { String filename = "test-video.mp4"; @@ -130,39 +188,37 @@ void testVideoUpload() throws Exception { inputStream ); - MvcResult result = mockMvc.perform(multipart("/api/videos") + MvcResult result = mockMvc.perform(multipart("/api/lessons/1/video") .file(multipartFile)) .andExpect(status().isOk()) .andReturn(); - VideoResponseDTO response = objectMapper.readValue( + LessonDTO response = objectMapper.readValue( result.getResponse().getContentAsString(), - VideoResponseDTO.class + LessonDTO.class ); - testVideoId = response.getId(); assertNotNull(response.getId()); - assertEquals("test-video.mp4", response.getFilename()); - assertNotNull(response.getUrl()); - assertTrue(response.getUrl().startsWith("http")); // Cloudinary URL + assertTrue(response.getVideoLink().startsWith("http")); // Cloudinary URL } @Test - @WithMockUser(roles = "USER") - @Order(4) - void testGetVideoMetadata() throws Exception { - MvcResult result = mockMvc.perform(get("/api/videos/" + testVideoId)) - .andExpect(status().isOk()) - .andReturn(); + @WithUserDetails(value = "testStudent", setupBefore = TestExecutionEvent.TEST_EXECUTION) + void testVideoUpload_Unauthorized() throws Exception { + String filename = "test-video.mp4"; + InputStream inputStream = getClass().getResourceAsStream("/test-videos/" + filename); + assertNotNull(inputStream, "Test video file not found"); - VideoResponseDTO response = objectMapper.readValue( - result.getResponse().getContentAsString(), - VideoResponseDTO.class + MockMultipartFile multipartFile = new MockMultipartFile( + "file", + filename, + "video/mp4", + inputStream ); - assertEquals(testVideoId, response.getId()); - assertEquals("test-video.mp4", response.getFilename()); - assertNotNull(response.getUrl()); + mockMvc.perform(multipart("/api/lessons/1/video") + .file(multipartFile)) + .andExpect(status().isForbidden()); } @Test @@ -189,18 +245,6 @@ void testDeleteFile() throws Exception { .andExpect(status().isNotFound()); } - @Test - @WithMockUser(roles = "USER") - @Order(6) - void testDeleteVideo() throws Exception { - mockMvc.perform(delete("/api/videos/" + testVideoId)) - .andExpect(status().isOk()); - - // Verify metadata is deleted - mockMvc.perform(get("/api/videos/" + testVideoId)) - .andExpect(status().isNotFound()); - } - @Test @WithMockUser(roles = "USER") void testUploadInvalidFile() throws Exception { diff --git a/backend/src/test/java/com/csed/knowtopia/service/CourseStorageServiceTest.java b/backend/src/test/java/com/csed/knowtopia/service/CourseStorageServiceTest.java index 479547e..1081148 100644 --- a/backend/src/test/java/com/csed/knowtopia/service/CourseStorageServiceTest.java +++ b/backend/src/test/java/com/csed/knowtopia/service/CourseStorageServiceTest.java @@ -107,6 +107,7 @@ void storeVideo_Success() throws IOException { VideoUploadDTO videoDTO = new VideoUploadDTO(); videoDTO.setFile(mockFile); videoDTO.setDescription("Test video"); + videoDTO.setLessonId(1L); Map cloudinaryResponse = new HashMap<>(); cloudinaryResponse.put("url", "http://cloudinary.com/test.mp4"); @@ -115,7 +116,7 @@ void storeVideo_Success() throws IOException { when(videoRepository.save(any())).thenReturn(testVideo); // Act - VideoResponseDTO result = courseStorageService.storeVideo(videoDTO); + Video result = courseStorageService.storeVideo(videoDTO); // Assert assertNotNull(result); From de77d6aa9ec24c38d32edcde3ed75063d69ded65 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:32:16 +0200 Subject: [PATCH 17/19] Add lesson ID validation to VideoUploadDTO tests --- .../dto/request/VideoUploadDTOTest.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/com/csed/knowtopia/dto/request/VideoUploadDTOTest.java b/backend/src/test/java/com/csed/knowtopia/dto/request/VideoUploadDTOTest.java index 993aed7..25a09df 100644 --- a/backend/src/test/java/com/csed/knowtopia/dto/request/VideoUploadDTOTest.java +++ b/backend/src/test/java/com/csed/knowtopia/dto/request/VideoUploadDTOTest.java @@ -23,7 +23,9 @@ void testValidVideoUpload() { "file", "test.mp4", "video/mp4", "test data".getBytes() ); VideoUploadDTO dto = new VideoUploadDTO(); + dto.setLessonId(1L); dto.setFile(mockFile); + var violations = validator.validate(dto); assertTrue(violations.isEmpty()); @@ -32,18 +34,34 @@ void testValidVideoUpload() { @Test void testNullFile() { VideoUploadDTO dto = new VideoUploadDTO(); + dto.setLessonId(1L); var violations = validator.validate(dto); assertFalse(violations.isEmpty()); assertEquals(1, violations.size()); assertEquals("File is required", violations.iterator().next().getMessage()); } + @Test + void testNullLessonId() { + MockMultipartFile mockFile = new MockMultipartFile( + "file", "test.mp4", "video/mp4", "test data".getBytes() + ); + VideoUploadDTO dto = new VideoUploadDTO(); + dto.setFile(mockFile); + + var violations = validator.validate(dto); + assertFalse(violations.isEmpty()); + assertEquals(1, violations.size()); + assertEquals("LessonId is required", violations.iterator().next().getMessage()); + } + @Test void testEmptyDescription() { MockMultipartFile mockFile = new MockMultipartFile( "file", "test.mp4", "video/mp4", "test data".getBytes() ); VideoUploadDTO dto = new VideoUploadDTO(); + dto.setLessonId(1L); dto.setFile(mockFile); dto.setDescription(""); @@ -57,6 +75,7 @@ void testNullDescription() { "file", "test.mp4", "video/mp4", "test data".getBytes() ); VideoUploadDTO dto = new VideoUploadDTO(); + dto.setLessonId(1L); dto.setFile(mockFile); dto.setDescription(null); @@ -71,9 +90,10 @@ void testConstructor() { ); VideoUploadDTO dto = new VideoUploadDTO(); dto.setFile(mockFile); + dto.setLessonId(5L); dto.setDescription("test"); - VideoUploadDTO dto2 = new VideoUploadDTO(mockFile, "test"); + VideoUploadDTO dto2 = new VideoUploadDTO(mockFile, 5L, "test"); assertEquals(dto, dto2); } } From 048d9d25839bb3e901f6aaa17a7d0c06e978a3ae Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:32:30 +0200 Subject: [PATCH 18/19] Add unit tests for updateLessonVideo method in LessonService --- .../knowtopia/service/LessonServiceTest.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 backend/src/test/java/com/csed/knowtopia/service/LessonServiceTest.java diff --git a/backend/src/test/java/com/csed/knowtopia/service/LessonServiceTest.java b/backend/src/test/java/com/csed/knowtopia/service/LessonServiceTest.java new file mode 100644 index 0000000..519d9cd --- /dev/null +++ b/backend/src/test/java/com/csed/knowtopia/service/LessonServiceTest.java @@ -0,0 +1,112 @@ +package com.csed.knowtopia.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Optional; + +import com.csed.knowtopia.entity.Module; +import com.csed.knowtopia.exception.ForbiddenException; +import com.csed.knowtopia.repository.CourseRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.modelmapper.ModelMapper; +import org.springframework.mock.web.MockMultipartFile; + +import com.csed.knowtopia.dto.LessonDTO; +import com.csed.knowtopia.dto.request.VideoUploadDTO; +import com.csed.knowtopia.exception.UnauthorizedException; +import com.csed.knowtopia.repository.LessonRepository; +import com.csed.knowtopia.repository.ModuleRepository; +import com.csed.knowtopia.entity.*; + +@ExtendWith(MockitoExtension.class) +class LessonServiceTest { + @Mock + private CourseStorageService courseStorageService; + + @Mock + private LessonRepository lessonRepository; + + @Mock + private ModuleRepository moduleRepository; + + @Mock + private ModelMapper modelMapper; + + @InjectMocks + private LessonService lessonService; + + private User instructor; + private Course course; + private Module module; + private Lesson lesson; + private Video video; + + @BeforeEach + void setUp() { + instructor = new User(); + instructor.setId(1L); + instructor.setRole("ROLE_INSTRUCTOR"); + + course = new Course(); + course.setId(1L); + course.setInstructor(instructor); + + module = new Module(); + module.setId(1L); + module.setCourse(course); + + lesson = new Lesson(); + lesson.setId(1L); + lesson.setModule(module); + + video = new Video(); + video.setId(1L); + video.setUrl("http://example.com/video.mp4"); + } + + @Test + void updateLessonVideo_Success() throws IOException { + // Arrange + VideoUploadDTO videoDTO = new VideoUploadDTO(); + videoDTO.setLessonId(1L); + videoDTO.setFile(new MockMultipartFile("video", new byte[0])); + + when(lessonRepository.findById(1L)).thenReturn(Optional.of(lesson)); + when(courseStorageService.storeVideo(any())).thenReturn(video); + when(lessonRepository.save(any())).thenReturn(lesson); + when(modelMapper.map(any(Lesson.class), eq(LessonDTO.class))).thenReturn(new LessonDTO()); + + // Act + LessonDTO result = lessonService.updateLessonVideo(videoDTO, instructor); + + // Assert + assertNotNull(result); + verify(courseStorageService).storeVideo(videoDTO); + verify(lessonRepository).save(any()); + verify(modelMapper).map(any(Lesson.class), eq(LessonDTO.class)); + } + + @Test + void updateLessonVideo_UnauthorizedUser() { + // Arrange + User unauthorizedUser = new User(); + unauthorizedUser.setId(2L); + VideoUploadDTO videoDTO = new VideoUploadDTO(); + videoDTO.setLessonId(1L); + + when(lessonRepository.findById(1L)).thenReturn(Optional.of(lesson)); + + // Act & Assert + assertThrows(ForbiddenException.class, () -> + lessonService.updateLessonVideo(videoDTO, unauthorizedUser) + ); + } +} From 1878df54d78e5f752bcb58085c2205de55da2b07 Mon Sep 17 00:00:00 2001 From: Ahmed Ayman Date: Wed, 1 Jan 2025 07:42:30 +0200 Subject: [PATCH 19/19] Update package-lock.json to include 'modal' and 'm3u8-parser' dependencies --- frontend/package-lock.json | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aea8dda..ed2a6b8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "core-js": "^3.8.3", "gapi-script": "^1.2.0", "lucide-vue-next": "^0.468.0", + "modal": "^1.2.0", "vue": "^3.2.13", "vue-router": "^4.0.3", "vuex": "^4.0.2" @@ -8464,6 +8465,24 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-vue-next": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.468.0.tgz", + "integrity": "sha512-quV/6T8YB1XK0VOEnebg3Byd8Rsan5/m95cvjnuHV4vcS3qEnLAybkrSh0hk3ppavx+V7R1PjNW+mGDvcBdz4A==", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/m3u8-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz", + "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^4.1.1", + "global": "^4.4.0" + } + }, "node_modules/magic-string": { "version": "0.30.14", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.14.tgz", @@ -8773,6 +8792,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/modal": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/modal/-/modal-1.2.0.tgz", + "integrity": "sha512-Xoz/k7hnrpS0BT3Wh0hyOJ6NLQ7+jh7uPN5hVVoaxVWtZ60+RfGa8CfBjtR+JOvsmLqHKrXcWDsBbO2qNUMPWA==", + "deprecated": "This package is deprecated and from before Modal.com ownership." + }, "node_modules/module-alias": { "version": "2.2.3", "resolved": "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz", @@ -9503,6 +9528,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz",