From d22b2c6cf0d06189a225393fd7c317f80c8296bf Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:00:24 +0100 Subject: [PATCH 01/22] working post movie and series method (still need some fixes) --- .idea/.gitignore | 8 ++ .idea/compiler.xml | 18 +++ .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/dataSources.xml | 17 +++ .idea/encodings.xml | 6 + .idea/jarRepositories.xml | 20 +++ .idea/misc.xml | 12 ++ .idea/vcs.xml | 7 ++ .../streamflix/config/SecurityConfig.java | 2 + .../controller/MediaController.java | 15 +++ .../example/streamflix/entity/Episode.java | 2 +- .../streamflix/model/MediaDetailsDto.java | 27 ++++ .../repository/AgeRatingRepository.java | 6 + .../repository/EpisodeRepository.java | 5 + .../repository/MediaRepository.java | 3 + .../repository/ProfileRepository.java | 3 + .../repository/SeriesRepository.java | 3 + .../streamflix/service/AgeRatingService.java | 28 +++++ .../streamflix/service/MediaService.java | 115 +++++++++++++++++- .../streamflix/StreamflixApplication.class | Bin 0 -> 994 bytes .../config/JwtAuthenticationFilter.class | Bin 0 -> 3517 bytes .../streamflix/config/ScheduledTasks.class | Bin 0 -> 1829 bytes .../streamflix/config/SecurityConfig.class | Bin 0 -> 6136 bytes .../streamflix/config/WebClientConfig.class | Bin 0 -> 917 bytes .../controller/AnalyticsController.class | Bin 0 -> 6881 bytes .../controller/AuthController.class | Bin 0 -> 12055 bytes .../controller/MediaController.class | Bin 0 -> 5553 bytes .../controller/ProfileController.class | Bin 0 -> 9034 bytes .../controller/ReferralController.class | Bin 0 -> 5433 bytes .../controller/SubscriptionController.class | Bin 0 -> 6910 bytes .../ViewingProgressController.class | Bin 0 -> 7209 bytes .../controller/WatchListController.class | Bin 0 -> 5231 bytes .../example/streamflix/entity/Account.class | Bin 0 -> 3279 bytes .../example/streamflix/entity/AgeRating.class | Bin 0 -> 1893 bytes .../streamflix/entity/ContentWarning.class | Bin 0 -> 1597 bytes .../example/streamflix/entity/Employee.class | Bin 0 -> 2067 bytes .../example/streamflix/entity/Episode.class | Bin 0 -> 3291 bytes .../streamflix/entity/InvitationStatus.class | Bin 0 -> 1337 bytes .../com/example/streamflix/entity/Media.class | Bin 0 -> 3878 bytes .../com/example/streamflix/entity/Movie.class | Bin 0 -> 2560 bytes .../streamflix/entity/Preference.class | Bin 0 -> 2792 bytes .../example/streamflix/entity/Profile.class | Bin 0 -> 4750 bytes .../streamflix/entity/QualityType.class | Bin 0 -> 1122 bytes .../example/streamflix/entity/Referral.class | Bin 0 -> 3264 bytes .../com/example/streamflix/entity/Role.class | Bin 0 -> 1275 bytes .../example/streamflix/entity/Season.class | Bin 0 -> 2434 bytes .../example/streamflix/entity/Series.class | Bin 0 -> 2198 bytes .../streamflix/entity/Subscription.class | Bin 0 -> 2840 bytes .../streamflix/entity/SubscriptionTier.class | Bin 0 -> 1879 bytes .../streamflix/entity/VerificationToken.class | Bin 0 -> 3199 bytes .../streamflix/entity/ViewingProgress.class | Bin 0 -> 4204 bytes .../example/streamflix/entity/WatchList.class | Bin 0 -> 2491 bytes .../streamflix/enums/MediaQuality.class | Bin 0 -> 1243 bytes .../streamflix/enums/PreferenceType.class | Bin 0 -> 1276 bytes .../example/streamflix/enums/TokenType.class | Bin 0 -> 1200 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 6839 bytes .../exception/NotFoundException.class | Bin 0 -> 635 bytes .../model/AcceptInvitationRequest.class | Bin 0 -> 826 bytes .../model/AddToWatchListRequest.class | Bin 0 -> 992 bytes .../model/CreatePreferenceRequest.class | Bin 0 -> 1517 bytes .../model/CreateProfileRequest.class | Bin 0 -> 2050 bytes .../streamflix/model/CreateTrialRequest.class | Bin 0 -> 980 bytes .../model/ErrorResponse$ValidationError.class | Bin 0 -> 1054 bytes .../streamflix/model/ErrorResponse.class | Bin 0 -> 2857 bytes .../model/ForgotPasswordRequest.class | Bin 0 -> 1173 bytes .../streamflix/model/InvitationResponse.class | Bin 0 -> 1095 bytes .../streamflix/model/LoginRequest.class | Bin 0 -> 1318 bytes .../streamflix/model/MediaDetailsDto.class | Bin 0 -> 3147 bytes .../streamflix/model/RegisterRequest.class | Bin 0 -> 1334 bytes .../model/ResetPasswordRequest.class | Bin 0 -> 1497 bytes .../model/StartWatchingRequest.class | Bin 0 -> 1171 bytes .../model/StreamValidationResult.class | Bin 0 -> 1910 bytes .../streamflix/model/TmdbMovieResponse.class | Bin 0 -> 2181 bytes .../model/UpdateProfileRequest.class | Bin 0 -> 1486 bytes .../model/UpdateProgressRequest.class | Bin 0 -> 1076 bytes .../model/UpgradeSubscriptionRequest.class | Bin 0 -> 770 bytes .../repository/AccountRepository.class | Bin 0 -> 558 bytes .../repository/AgeRatingRepository.class | Bin 0 -> 711 bytes .../repository/EmployeeRepository.class | Bin 0 -> 648 bytes .../repository/EpisodeRepository.class | Bin 0 -> 834 bytes .../InvitationStatusRepository.class | Bin 0 -> 682 bytes .../repository/MediaRepository.class | Bin 0 -> 951 bytes .../repository/MovieRepository.class | Bin 0 -> 357 bytes .../repository/PreferenceRepository.class | Bin 0 -> 566 bytes .../repository/ProfileRepository.class | Bin 0 -> 640 bytes .../repository/QualityTypeRepository.class | Bin 0 -> 461 bytes .../repository/ReferralRepository.class | Bin 0 -> 1241 bytes .../repository/RoleRepository.class | Bin 0 -> 630 bytes .../repository/SeasonRepository.class | Bin 0 -> 567 bytes .../repository/SeriesRepository.class | Bin 0 -> 446 bytes .../repository/SubscriptionRepository.class | Bin 0 -> 683 bytes .../SubscriptionTierRepository.class | Bin 0 -> 592 bytes .../VerificationTokenRepository.class | Bin 0 -> 525 bytes .../ViewingProgressRepository.class | Bin 0 -> 911 bytes .../repository/WatchListRepository.class | Bin 0 -> 899 bytes .../security/CustomUserDetails.class | Bin 0 -> 1631 bytes .../service/AccountUserDetailsService.class | Bin 0 -> 2678 bytes .../streamflix/service/AgeRatingService.class | Bin 0 -> 2552 bytes .../service/ContentAccessService.class | Bin 0 -> 3330 bytes .../streamflix/service/JwtService.class | Bin 0 -> 4267 bytes .../streamflix/service/MediaService.class | Bin 0 -> 11945 bytes .../streamflix/service/ProfileService.class | Bin 0 -> 8622 bytes .../streamflix/service/ReferralService.class | Bin 0 -> 7331 bytes .../service/RegistrationService.class | Bin 0 -> 3405 bytes .../streamflix/service/StreamingService.class | Bin 0 -> 5228 bytes .../service/SubscriptionService.class | Bin 0 -> 5597 bytes .../streamflix/service/TmdbService.class | Bin 0 -> 2884 bytes .../service/ViewingProgressService.class | Bin 0 -> 8407 bytes .../streamflix/service/WatchListService.class | Bin 0 -> 5284 bytes .../streamflix/util/SecurityUtils.class | Bin 0 -> 2963 bytes 110 files changed, 296 insertions(+), 7 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/dataSources.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 target/classes/com/example/streamflix/StreamflixApplication.class create mode 100644 target/classes/com/example/streamflix/config/JwtAuthenticationFilter.class create mode 100644 target/classes/com/example/streamflix/config/ScheduledTasks.class create mode 100644 target/classes/com/example/streamflix/config/SecurityConfig.class create mode 100644 target/classes/com/example/streamflix/config/WebClientConfig.class create mode 100644 target/classes/com/example/streamflix/controller/AnalyticsController.class create mode 100644 target/classes/com/example/streamflix/controller/AuthController.class create mode 100644 target/classes/com/example/streamflix/controller/MediaController.class create mode 100644 target/classes/com/example/streamflix/controller/ProfileController.class create mode 100644 target/classes/com/example/streamflix/controller/ReferralController.class create mode 100644 target/classes/com/example/streamflix/controller/SubscriptionController.class create mode 100644 target/classes/com/example/streamflix/controller/ViewingProgressController.class create mode 100644 target/classes/com/example/streamflix/controller/WatchListController.class create mode 100644 target/classes/com/example/streamflix/entity/Account.class create mode 100644 target/classes/com/example/streamflix/entity/AgeRating.class create mode 100644 target/classes/com/example/streamflix/entity/ContentWarning.class create mode 100644 target/classes/com/example/streamflix/entity/Employee.class create mode 100644 target/classes/com/example/streamflix/entity/Episode.class create mode 100644 target/classes/com/example/streamflix/entity/InvitationStatus.class create mode 100644 target/classes/com/example/streamflix/entity/Media.class create mode 100644 target/classes/com/example/streamflix/entity/Movie.class create mode 100644 target/classes/com/example/streamflix/entity/Preference.class create mode 100644 target/classes/com/example/streamflix/entity/Profile.class create mode 100644 target/classes/com/example/streamflix/entity/QualityType.class create mode 100644 target/classes/com/example/streamflix/entity/Referral.class create mode 100644 target/classes/com/example/streamflix/entity/Role.class create mode 100644 target/classes/com/example/streamflix/entity/Season.class create mode 100644 target/classes/com/example/streamflix/entity/Series.class create mode 100644 target/classes/com/example/streamflix/entity/Subscription.class create mode 100644 target/classes/com/example/streamflix/entity/SubscriptionTier.class create mode 100644 target/classes/com/example/streamflix/entity/VerificationToken.class create mode 100644 target/classes/com/example/streamflix/entity/ViewingProgress.class create mode 100644 target/classes/com/example/streamflix/entity/WatchList.class create mode 100644 target/classes/com/example/streamflix/enums/MediaQuality.class create mode 100644 target/classes/com/example/streamflix/enums/PreferenceType.class create mode 100644 target/classes/com/example/streamflix/enums/TokenType.class create mode 100644 target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class create mode 100644 target/classes/com/example/streamflix/exception/NotFoundException.class create mode 100644 target/classes/com/example/streamflix/model/AcceptInvitationRequest.class create mode 100644 target/classes/com/example/streamflix/model/AddToWatchListRequest.class create mode 100644 target/classes/com/example/streamflix/model/CreatePreferenceRequest.class create mode 100644 target/classes/com/example/streamflix/model/CreateProfileRequest.class create mode 100644 target/classes/com/example/streamflix/model/CreateTrialRequest.class create mode 100644 target/classes/com/example/streamflix/model/ErrorResponse$ValidationError.class create mode 100644 target/classes/com/example/streamflix/model/ErrorResponse.class create mode 100644 target/classes/com/example/streamflix/model/ForgotPasswordRequest.class create mode 100644 target/classes/com/example/streamflix/model/InvitationResponse.class create mode 100644 target/classes/com/example/streamflix/model/LoginRequest.class create mode 100644 target/classes/com/example/streamflix/model/MediaDetailsDto.class create mode 100644 target/classes/com/example/streamflix/model/RegisterRequest.class create mode 100644 target/classes/com/example/streamflix/model/ResetPasswordRequest.class create mode 100644 target/classes/com/example/streamflix/model/StartWatchingRequest.class create mode 100644 target/classes/com/example/streamflix/model/StreamValidationResult.class create mode 100644 target/classes/com/example/streamflix/model/TmdbMovieResponse.class create mode 100644 target/classes/com/example/streamflix/model/UpdateProfileRequest.class create mode 100644 target/classes/com/example/streamflix/model/UpdateProgressRequest.class create mode 100644 target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class create mode 100644 target/classes/com/example/streamflix/repository/AccountRepository.class create mode 100644 target/classes/com/example/streamflix/repository/AgeRatingRepository.class create mode 100644 target/classes/com/example/streamflix/repository/EmployeeRepository.class create mode 100644 target/classes/com/example/streamflix/repository/EpisodeRepository.class create mode 100644 target/classes/com/example/streamflix/repository/InvitationStatusRepository.class create mode 100644 target/classes/com/example/streamflix/repository/MediaRepository.class create mode 100644 target/classes/com/example/streamflix/repository/MovieRepository.class create mode 100644 target/classes/com/example/streamflix/repository/PreferenceRepository.class create mode 100644 target/classes/com/example/streamflix/repository/ProfileRepository.class create mode 100644 target/classes/com/example/streamflix/repository/QualityTypeRepository.class create mode 100644 target/classes/com/example/streamflix/repository/ReferralRepository.class create mode 100644 target/classes/com/example/streamflix/repository/RoleRepository.class create mode 100644 target/classes/com/example/streamflix/repository/SeasonRepository.class create mode 100644 target/classes/com/example/streamflix/repository/SeriesRepository.class create mode 100644 target/classes/com/example/streamflix/repository/SubscriptionRepository.class create mode 100644 target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class create mode 100644 target/classes/com/example/streamflix/repository/VerificationTokenRepository.class create mode 100644 target/classes/com/example/streamflix/repository/ViewingProgressRepository.class create mode 100644 target/classes/com/example/streamflix/repository/WatchListRepository.class create mode 100644 target/classes/com/example/streamflix/security/CustomUserDetails.class create mode 100644 target/classes/com/example/streamflix/service/AccountUserDetailsService.class create mode 100644 target/classes/com/example/streamflix/service/AgeRatingService.class create mode 100644 target/classes/com/example/streamflix/service/ContentAccessService.class create mode 100644 target/classes/com/example/streamflix/service/JwtService.class create mode 100644 target/classes/com/example/streamflix/service/MediaService.class create mode 100644 target/classes/com/example/streamflix/service/ProfileService.class create mode 100644 target/classes/com/example/streamflix/service/ReferralService.class create mode 100644 target/classes/com/example/streamflix/service/RegistrationService.class create mode 100644 target/classes/com/example/streamflix/service/StreamingService.class create mode 100644 target/classes/com/example/streamflix/service/SubscriptionService.class create mode 100644 target/classes/com/example/streamflix/service/TmdbService.class create mode 100644 target/classes/com/example/streamflix/service/ViewingProgressService.class create mode 100644 target/classes/com/example/streamflix/service/WatchListService.class create mode 100644 target/classes/com/example/streamflix/util/SecurityUtils.class diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..76adaed --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..02dae28 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,17 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/streamflix + + + + + + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..86993b2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java index 7cbac83..065bc3b 100644 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.example.streamflix.config; +import io.netty.handler.codec.http.HttpMethod; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -30,6 +31,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/api/v1/analytics/admin/**").authenticated() .requestMatchers("/api/v1/analytics/**").permitAll() diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java index 46486f8..59babe7 100644 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -1,5 +1,6 @@ package com.example.streamflix.controller; +import com.example.streamflix.entity.Media; import com.example.streamflix.model.MediaDetailsDto; import com.example.streamflix.model.StreamValidationResult; import com.example.streamflix.enums.MediaQuality; @@ -12,6 +13,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -70,4 +72,17 @@ public ResponseEntity validateStream( streamingService.validateStream(profileId, mediaId, quality) ); } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java index 521d66a..4ec6fa6 100644 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -7,7 +7,7 @@ @Entity @Table(name = "episode") -public class Episode { +public class Episode extends Media { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java index 21fbec3..4496e93 100644 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -8,6 +8,9 @@ public class MediaDetailsDto { private LocalDate releaseDate; private String ageRating; private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; // TMDB enriched data private String posterUrl; @@ -87,4 +90,28 @@ public Double getExternalRating() { public void setExternalRating(Double externalRating) { this.externalRating = externalRating; } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java index 63229a3..37ceced 100644 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -2,6 +2,12 @@ import com.example.streamflix.entity.AgeRating; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java index 85293e8..c6f2021 100644 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -2,8 +2,13 @@ import com.example.streamflix.entity.Episode; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.List; +import java.util.Optional; +@Repository public interface EpisodeRepository extends JpaRepository { List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); } diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java index b3747bd..8deb3ed 100644 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -2,9 +2,12 @@ import com.example.streamflix.entity.Media; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import java.util.Optional; +@Repository public interface MediaRepository extends JpaRepository { Optional findById(Long id); + boolean existsByTitle(String title); } diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java index 581d4ff..ccb9a1a 100644 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -2,8 +2,11 @@ import com.example.streamflix.entity.Profile; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.List; +@Repository public interface ProfileRepository extends JpaRepository { List findByAccountId(Long accountId); } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java index 117844e..e15ec1b 100644 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -2,7 +2,10 @@ import com.example.streamflix.entity.Series; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.Optional; +@Repository public interface SeriesRepository extends JpaRepository { } diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java index 3f903e4..909f633 100644 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -1,11 +1,9 @@ package com.example.streamflix.service; +import com.example.streamflix.entity.*; import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.entity.Media; -import com.example.streamflix.entity.Profile; import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.*; import com.example.streamflix.util.SecurityUtils; import org.springframework.stereotype.Service; @@ -13,23 +11,37 @@ import java.util.stream.Collectors; @Service -public class MediaService { +public class MediaService +{ private final MediaRepository mediaRepository; private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; private final TmdbService tmdbService; private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; private final SecurityUtils securityUtils; public MediaService(MediaRepository mediaRepository, ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, TmdbService tmdbService, ContentAccessService contentAccessService, - SecurityUtils securityUtils) { + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { this.mediaRepository = mediaRepository; this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; this.tmdbService = tmdbService; this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; this.securityUtils = securityUtils; } @@ -135,4 +147,95 @@ private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { System.err.println("Failed to fetch TMDB data: " + e.getMessage()); } } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + } \ No newline at end of file diff --git a/target/classes/com/example/streamflix/StreamflixApplication.class b/target/classes/com/example/streamflix/StreamflixApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..bc3bf41ff7f8c3e5661cc4e1a1ab40815d65ad47 GIT binary patch literal 994 zcmb7D+invv5It_2Y@3F(v`}cdg{zVX#Z*YV1XYwkrAmQT1ce8loQ<<}*|j6DH_dPH z1mb}Y;G+=ZO-1q$2@xqX>$#lqoEiW6{o^Nqw|G**5|#t3luoQPx;OP)F_MKX-lgm?db9*#z_3WZZfmr%lbfN~jE zP+@2o8}W2xm5zp1B=W-8FT8Jzci9HH*)A#iI&1sq4dx&)c=v{qxdliN+~$ye4~|VqerxJRf1&*#>57} z(#GsJpE0ZtWExsE@}+29DW*CwVU|ce?YTNMwmJtQX6uUk2{3^-vGyq%Jrqdd(;W~l%r)$56#v~MWRtKInp*Alp|on75Zdkgk@O*@RH(E-p{dj=ZZ;RPu-Q#_Z+O(V zU-hH?p#Mc@EEPNB$2#NRq>krq0un-isSeD|?%i|G<9E;bo%6@vzy1bb0N*4KLRdjW zMH`|FXIyVa_bZ;|%uIVmnJ>8BoW8&d`m|*W?&%|r$w#?&mp`geGlFFbCPr)_IdH(h7in$d49h`y>QaYtCDAuQKf z)tQ)G5aZmNw@l8E8E&D)uSrNXb>W<}ze`0Yx)_pG61c&IVcGtgvg~%s`b^WUI^tf_ z*<1`sh7Rel>sgNjf7|gI_9}Q?#T(eiaBS9?GdyAFba>w8LN5tX(FbXf_GzOI9YcJE zi$QJ_xyNucH9Tv~8@g>cGy1rY@aNLaTQTfsP_A>s<6d_H2XRQjn=0NyH$&G(1q=ya zkSqRuOOzP)Zf4>^JC5LO1@EYM7e^TmZ1lc%8~r82au{MoclCB|NNtUQHpZ80{oSv) zj?Z)Hyo?~FB8?tG!><-7J1B7J>d+>^C(w)I3QnldCD7>|X9R=F7lmh-VxUZkkiaRN zR&YkeS)60Iwpqse+^l+*SfVWTxJ*?+8uan{Zh!54(6xihOVCBuw-~OZ(!;N;G1ra@ zxTxTgiY#&rmtI*D!*24bv9cJ>?#Q{uQ@I4L;HrXaD&9jM!^LfIZ&^H4jkaqPWmR2Y zlB+}f!Z5N~UhV)wS=q%}W$WuImE7$`zls|epnJFc33rY=dBe7f45OQv`VW@U4-~vl z0c?3mMgzG8p31mK4c{kjEjD5uINOdP+*0s?iecQ|Q4X|#f?-!Z2Z*eM#eIf-lmG!w zRE1^h{jP0uQ<5&Bb`vAA2aL)TUdKEmg;^4j)x1hi4Lck4r1J{yGThk`6WLET;BucA z8lhMvZMW)~Jb^KU<#JC&9{1^D3suW5)@2x~&1}?yc8OL0FV_OhCRIGZN0Mr}SaYX= zYU^;N^Rl)-R`Cg@s5nWyF}9% zwj6$^S}t&J!YJ6ZlN@$U!_FI?CEx3t5mB-z=PtCex%C7<^w)KpQ==`88gde$>4TJY zt+t>cqGVzao5A1tBE)zJYSB9~zFHTDSJKyYt0x0%&S=4J8 zmD}1vkQyymfOIKdZCsNZQr(zzjfx*zTgJX5z$n)u7Dy+{8hnkMAN0JIh zqO&>%B}sUdX6jQA&6G3=MayU%WYKTVh5R!6Url+JFU4tEp8lTb^gWe|PoKC-;J;=XqpJP0;jEQA@NNTj$3Lb{AW|ABWL&knJaQp}oI7-nUBT!Q`*JkLi7w2(;dYewo z;}oWFnnF7VkEr-tT*UXt;Rjp}aC%6AdW4&WS-O%j`Z5O_iVS`YyC*2afyC|!TvP}< z`Sgg+g~|4J@F57k3~8v+QybZy!aO}iu|SeZg#U!5;9%@8SoA~xSfu?Z=~=>O!98?@ Y$plC_NUh9@FM{<;d_})eGW|915AY>7LI3~& literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/config/ScheduledTasks.class b/target/classes/com/example/streamflix/config/ScheduledTasks.class new file mode 100644 index 0000000000000000000000000000000000000000..887512dd3142a864888076fe3866fff28a59aa2d GIT binary patch literal 1829 zcmb7FZF3V<6n<`+cJoRhX~9y#O$(S!v0D+4HWjTg6)-7iYy8OEZf??THoKX-H#Lmo zU-7G-@x6ZF2l%5L&)uYLq?uuu%;dhEbDnda^RoZ^`}ZpVRXj@}hA9K_45l%|aPg2I zaogj5+kU!#C>+Hwv+4S-ddM(Uv>HhykTj6WAdL*eZ71m3V!*pSPuQW7f_GbY>Jy{L1aw}oU_t{w6Ymx|jxAwxG*!gqvS9TzHV#0weB z;v%`?1?@;NR|{m@4!zdhL%SB;l@rJ@tnVr^=lX3kJb^YHPjJ8AGlP~X20d4brgNtBAO_Wz|55A%DhKb>N7eW<7!1 z48^LHfi#TbzT*c#IShmiW zEW<4=_ofc88dQ9hph@x0)?8og^t<~)?(uz(Zm1I+?lrh{^?7s|R|hV&+iLA?+Otem zUl-~iXg=eTcPZbJY|M??r#ft%y{K6j7(vgE(RG?wvXZ4HbUJn38+o(vO`kftD;jR- zl8;-yAE>DH$o#z_D?MESEsgqpAUpP`%;?VMr_yKBgfQZc419+4nz?4)F-!V4K2DV6 zen^R-O0)OQ_S_@Y#HQnoCR%DY=u1azyPC=T|EJZeu3Cos6CI@aro=#vdsm4@USCF$ z5zOX9#mSLXeVx1!z8)wj(A103gpT2=9(}aS>4{706sW3Erk71RZPTjdQl&q^N-Mv^ zcuA)iKB4~$B&+q}$RkNT@sLiJhtl{IpV5i*BC8Brw4WwEX<_&rGbEgLu>HX;GXXI=8w6gLCK3F-%hjF|lbD-H%qz;~nyjr5j zfNO+&9rH8{7h%%-GD#FjW(f~)o}I;xF9$6JSwX(WN6s3NxkXWM8RC!YrtY3#=pGoH=|uv)3X&>r zgUaw=!76KFm6t1q&>YtmygYB{t6IS_=k=177lo>=yK7nRli{w#6?d%aE(p`r3*6N$ z^OSD5!e)3R*Iv?@){LWRvSO!-T}UzX7OIYGmG$QZ!?A(EoMo3Zr()}7Y2M~#v0~Xv zniFUgYM2dK?9n9B*rVWf6?foHhSQy}bJMh3&k$`z%xQB~-6#s%(N4Q=B{ZHPR&eZj zh8G96s#0e*bfN?~ieaZTai5C)(!Sk}a2$%jBsY0Ulqn<%4lqo&HCr0V8v>|ZaD;7= zd(U&nAr}{0#aFJ|r{W&m%dm?Vi+qyn_SMdM_7<#<;GTct!s%z%w$*6)1 z!$X^TFGPiJVRnJ*rh*3T9VhG(yXgH$asET7a z&QNME)KAg~e40Y*XK~f`0p1u`Kc#~)hP@@xl-WM6D77axOU0&D8`lZMkb6JcP@VFw zOzZ?6p}=XpqHD`XJ!ERODCrK>lms4SNH+`_R!KJ#7-iVgKwlQNKEIa0#~BVZ3=lp` zmOEStpq;=c818PEVGBppCnPb3aRpfwpTq>i@HQSw*BLHUKhoys-PjaW6iB zqXOJca<{M`>?zL~_8O&~?E^X(a2OsKcshb&-=CCVEr9(5gYkbdMHDaCMiQs+q>3~6 z6czP~u*PQa&I&J;ggso^fa5K5FMs3s4c(JUT#3kKy zQN?GZn|4H!NE8!zimKE`PPzIs49Rdr${52X70=4@mgMGI0H0oP%qb|SD1s{ViB6)w zKvk1EzSmT;(ht;J&Rd3FAoTU-r^luzaua#7^JGU|@V`r=MqR>!0$s%-mKdJuM7cJQ z2vn~3!Fq;KZ3(($vq5in*2~*iaZ{}TxO)e^f`iiBZg69|}x9OrR?O&o^QHr|5 z=L|x|nVU;V*WXt3aZJ)^IYyIsmqXzJx;KvjI?xyFLefxCS11MKyJ3{ zku@$Aa#^+9jQW9Vq&!*Dl)kp1uv2L^@dR!Vc0iSr9FOG%;=6zULULN zlgVMjE!81tyJFRi7VmMvO|oKF*nfka4waBiQRnZMLPo7=uS7z<-Cq41QroRu+j86@ zm6x1SYv7x1nl=k-lU&*CH|R@-B;`CGpA33S*M!MN!gaSs&+vT)~VO*39GK3ej~CHGCbUJ(H7;+=L(XI z5^B07W0+nok~fM>zuZiivd3K%bD?G=HZ^WpF5S)Wir>4O6uwXOPae9%!*y_k9h}(} z#}AWq#q%Q-Z{W=o-Xy|LB;ltjzKJU-Tp_|QB;l7TzK+W&TqeSAlK3rtr{MQ0{(wI+ z9I8v!&1GvzXgRM`(E5Cy7hFpoNN%J4S^xTHRGomySNAkGi-vIebC$JKtwgT`f+I`B zjt0i*wTg&N?|jf-vORC~pcF{Xa>K~$t{|VQPnxE%y*mp*ZSv0ym$tbDljXn7S&$(U z&gkScxFekGowpSHh2ijqptbV9f)5z_+E1k`LqAyf2mz&wZs_PBF7FHJO`-Q5*hF>c z>l8iZcrtSxEHm^0ly~W?3oiZdL^u84i5TueFZQD9eR;F7j1~GKK2IvcD!um*r0Bu0 zhUbY%ig!qaWEdK{fgQj)cHbaj^xT_yA4>Y}bsW4wIuhZ~#i4Z^{t$!j&~$(O{fN{1 z0s22ky7Up_J@Ntz_hB%i%TP_1A-sSWY5C6+SB#!t2(@~F2rttwPQ)+LllF%AGA$xC zJVsAO&%|FbG?h(X`f)TF2>_KPy+K#C^(H$#-7zP1VPO+0!-3OhB2fQ9_AN*C;dV_^h|s%*8r@ z4iWf37k7OW;v+bO9EI)@UG-IPm?CgLUZpV!D&MFRHn|b>Gix)i%lZj<_m4Fa^PXp3>9bTd zU~Prld6N|`=6zFIUOOxN{O=x?jYfF-==T@>LadIBjAKN3~uwZ9( zlAQ*m_Z|Z2B&0WofUzJUq$h;*klsT=D(N9T`DS@zrFmH!xnchx;s>Y_iTEtKrMO;H2 zG=a@&JEzNIMlPR~y6ZX8$PH%AV|v=QJjc#vrK7i5M)tU8rrq|^oWOi(d8T)~%dm_g z=?I*gI%A0rrNh>Y&j&h>bI2K@G6!jVwAtA76LefsllRlf6X?cA{wjh0B zJL6b_rBN)?upDOyTwpsxx|?@QYiQ6ha&pXeM)YCN%j@0J&D)kMg98QP14gD>URRJV z_21gql$ztt)w}J?@!bV8n~_dy3}*_gOqn^;OXdo$mmHA%=Z#5eB{w7uE0f%?etjLz z!V{u6Tf<5u1vWm8g9NGvc&xz1jkIE=_gd#HI>kLl;G&+6R7ZPnvN^e@`#@K6NP7Kw zJ735ePQP*~z1G;?*3;X#J?pF0)|1@bxwmtFZ&Ongs|6aS^I~s-;Hbm-xFCu~4NYhk zST!ACg}dqCG~wjQ{f1|b%F7GVp^Cf8RqXMUD_hHyZqFL7OP#0V?sK4)Edn*Z^mSMV zJ&N@jHsHb;WDk$kTGw&9{Ewrgm`4uJ)g%M8&V^aNH^>(3pQSxl zR?rU?th8s^mcA$a;K94m9>t{^Ik9#RBq*ZRqSVM??Y!4 zmuYw+QdDEgIqfQ$q&?elsjX)RmO8sa`jBIZ(ieWeh66Y#5KRXR1lE_!IAu>LVp#&D zY)jCsp-07M#2$&_3W3GGfId~nv9!!9tJUHVu8iU;4Nt;hrnDJDc^^|wk7sxV>avu< z)6u=ZE!E%C(cRb4-QUsOeV|+5yqQzZ#CBh_YcyPoejft?%Oj0*+1qYsOQAclyaoG~I<2hxUj$n=K!<$8wY(GKTN z6H?{A2${uT5*_VrsZ=tZm4?+XkL68AX8JvQL|SfRlgcwAD#>I8^ci?gEi^%HP(f*F zuvK-LFOSJ|!IM|yGt5CzTvseOKD754?r@ipk0Xz&7#OqBTwsG3M(O#EK&WIOR3NF$ zB;7(fO|rp4Hha7d$8bD~Cu?{Lp337U08hRQr+k52i{Vs`#;5izet9&2x?7%HC zJfA5=I*#qc@IryJ_ZViD3VU{-douJ=Xpk6QOp9ftM=Cdlm-^<*m_}zNhL@9VPIAz1 zX6kS*Ua8?#cr_&>&Q6b#&>}IshVrn%$jTUQp(T8dy~pz$zK)m;4)FZ(8@N1b%CUC4 zV0kgTiOXupa_5h4rEdzBd0j#FahAPF3~yxu4yO7Qr9byJ&ao9@p;MWln+A>H9Rdp{ z=K39f@?Fdw#^}%?!%GhS%~98-8J+3CMmCa(8^oH$BsWOt;BKqlI z&@rffj}3#s&hj`6d$qZz9-dP}^Q-E=VwS>A5IKGkr4aR#>nFVvwlO841g)~PESO4Z zoaZern=i^s_f*=D=n3t(cG7V>IG*Y)zYS{LN%niEp=SX5nT64>win|$OnYTBA;fq3 zoaVEPq(`_Xl%%%D9I{vi94c4Qq!hK7b33@BSxMT^%^)BY`%Kp)ZrUu%X5AvfY2>Dq zY3uHoF~rVWAKj?ihN~MB1o{DX@;)v3MBPFzXDAmg-YdOi=!;~!h>*IBbkmOMmr;TC zZb=m^H|Y(_WX@*iJZVF6z;KyfY%8e(kZdTW-%v|yh=nNUqi)M-2VapXiY{P1}Tw$pL-9v@&H#(TNBmIImiHCVxG&h`>3O;45XB3KVXc^D*Il&S%4e zw2T}y*mH;1#mEL~b2>3q-U(M*IhM#}C**f0g?!iv^!nY2z#2A4 zGq#0A3`^(}w&b8==aOu$3>D05nkoXsHy^{JvT-2lHY?OE=}^{4 zW(w;57ql3KyfVskww(@g2bokRFt=1UlmdF_v$a9nCU9of$PHwS)l*8^YRb@}irP$B zzWk!Yb7=S)Pu+rNDq%rwVEr~dnR|@hEfoZIE|z6Q&S0xsu|dA+n(z=Mn33P+!vlzl%S`4fKe`E&`_6K7RWljF-(je zC~uv@O@SKw_<)p^?&_3nj}-Fdk04WfhKh}<4K632`(kk9?0d`aY$4}%Bb)6Zuv=-8 z`Ejv8EQ|_1;th&L0;{K(B#@q%V}k%8UFI4X5*8R4nJUX?9YU^T63y#Yfl(Uvt>kt7lk#Rr9+B@8#_KINHNswH|N2 z3!-_=orr#rqX^#5zY9>szh`sDO4K5W4{)?BpvDO(I_~qi3f#``8tTL!fe+yh9zphE z6Is-7&RcU5^Q&)#rhe-)w_(94EKW3xV?_ifn(sg~u_|%yIL@m=S4(2ensKbH!l9ON zY!ukPb{ty;?#9KyINBmOg}vkGir_wM+g{yJeF~SK=(-(!r*L)4NgTNw8AYe~Qj)W9 z4WB%Ese)2y3+D6hT0RP|HpxIWoTgFe1C~d9FR+ z;tOj2i}=!Eh+X(HX>W1y6?}CUzDCNgr}3boj3~-)D9Uf*TLA~*;@kKR&masV_%0{E z=i>YL0Y6CgL;m~-Kjsn2rwSGd7=ymwhR1Oj70hiei~ zJB8~{;@RO)c`Fst(bdmOydYR;NxX>AI1&s(Z-$b9y+Yp=Jr>YE zRHA=~^!Jf&9>g)_t-wV;o9v>WE6Y7RgNuGK7Z?3farhN}&6DY5y689caaCJ5R-yYa zby1;v>5=JX8y>pYW#_F5&YRVagt~Kh2y`L%7^hJ5JlIP2%oL+PF=cv`rf9LbRXx zKJ=Ek%em+L=RfD3cfa}8i(dlJDxV0U1f>SbOq8QSFynxA$cn_R#9*Ye?|>b31r_UK ziI}@lP}uEhACJcO%6xw!`5&rZbuw9ZCk?w@z~)=G?{SI$#~pOM_MxO z(B|BuU{=~5jIpWZ#*&F{JAEh?wFRr&i;U!WEh1eLdo?RzYD`SW3_+d6dF+H6i|T=Q zS_x~=P75}-C)0xwCzXyR1_#pCusxDYAB;G5G?R|GqmlexTG$DL=y_^Q%)(iM(18&* zC-vgfNxePSSPwGW#MwAUFw=@glbM9uWv7x(%uS|81#60pmCiMaw4B;ak2=T1xtJ?B z_mG{A4fqP^NglKl!SOGf!uY+#2hcN}XX1Q>1=CZO<4}?Pt%+!|U#WTHbbgv!bEcVuvYi+y|4L?y_&l*pAz*aNU4L z15G9tV~OCxLZBIP-Be^7|8%>Sn{foj=C0P3p4KgbiwZ9Plnz|wGDHk4H?aaM1&fMK z(q%iTWWupqiD#GCsBruluWy*jM4P$D=EmYJ^M_xsDcL`|DHDs+kj<4?EttETUKh@) z)9w#DnP}8@oPkU{K3a{nxY)oYCN9M~!Kx`PA~=hntdDaFHZ-)K$~RM&yV<~c!Tj+7 z+Q>J9R=g!Wm>H%Mw;qn#DMd2Mwi``crVLxXsb$OluGTAew|4i0(2N!no3NQ~J!rc- zxkQqN;H-wmfLz_K;tm?G6Cg@gE4xaMi-IE!CQx|SiBnBvBN;S ziJj=6?uv{tna6Vp1F=N^rcphhpdqM2UsPZEj2nwbI<*R{IOX4kD-Cp+=thsAR_iUV ziJ&Ux>`L1Xm8aQ$pX$?VVh^t3GZ%5HYP=9ZL+qmq6j_X-QxdIDT_NnnH72fAB5<}% z@nrO%-LEXP-^BIGLRDKkyEe6L+0xoki9W#=zhgM&gk5_$l}uadSbQ|x=k*NtXY8<> z3=e1GZj3=FJU}%Qv2kU9u;r>Qu4ABI&{+tn#R7ryfCQsOmZ($|Q!DYJizZhEhyfFW z3Juj%PPSD}VB8ils%RYM}%>}PwW`*J!Z%%(?VK?b5M1|pJO?*s+uae|J zMLy4&_=F-Kb9YC}?w)O(U2XeXw^ZU&Ojp|yhpc$4KO9Zl{fYssIPv`%#;!@B9dJH{ z&@C~?>LV}ZVp1hO$22FvZHc5C_VPEoe_41}+_oIsdodawO=i+zz2s#EJ}50P1h6Bv&3B_0OEd#Ha_%{Ar!0k}MT~e{Bw8t`kNyXBmTj;h-?X!H~#)j=kdopUp z)ek*l#@mF*R#$S7YyiYdqHKe;mNtuac zWV62H&(*lE5;%Q~4|26SjoFv7i;u&WfIe=S^xeeQWW}awaBJ?f%&LE44NC|^b zQP^dMorpGJ&uEGYt=ZMm-My!?OJ&C0tz2Qz?US9z>q=Q)6;(Qra=|S@vt*-6Q4xYk zVY5f`1y)oXKKXK}ApBJvsgz2=1=&Q5I~YPK9FHXqhKDRC+-KW~uu5)25|U~|OjD+* zNU@@bklDQ=2gweZm(ptuDh*6GWroZoFvzH$Mw=!>e!l3HGK&tCjWVu^ph~_vQ)cTh z#xXnWk*omKa*os+GRKs2Wv*cT-%a&KFh!l(S}KJH<1zMB7^_^92jCt;S8^nJ2inG-QEb&iI5~tdWr7a^WQ;7s^5f zx3I{bq9DztK!r+11vkaHjP{hM9mqo&#Qn^=xfmw1(~f6eMVkh~m`@+$m71w^nh>0V z-Tyz3U?qehUeym6)Y@YSyCXB)XQz8q^1`fC2ZLU2z|^zHPYG5`?6vS zD=ZIvndPDALFT%?f~vY>g9#RlRGb}tyH>Y8uq@y4HgXyDz6bTjoES~kl3!Z|k zV|y$aaYn2`)=49WRz;GQ6S2k{uyUaG3B{9Rm?W)UzFg3h<>Od_+m&#?lwo9Z}ZA>`O z{QS;2cgh=K>QeeaPL&0fmUSsb(0dx_21{N=DhGwV(y+V?iq(>0u69bcsF-LxNceEq z1z-;;io5jhI;o<_1H~DYyqeqo`8!gR)0~ zg(na=Z?9lQz%tzvCy*4K-^Oq5F_w68@BH0tDxFlH;+Q)}Jf$uj&!na?p$Trst~aUUG#9&6OR4j66g+2qJYCe;12V85AMI%cku4U@W7`-|W6uC_htjlc~9HKQ& z^!AfF{BW7~0nLw|;|D>A=S|6^>+oDi%G(q=GjP@P3i)6NmGY=5?~?mw$bEeAcu1a* zCk=VZln=|(c^`mdi9?LSk#=n&j$#d1QSWd?HMf`JPp<^|-tXj1x{X_txa}-xPbLp$ zQhr@ZThtE|qnTG|PH++;BBYE|-mbf0-kf^F3DQ9$QqiO|sc*CXQ^8Rt{7JmM9@x z`Tq(&R`Y9`e%Qtb;pS$$r;d`}QjfH;Y@#~qsE#cs&{l$-Tz}`I2=lW0E7;qybPQKN zikU}ool=$MX@)(fy@cpq)r2|rt zC;T2yW#7Djk3N4CA6KG&(i64AfBdZf=q+!xzdR+*7><@*zqY)-tgf7b)sF$K8@G#V|`W6Kg09-SzY-|6uhXO^|eeR>K=M5a25Sz zACY!7=HVJF;4zGq=p*W)Jj&3A9T-3l&oc~Q2nig7%R>pb^X@^U@HlRuucmPv4!?k9 zIQj@@AI0mqk+a`~H*hn4g!=~_yLk5|8oVBy>3r#y9`<+x zTlwp5LVLAt*UNT9T-HB$w?g*N&uV0~TqS#n)P2;)YXgU>WOV$oalttY9|~Xcf#wGckRzQ zVX5-EQsso#tM``lT@CtUlv4FOEMN{?E`#*0p{R7`{4=In9>_f&RFCn1yADsmT@#Wd zEi3l=0hOXC@LHm>e(?$Xk@D5AE6@D8LgF_)Bof!Jynw$L!}qn*DnhIMQSPUobSx#V z|5o9ep6dHv{gM;-hZ5BOi*g=L;Bj3OLcD*F#QHFa^${ZDQ8eK(^7<3x@h5pa>nWa= zdYT77p3&%O@`TG;mKau;Vc0{Lq~rz)zkx>-HMX=Jt59+h^9;N?$F~I@A~HNeOlw;Z zQ&~cEl39(KQ;wWEo%p2t<^{d0lMQ)Z` z{7CU@5@Q9Ie~r=<3GQEW)cA&0tJe6>b86K@`2zlP48LAi_7Yy*TVJ-6MhK1JH%Ia2 zaVcF_u8c+=lM3?PEHQjldnB-slns1_B=A|1z~^uwj`FzU7qE&40@l&B+ejO`@nx;% zjhY$453XqkV$T)63$H?>HbNOkn5B(SuHDPbwa+?LCFGx7(Lf-#x5l#ysm{n6oQf=A4f3-VlIrB0~6L0!L$ zxp;+|evR(%ZEWR7;T`x6sr$RI@I4as_esk?B>ny<;2KAAHakL_-K}ll`mV797YAG; zgwm6kOV_B$`bHpGxaU-R+^g*Ijw1G$pRdMPyPW?`4zsDF-82I�fpT9of$b(AchvZ?l jCKlyGZ25?MKpvBi@J%hLVl9@*GZj_xQTdoWhx*?G`w}A= literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/controller/MediaController.class b/target/classes/com/example/streamflix/controller/MediaController.class new file mode 100644 index 0000000000000000000000000000000000000000..5c4be08e52344f9778a14343380937a150f4974c GIT binary patch literal 5553 zcmc&&ZFd_-6}}@o7ItJgaay+xkZjtd^;@!W^Fo7NW5-St6FYHj$26pru2v&ylhv-W zvn$7hP$t&7AFTDv+_^8$eeRvv{nx+$@lPVU zMt>Nh9QEa?zd(m*fYFirR?{+F%d48VOZT}QG8&k3JSUuI)Hgn{JxE`n!8{EW=r9!+ z&Deh31r*X4n^#>T+h22TjN3jLxoR=nz^$ zD~QEsrTt=oM(7B9)dn4}nkbvuPuZsOvAtz>h(_sHo?a@@%QVJl>Hy9#I$Gu7V$*V5 ztK@POD5KZMSKAn_`d)Q*LQh1Y6Rp&c? z@W8BvVZ+?uqTzdjU-LpI++~#WA2511Z9pdpCVJGN;yqEIlVmU&f&0qqWga33!Dv0L zTRH!1Fc}Ed{W5oTvSM}1p$~Jf(;Im@U7$1cCL<7|D_>fR6AP0%Ex!!}4CwvvBm{P_yg zXiIo9?Q@lJ?JP(?O=t6Tu0ST8XY@v9u5W-eo1qm(NM(L$X&z~MvhzN!R%qWOy9`UI|aHTOW{IhXnRW`t%Fg2 z2^tw$nc%nRrQmP1lB`U%ggd!7JFyK@GFDX1s^jrnQN6^2Em`}FMpu2?a<{F(k>hx> zKdd=O#f2(~l)^S+Q+4hXx(>wtK=C%v0W8fuGXWR@JSoFb74vaBH{oXBwLB{R;l`8cVdCphT$ax|&a1Cce^1v^Jq zmL=oD8c)-zTSx5X^12n)TI~nQZa!1NofUtO){$4qmW|`_e^2j$I|Cq+Rjhn5%B?%g z#iVNW6fYZ&hcc}z0vT4xk5KvTl2zLooz5tc4(-R+WT!At^>OagggRk9R^lt=C*3(R z(oSh?wz)5US$yw$ed)%IE>}koBL7g|d2q))fCWZ>{J%K74>vL{X7cD5iLYzi5bCba zXg-;7Dj^Nafm0rLfMqGrLN&tIRn={a<!M;rdqDsNkN#5$UC#Gr8`I)R8k{cIK;hO)=o| zmW{ZFHvK5D`FrpOCz*1x{W5+|zNs>Lr)Mv5aS7Yoq5dFSRb91@)nU_WIA(LkRQNL* z-{+zbT2*0gS@J~g_i!b~)k%eeuMo+VPoOYeypENP&~FvGzOfjVhs3qs{&8(u@jMk->CfJfaf8CRr)4YW-+=ezn$PYGWj=RlT%+% zescN?8vZk;ax{nEQR>I<3H+XUZBJknqsk2Y%)Sk$T3YP1XryDecB+2Yf>KzFJ#RP$3i~#hJOLgpBfc$VD z1T+>do;vehCmz1UqmhBf!)!c)y_rwrArv0x`ksSF2!|4QB9cQ!P1=d!`2dTO7h^Hy z-&*?%O?^o-(|@N+Ick$%e#|7=Lap6Nkz_1^XpA1AXz}g>lI+qCAiV&2l)@uCG$4-^ zAWJY8m%@JSGV|W}-Wv!; zyV!lPTDR`iwyv#h)hfoddu#1x?PgoM@Bc(U{oec5ff+Kyv_A|p@7#0GJ@EAU}L6r%rQfL`1XHIBs&R7(n@-AOB}@uPY+m*JY@+FZ|0W{jg+(lTA!%4E2$4cpeFk>LkI38T84CmZGV zw2|bDwEhxy98V)K;!kL=p*mWXpn8Q?(;7y*mNc8u$~1QeW_`yTMr#_I`sKd!u94CD z4ada_YiV7A)+=;6ZD4e+Wv4YKXB%dE($=$l#{_#%OCve=o(xM_t{`1H0;jLnGaNJw3gHjLs>& z#b}CzcDq97QX{zK_-3weENAWkE5*wPa$C2Rn(fXTnH0CNY6CSZw1ajssuA($j-#i+ zy|qE`Ww1tF5##n+I*+sjwJ6jo!oRyD6ouiBugIus0_!JeH=`{P>U}sAX+6_rr}J3^ zr~v#66lxR5R}T)2U3YNk&|psuolng*w2v-IP`g4M!u!?|;m!h_jCKHL(B(d5M)(bR z?%)h3u;W4SR+!qa(3_~!qgGd%kLWITET|P81Bry>meB83=zv(a7B>2_!0eEn5%OMz z4$>uz6v(>`+nwsc!i-K2@N|tV*ZQrbo)H6*)qM(GMwjCNCgWZ*vg4%oLl8Sb(v{eA zfyTUL$~_H&>;no73cM2sMtZx(dV7RySfML~thT$W=em*JD-QLJj@8hp2*B8hus@Vb zEx=yP-7o%hi=_2th2BC}0f2m5l~Fbhz@poYVzUsCa`!Ggd8`d*ga058Bt!5rs08#fJ4%Dw@OS*n$l& zX}yz)Lg27G6P7|bx`EL;xp`SDBlnPCw=D3lNU^FTMZWnzL6No@t?D;SKA6u=aC=Oj$Uw42W`}XzHpH_pta7Ie zZ2N*z3oX`Kv}6AR+?}#g!@4NbF1KN72Pjl#0*8SWIj=dv#Ci{>s4HN!zR+b*)OUN| zJl&2`{?Dt*`>|y8sF5~NPHkW^S-!I!C%vRtbmK@K{Xv$G8;$|=yG+wUEk+xFjW_ih zmgdap=`D`*8JDMITXhS(}OI_GWIQeW=x6%5nx`exGc2)npE<5mW z*|f+3f>Mj`s;B|XSAqeL$Cc-|wBW3a#?qf^`#Vd8+X66xsn`Vi(-SP5Wp@lWp`9+!)%=)+H}!%NNeb7%j75$deH+k z!zZ)}!%W4vp@9Pfa9K1_jM^7uo~4|`gijvEG03JLqT@$85DY6#km(#H`xagFZPr2&$9gNn_GT#&rzWk$Pt> z&N1A`_O$FqW$u!N7%AH{aEM$>9-$CCDD8nC!(Libam>|8D{o@|igqkS<7QNv!i*56 z&?TjYaF*D6Uyo`{7JAcGHj{>RcPgk2Fm`Rp(o?$eE&r~71rm19E$=RME*4pdk^Vn* z&|}kDuf11w)#Nh)WzZxQ6C=uY$z+No)*9>Synei}r<3$Trd!izB7qty)i85;S4E49 z)N?`kVYHlBMrbJHn8 z?~#{~0x#x^MDh$0&VaaW zrIZU3QPAoK&M|VANToNtO`lC&sgw+2;1SC9e37V6F}5{C*s8dhp?<~hm5esTYm^ot zi7wT9Hx?`fL2beC(%a*}RJ$nLr^gF-f#K~@`IdcbFwKz}H@3GeCF^%Mh0)~;LlwO( z@@{8E*;KRvEnwzn5Wce4*o`w_~a>eFf?wxnph`&1Y@^3bi0s$lchF_N>0{!6O0R91|k2cT{G>2(BUg`=G zH-pj}py46CJ%(>vh;JJ`4j(;ym*eUg!&aOS58JOvY{w&PpA?&YD!{fP#CA@|pDqX6 z<3jTpdKPOuhYwEcdVD>PkHEB6U@9J7z=z>2@{2y4uVdp?kab8nLC+C<4&gICM^_H+ z64V{^sJknqE(8Kp2;$V;NSh>eH{xGO0_rw{VnjODO5#@v^yL6or*)Ku_7skd0qw)I zla5G$_d|1?PvuICdQ`5&xcGIi417ljb%#%7cn{FOjve}>zA8@Y>I$e46aw(SF`wAq z6wrMu0Dm5_zg-Ua^N8)o;yXZEWD`N`ASR$+5R*+JgBI{S9-?c)jPfYd;u+=YHP@Yz zjB*rdaTX!D5jo=+GRjS)A~^>D~&fLmqrlFflvFYiYRd;siz5bS;k zU_T6YKO(~r)XP`VB9vb*Z$aQiDLb0HNzEZM+ zy7X&!E-L8LAfHJ2t-+zN*Gw-V&*T42$Wu@UK!ZH*L4F+IJcH3^!RO~N`aHVG7edJE zL%yFK<9ln!_tuc_t@IjNlrWnQ72*49@ZT$?zma^8rI+6dkbhSIa+F?XPC)7?y}T}w z+*~9_`zibPvNfrsKfsj=`lED11+;&{^Pj^k^%wdpM*rr}--TQMpnqbt2BYVp!T8tm G>i+`5Z61yQ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/controller/ReferralController.class b/target/classes/com/example/streamflix/controller/ReferralController.class new file mode 100644 index 0000000000000000000000000000000000000000..56d1986fe9003feabadb00182412e9d71c634d5d GIT binary patch literal 5433 zcmbVQ`FGUT75?5}JlGc4FYyE+UJ?)v9Ly(=xGdY zo2L7|@B6;*?P*Awp3~Exd-^x^^n22*2ALs0*dsmNd*5BY``st~=YMbf3&1q~+>Z=4 zWzl0{GkOJvZpl^Yc+xLBvx~P>F%sxK=lX7RUSQM6=t3X1pf8Jl3j?qOP8EZSqwdH` z)l-g+LM1Cpo_ohB27VLK2v2L_WomP9&OU>KZxY z$Tkau7!ug3Rk0Sj(c1OM^|Zivnz0(6JB4=PTt9}fJ&PR{c4C*nv4?Ym2`e(_NL}$) z-AG1m;0v4_8BKGb5|otZw1nrBt_HqVxh&oy@I(-n9bFAwzq}O6in<+yE6#EhRT+IE zH|<9Zvk!am)-2v;VITGjJeI0U`M@vJ=~0YjF>c`qj;6yiDFaQZYSdngmugBA@mslRysS(B0y{c3DnzD0F>o9U z6F5!~$aSU!^6e?BtE~4n6(%OyXswC)xE3cZoWf~=Ot2#GOuO}@QFEi|73qXe?D~@y z&f+P$&RA#GT1g;Mnt6>2l6Q3!Yve4P!!rWgV~s{zH{i9ak)v2?wrW~Idbwe;%-bzo zz@)%tS*?0&0tY%omuh~|2s@YR7u;+L&t@@g;S!#sJ8_LW%#w>LvT8alNooA>UPE+L^~IX&@i#Ys8C=a`*1|QsAaLf* znPGkwjX}~^_pZdbDp;7ubrN+DPJ5boJ$zquv|6PO7=fE3X+)JqyJ_-qjysukbU~me zX`sQp>#M7^%AyMA<)TLyhx0*EdJ8gi&3%2bCt7xizcXo@Y&cbN#PW=amV?qYX~;8T zg;?RR&YS|r)6AxlvOr(KE&JrpkS-tVzAWd)&eNSeb0jLaq8423GRR4vXp2V#{W^VL zu-#UR&Z6s=9NCn0t_3=pVdogmAx2iES2Ou@lDTu#Z5uqJc{gzMZCNgJ?yjD60;$_9 zX=k>o!q@>C%IaFBB15+0*i<~hZ0XxAW47n|D|QqlOUe#N!6u()9&-!U7P)e2E*qyE^K55`#Czm9+dBQS6cwR6b5Sfp5DmS^b!$SA$HJ1SBVo+M6@$y4ZXxn5rN04)A z?7bcx=W~X8Pj$_&&++`zq}M^Gves5@65atmx25N`RYTiXlGfDRn;9##ag7}+9uL(j zi`*hpn%Qm)H{N=V>iU`d;MB4zuGsDpE#8*84uF!qxMu61q5_|+b2!C(!^Q&INNpBNbONeoe6&!)tFOSNq@wVRo7WP?leMT zzD6Fvr;#jcXI`3-*yD5BlV}=IQTupv(rJ5%om7#&ESm;x?!fk4SD~ScK@jm3R#uZ! zgdW26!Cssg!cPPWsf4p0eqB>cj>do1kDucgS^Uz%ukdR^sdYvpdV&?@RWDMRPV)2+I?TF@qS4D-rx?4 zTr0i^p;3b3^^!&z%gJKq0$hsR(pZ6~QGrjv3kYbss__!S3(%B|inwVcGe)vzBv-jQ zf!pYf1Gq&SCNFy#z}CBXbjAcSeitK0?&8?hqp#ycD~xTF1)gLWXPJVh;!wsC?Pm7& z;ttlh!xrZI1Nb2K-N_<-2rpCD}lQ^5DX-hJ=2ki-uTf} zoB=Blt7jl7;pgJOo@Zd6=hLKwUuXpO#YSK(SG|OBU|-rGFj_g+bZwq48`t(Hu08z* z&I5OG@pWAO2l9fKbN|NfR^1ZhUnR_D3E*q&fvatv@9*%u1-_Q&Uv7B*m4@e;PS3x( zf#;ny_!{Bzb-JFxH{ukT=KCh^ztv3BxA7f5{jSFMOwGQJAMmM3@lPrFL;MIo#&5VX UOqm&e2K#MqAAW~F;7{1|e^69o#{d8T literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/controller/SubscriptionController.class b/target/classes/com/example/streamflix/controller/SubscriptionController.class new file mode 100644 index 0000000000000000000000000000000000000000..74611f8c75221ec3126622b7c30051101e3de7f6 GIT binary patch literal 6910 zcmcgw33t@i75<(L3^pQ6O+o?*WCA331FAr9Oq^IALAw5!{qFbfy?W37`{L68 zhVidC0SVVCsrsk5@+3#>R`+NOU{ptZB>WE)nZErWIqtDp(o zT5!s`Ja3dMmef7pl}35mGSBM;$M#*vvZSl$t5aUVH7ma9*n_oUfwf-jMP9nIW4S9Emy@n(SQGNWs`LnzR+4^yq2N?)f21hT(b+Xx$8*%NOTiM?XjrfG zw^4f1lITIz7Z~eYY{lZ|t~8LO?Gwrb=zSU7 zAh6AGOS)HaO}jMh8fAITacA^1zF*NNq*rllPYwrD+pq;&Gq_R1P1q)|C3V`JoSXLz zzsi_p1}BF5Cx?dwZX9i50zu88OA)wR;9%U@kivbdl>`?^kM&?~UH9<&j1xMW!U zZmC)(7{liaGGKB$day%-j-3MSjN?)1c}9r@zA_Q>ysr=`RRjG)rzeJw zpBT*;PwX4cco*WBnoN8cy9@i zw@pW%HkncA6LB4qwkeA-=m)VwA$o^~I~AgBN5&@xMuvul$1-?v`tFenvD3#K{|NVJ zT;)ip@~DQfK;_u@p7RNLk#{{>S3iMom!8d0UunDYNh@9}iv;f1a2gL#3*AYzCtszxOiqD4QyL0jLThu}Vat>xBLlx>uVR^luf6IYQ7wOX>6^Qr-NrmL1UrHi@@2f7c^ z_Vp9&9L%yjX?i9D-f!CuOCr+)0{toFGDK zmyl}$+b>~2Sr$!0SDh=xxo}%BFgaJD@mZDARrysJ{H*8L?3TU48CfY-JS5olv}-n)~%#p)58K$*pyX;-SW_U4A!>ztg@r%byTBZPj;@qF^QqWTY- zr3aB0*qEf4@*_dB4cuijB!Hm@>xQnKR3=c%=H%pB`bcPK-o&Wg7;uVnRK7OqG~x+e zpm>Q*m)^x(R(fhP>TP>AmS}>eUG!ovw@Q z$oWpL!e($$8qpVC8+J~fH$9)sONgj>MmW=FXF-bZRutcy*tFHK-CV4kyJ3&&^c;7} zEEXj<8b|hWguPrYXyz37jXo9{?u}|}Ypm3xHJSU^fHcz4;KPPdyD}t2vcY06m@L}d zU{Ng@Xdr(eE%J@(pehlknm6&Xb#Gu}mn+ZZjG)6!#7OoH+h&JH+^&tom4686hp7C#fX zso{5SojTFy&YBfCl*KOu?n=$jjW&7n87PZiEw-?iiS z_(KMN)bJ<#IdRBg+Oy7#)JFr}&{JcY8NgBP<0^g~)SsSo(h;2)E&@wzvdKvtUWxgK7 zgds`yGWdtUrlm#MafMjm2F9U<=Kw7{T;!Q4?>f{;C9f;^ExsH!-#C0a#cM0CYr3C+ z=T1>ZQ=MA1xBD?X{m# zPAqu_)*3TyI~Uu*Q5_qxGcc{2+Oza|7UyWjMy`GybG+kvHFJCMGQ6A`U%^o#a&LxO zmBGseUjVPUgzQUcmpTC@gB4s#+&%=%^u#O#@~v!-2-tL zZ>Q`#JiHU{I*fNy@;wEdQ<5!8^1Vv(eFPxTAUu2kALJTs5bJpP5Pu)$ZxzqBf`^ar zLExkKSOnbjbWO!$KY?34kG)3~fW1WTws{;F+x{dD)nFUGfFp4jUvdGX0gMWW_1J*H z;M$Fm2+mE&aO5C~bBHI=hbhrdxChWp$sP;^&>o~6E6ItE;}f*-DiTS7+)6nG@+Udg z6Wo1@ceJtw_kN=KX?mjI=Gp(<84QkRxbh1i|4aaRnJOd5KdS)y+ydmSHON1&#=p=E zhU#dmm%j)qf_-ZWzUuz-)$MG;tQq*v-i1_rWHA*VRc>5Zz@;UL z%eR`rB?96xu0b-BijVWIRCClN(UhZ`LPzCY9j2n!sM?|=T#Zg@SFUDSSfEjuFz%xV z_jB|#>F_|!<24aE)SOL`@6itADe6e%>wMtrV4VX`C|{pU_!?}cfCJxN9NCLChn~u| zt4hRY6}c@`sQy+d4}#4wqv9ljT%xif7@uYMF0UB literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/controller/ViewingProgressController.class b/target/classes/com/example/streamflix/controller/ViewingProgressController.class new file mode 100644 index 0000000000000000000000000000000000000000..fa2e886bbd50396598e9e951c7015163ea5f265b GIT binary patch literal 7209 zcmcgwXPxAZlVzhgKkxumuofOf*IJr77vI>Qq&OB$Ivi zec$&zlUavt-L0#>-j=XXpUu4`$_bZsk_6Sg*Oh(*K9j@wq&7LIc+=uxP7A=)=7 z>;)q&6nbblMy}(HSSi$>*xNvxsWCxKN!mhN73z+WlD9GWi5YH8b z18b96;X>D0BPWJ33T3~8H#!fqrxq#j+ItnGuO$?-_ z2Kp7+KU~EGK);9D*i+jTIuo|v^VQyJgq#B@z3tNpIs`2xwWeWd%(s?Gr{SUf!Zbu? z;CfmVT*EROsDnC_bc8gYZVb`0aMEoN*O+t}qF#ofJ4r_wgZja-iL*of{R5*3I;POk z7~(}~C2iDl2d$zRUb}}nS>!`WdRU4a9hHc96QB*bax#mj|4Mt4Oij)Q<{hOZCn zjyrBS2IDm;5d0aCyFG%>km-u7ut8xT4JPSPUT+ts_7`oPr^!GR8Gkm%%utdZqsJB6 zT#PKNP&UlG49Sq^m)5E9>2Cd>y=^2(qco;ajWw^(>F~n7%eBY*Tr@ync?&&}qzRgY zO?lm(PdS6gtj?T3o=Js8OPlUi+k5E(($xs$ij1l|>Z~uDm%W&zC+U(x_i?&f@-y;z z!*MNp30_oTZRuD=*T`wZ7$$YUoTR7e8Dwn;@iU|^iNhI$x2`E?L@ywtTvh1)veSa? zqK(uO^42TVsn8>Baj9zUEQz>QsE#9Cp~hju6r;ubjIgKlnH;`s9k$YXZd$huZu=kW z+&KffpN?Cwnw_i{`{syn=d8@Q&NDH6nih>8;+KVBw1GQz0$RikJ+qkOExx zd$F>18ib93MjWPi+09ku|Z+*su_L zWlZv2LuA!doa#6O{dY|iu@UCQwBZ+ugF0P~Y2 z$zG_|9_{K@=z+h` zExz_}F<{vkW_h`>>Yi%z63AK9P|^h2dw^&v-r!O~HafqPth3R7JUXL-i-rXFhaF~WUl?d!9atM_oHt~75vY1J zz`7K`tH!Q|x-|ys@>Jk5?h&J+zQUq7E+bu3f{?dHhNfZCD%xo=D9POI!v`TNuA`3d zu|%PVt4=N`h+M8Qyn8Mnb2n4vZBy9?c4~UT&=$HiAFNK`DD_jQZJnaT)w7N^rE@h< zXX5aIEzh@6Eg1alu!Et3e!>_=*p2_uzd4>G(Q>)*$}5 zzoFv}DIJGyQ{pYOYAB80%~XruJyb_3)k9JSt!8gD37SO<^Q2T9;_jfO=`VE2XAO6o zsuj^%aA=XPJG4a4L%<7{sjg4<|{sJ z?}N5Fd^;g&JG@LE#xt-?=SMr=rr{f-%v@)gtWvu~7Nim`RGTF^+vxxx_eg1heD=95 zxG-v_^R$D8(EAu2q{jm?w!k}?j9T=1WYnUc-}3kdodOC@&{yee(3`(m^mVMtWZ(uI zw}FxjNkn}Oh7kso$oR%eGQP=ReQPxt-{$Y%`Ja*T-GGem1!O?^5*aNVh~LK;Ecyfb zA=C|XGeJKBE*w&C`XQzHAvJz+>lBl+UIa-}aGx$o*^HQplJX>+;}UwGq9(eG-lq{Y z&qz`_uwau_JC--^5HGR9rhp zKz$EG{C8Sg*F;9Wz10!lUXo^$~fF7Rix{}S-@SNa=z|L)K~SjB(-7ql5I A1^@s6 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/controller/WatchListController.class b/target/classes/com/example/streamflix/controller/WatchListController.class new file mode 100644 index 0000000000000000000000000000000000000000..7f2b23cd8c2c29a2ffbc40a55ac08a019a98dbea GIT binary patch literal 5231 zcmcIoX;&N98Gf$7R=_e~$FUPH$(Yz>F)-N1B&{$ti*RVf60z8h)4GFn5o4qoWo87X zuAA=rzOVlw^=X`*_MCp~hyI%Wf}TG2YP4Yql%CTMgXZ4*-uJ%ud7pO~{`H@~{R2P( zf9ga8ZBewxa0EvMdLCLkmYK7hthu!DP^JTcqZ77c2a^JA{R8VAcmW+zbjENDF@YOt zH*d;ER=$vvrXP6H%5UcEM`qe}0?*Cmq-Wl-g7j9>_Jf(q9FN&8&#p*s$4*OutH~z1 z{E|dfQMe`Ei7s?U(G$Z7oD{g$WPjex$ecNq$*j5+TT=4m|vZi0~Y$v8Fq{b0qO7U>9G})-z?8t5l-`~(HmShSk!j=@u*zjKG0Y+%5OKE!Zq{lH z{Wj3tarG=C1J2-f3~yncLEd&Y3EG#0AFsn*~6#Fe6(8+7)jE zx|6me7mN7~>8)BDInJC+x@jx7Zh5vEhm-BWmQ8-Y(d?*$QyZ=9S&+e&n_0G0%mvb; zvIk8KKOi~9TZfi})oxQb3ACOj1O#EYT;j#Sv83Q{2FU?oz-^amXxEJD6pMQ0oZR%B zgTN9v&eW9NUa$(GBRf{?tixpG@roP&Uk6MKPVxq?q>9X^dAV-;Hu*H=I4+YV^AH&@ zkYuLvcdcxeC2{Av=~}*NRRzqYg7mbkLc%ZR^Om>A$Yj+r@~Yq%fm;)qOa&R~r#)Mz zIe|eXXN8h>AoEJxG76=_*5}u49Y4erFDwqeIaRRBG3?W>BRf{ENHA~wFABInR@PjK z$F^&uOs&RBQK#kexAXr%WU>#i!()(FI(^W`Tgg&|F98;8^Ng`U9@)`)n?)fL%^rX_BFhpiY< z*T;rUOCv8e;+U=&FWr`bQDpP7FGUZfBzsc1BMmE;GgLzNnN*r)^o#o_RN9-V%lcz2 zyN5Pa$#bd^dDu;_xJ555=h%7+yj}g8NCf>W{gq(I-r+ zV4FKvP2Ci!R#u?@kQ5(SS>If>RBml|*tqeI`dUV*r|Pd1Xy4d$JtJ>9R+gz)d#?s) z9~b$B9O1V$+e`J0%(r&FAHnB1`!+{&{8j6ngC9c-Uik>oKXVj8l7G9<#=jT1<5j3{ zhXsyKm!z?XC62gHYZX}L`w=z*{1N;HeFsOi`hRn?Qj#3}3!+z^;W(ccpW*bv-*Nul z&{MqnF)n>r8a%_mQw-ORuRX=}+VPuD@o6>I8qe`_ppTl*VF;u8cLL*FQHJ;O(%^g# zhAB0|E0|hp7*{cYYq*K)`ZANWvWK9gu)=FyKsQ#g#KEy3Nr0lkj9P+o|P|{6fMM*}KWI;*3Um1F8=vRh;8Wyp`Q+N4vK%C~!BR+e4 zj`8bB55B|!lLj7>4T=dL(N*pIHMOgTo?+tTt-oUW!$sw);i`AaKqK**65G%;2w?d8h04_ZJlp4p@OuDFXIW#RC8b9TSQ~d z{}K^vjk(6VBSJAypp5wk8uJvDg_wU;Ve+*y=4}D(lv3O# z&is!<6=w#x!nwrjA8U;BRg8b4@c3yR#+ug}&Ec^yaN9(H2gh^!TaF{gTmg zNqVMw$7?m=n{Tg~9#8ncD6eA&?C85z%A`Eyek($+ach<(G` zHG(~1@FaP;IKfEE6-FmhJzDdeX5EGMp=PLW5Y@%tm)mq4j(F)kL3l%F#^AQ?3#EE2 zO*ED8@5JU-P+N>Hm2`>ias<_XUw6zm+}c;3+u@-35c^`y^?V(djLYkS(Rh;E>K<2n zjOHKUBWxo%?lC({Xx;fO}c=29CvECAdN$72BLDa2}xXu2D| zXrPPr#;qA#*O#9R4LXB4JAjtYVZ~fU>z~E$G!4=@e9vQlLC|NkK#Kq@DJsxKMa%e& z0Um?rvAcx7l?s`3xk6VG>{X4uh8s-r7<&qeuS2oMhtSUzeE~evy1&FBUaKA`BcQ%Y zw0^C%egh~Tqjczv1bS0LzXcSpQ#$lk0==C;@nWSz?LoZ;3;d{LB zC$YldN6P7CTrWrTa`bmx4$vCb6A&U=p$vYFAk>Et<|$gDS%hyLH#z`Ngh&4#zS_ur zNNOX$;HwApxqe0p7eCUm!tzI&YnwZZ3=@*lYwju%a}D{v9-1{G)v;L^iBqh9=>!hY z1LzsinI{}*W<`34RWmd3)vU9HKWP3p{D@rD(IMou?N+ws)*~v#Zr#Fn0JtGdf4YFWoNlLbdO}ZOLkl0^bgY}x8aS=@ z;S@DS%t^yYX&LFH$*uPcPFrjdyc;1x~N~aH@pO zA9JdNoTgGa9qHy&P380|ozszSP8K-XeK@`DQu&&MoA3CDg1*~I9!Fge2kZ~q5Mk8sWa literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/AgeRating.class b/target/classes/com/example/streamflix/entity/AgeRating.class new file mode 100644 index 0000000000000000000000000000000000000000..dcf38027a6bc1175aad22b25335cc61334237ecb GIT binary patch literal 1893 zcma)6TT>KQ5bm?=g~b&V6)_MT<82p(kr2&AgAydxR;|VovQ_5AI_xyV(YdU1G4h}C zkW|G=RUYyK@}pAudS-wX$4VadobA)Mue-mVzyJC3FCyBchZ9txu?AIJRHM40Q!n)! zZF@R!?H_ht@*q)Ezh?rItSK66clu2lr)GmDT6BV1imner-{!a4A9`%ZNyOUU^~_tF zgTy5J_PWc@bYcQmQO(mk>?xY=9c9=`BIxc)%*hr_(J3VJO#m-Nsw>aaEjmMI6;+G_ zY$L9{5RQs2JWG+u=e~)}j>qdk5GF_%2C<@9n7`6dqU|9^v56B725fg7xLqk#J>q~P zov_o_Udk{VM@5j`{r~VlzU}>C=Fxq)`DDAh{an$_o(b*vXYINi*>6_uP{+1D!e{&J z7;SG2_So0Rb;99z5SgKXS9JNuz`Rb`GLCd|*KlNoyH>KtmUh_!sH~@rqVwq1ilX__ zqBcS=^#ds;NIlHJ<1q}L&}okYH`x;j4L;wK@suU0J$L*mz|iP*7r!w5J{d z_dnT%>So#_@d6|Zze_*SXLPwsR}O5iitROM z@F_TEdp)Lh?oh<-i0vZ4V4hURc?0QJaVy-l#Xm^>hCda$iBrCDM6*-{uqFHxATXk3 zvT;j*Td0j}X!G(nui=*R>H_!l5z@Tqt0?YT z7GWm0Iz(76#wWOH3W8v|Lto-Nj;V1RQ>PTuSM+rmrp9qhOIhosst+;cEGaNauM~Yl zcS|sx`~}LrzJ~TpdWxrC{2+i6RIY4RlcCfdSXx6oi)4T zw)s~@qN>CPen5W|;*58R?bH!MmiNx=oO|xMkJ)#B|M?ey1KgWM0aFzet08n zW3@f=_hVzZd@2*8O-Ep^t8Qkiao3M!>dT=%KT>Ti{iC3(BFR{_ zCr1C)SDtRWdYx*Oc=4%cyUGi)!g(+Mtz0E9vVhnV*qE5hejN5A<8mpuM>3I-vTUkAsiSOzv$0gGkND)%QA)GJqz=I*jY!gbL$N%Lza2!2E!Mra|Y`J zrqv9cGr35a^cs;ResX98>e%7a(fhQSzCUo~HD6@v)CNbR6f^K&TF4*S|EkM=h)@*qNd74P1_SS`RDZ-(=<}6OjC`T>X&GGuTBMg cL1hK(4TfZm#lFM6>*$v$zGB^3&JJ(_?V0I5J?Bh!@2}rKeiBiUp7c|MqDhLSDNYH426x0KVOm1h&GoGv zS#=FcJWyJ>O9n-A`AQ$9s4q$VX*xk^gJ!CB!<3&zW7m>q({-e1ytmY6Q)*YaZL^4T zyDge8DwIsq)cmnL}?PCzQuiW1C5T&QZwQ{Na)}RZacf1n1UAEUX2Tz7Z zys(vCv8`4^BP7Z1{Eo$^bjz{?G)7TqGU#k5_yCx&q^`T$91L)xrParl2Q^TH6}yHa z46G_G*IJD&>6AG!m&L9Mt0El5``|F&7cL2p5!a2 z`YY*f+qKuiLF>4vB>JN+-4bLC=kgtxpe5o>-#L=&_Hni(pnN@g9pL}yUO^Kf5) z9ca=dUEHKgyr#h*g6(CTc#f$OO=FF5@AA%U(iK0XA0GU~@XaCbBCG)$FMKECD;5#T z>CQ7O-ME2%=e93qC%`jZA@jUa?GOlJz7-X#wj@6afSm;gHFgbQNDFW5W7szESoI4tzB% zmhk>zO0LoMK*I+2&&24;s!R%X_0Q=+s!H2%_$$s z=_cI@cPox+JF<+ z&!GFEoQAqN-3{gRAe>W2-}$wB2u@2qIQgRzaN>T7&_nOL!RR@>Uv22mbsXrM^aXhFpx8N#-k-JIPG!gun8 zp3@%B@r57259RScvr7V-O+jCM~ ztLu(c5sV^wg;9V0H9zK>!Og0cx6Eo*I=hylH1#8jBe-LkjHdJN_gPSS%62eCLp01N z4qO9K2G0d3xVG+x7){Z2y1^)2Y1rJ=EwdoXmRWHav7AKsAVuTSP4ZZ;2e;299;{3u0{BR@D}cBjC{AV#C!9Ew4LnmeJTw1L>@Z zlJ4jShFCI9%T*LmzX9Mk+;+KE7q$a^VU~rKtHA6|sOXt61@<5+C2lkX)RHJ0*45KX zEU@e1XYeN&Yhgay57a<^fFb_uT0yUoHb zT+*ebi@lLu1YUT$k?5i&a}JGUjo0gZ^JbEnMZDVhr3pC|U~5M9dW(Y2Oc zKjP+&s9DF{kXdDn9$zi1VBu9!IiJ|riaN#Vylx8bAQWY_%}Np^5w7UXpWzGq{sfX|w5c(2eiOcebTSAhtHJ4nd7Fa>)@`C# zCMV5$VOH{o7FN(~t@S zHhpL121hUU7bckBW2=55o9h1^Mf&e4SPi=@)^*v;DL*&!@~C&3CTr?$L^j4l52M1% z;(|!xswGO`iHi~=?KV~eSlz+v%JI zM&ICh(fBPTeuFSVE#v7>;~Z^3OByfY9l^V6cJ3V|TA}5CIYhGvIHx>}dRl&XiRP8$ zIke(9Hh3}0(`E=$Uyx}&lxd4zAVwQgUy$iOm>zUs^7_DMN-L&q`Zk1VAjtG6lxc?w zVN3%-rl(-ac3_h2#>i(HQcOj98NxL3D}?83w;0M)qVL+6#^2FMkZBoAD;=1;*7upN zE2i&5^E4LB(|Rb=tMELH1(`O$^t=O;cj|nmamBP7nx~0ip0+}n_QLZt5oFo{Q=tQs zci?@dn~LcN`q58+>(IWX$zYyJAxz$U_z9)or=Kw!WUp_-S@Zl&O17vvz25oT1>0T+ rHaT+`aaeexp$k*!NvqSQAn*bnAV3+b2vyW*O5>Nn`Vh*Pc-emefmgiw literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/InvitationStatus.class b/target/classes/com/example/streamflix/entity/InvitationStatus.class new file mode 100644 index 0000000000000000000000000000000000000000..09d4c2f51458f8e21e6c3194ea50e668d6c3cb72 GIT binary patch literal 1337 zcmb7DZBG+H5S~5yLTjN=KtVu3L0eFc#E%*hgJ7CeG}1JYFJ@`ivUsl_nY! zKllUuQO0L_2NWzZ=8~PA-I-^ed3NsC?;k&jXp7d15FLuh!8aMHU_k>S~}AEo_cOf zkQ4y+G{Qg1&`S46g^`Y|GHvD7J4jm^c=b{l6-ui*E$PJyVZ&$ve)ser8EB|^deqCP zzu4Mo)|>AH%?+%v8F;a87~902C&76L!V{E;js6t(JRvBv8FZAOazh)n6Z`urY;vri zN+W1XuO&mxetMX(2O15pH~vq4u$Zd$X`1R@YeDK)${qxrH!_sIvXD_GK(3YTaeLj%ylR>0K44s1qaqwaC@G#@>#^F_9C*V&inxdOHz{IE59CiyQ z(|a^SxA$n4ayY>cgmn&SPhbUXcI7*XFW5LVkCp#5LX8aY?_kGp2%@{R0LuW2h+u$i zSdJ|+z{=`3%3q+25>L+Yr9J-kAyr9I;@OCG4P-i=JrRWT$1l`En6p4nRil%X( zE6X#yLw!{h3@NBnmei*7467t)IQ#!w7V4(6?5Zb4XX%`vB;ZUXs5g7WZ_9Il=$rMX z=mPZ#I^!r)Nmr$%2jl+0*fT0h&)Ruu@@L}lhR>g*moH}x*K->HHP0}0{xmCSaI*#lDr(zsja^eMSeETU0NaB3 zCHVUw9Z%|2<+!j{R$l3u0=C_ZU0PO_azMSXEzO#OSqGHHymWZV4*1I*R{b5B^wL@` zlY1>_G;7$p`$?8c%F*|ybz8c+JmRNU_{{XJd|6c_e4QyMH}4o#CNJpf3(NReQ<_oW z6D}Ic(d?q;m6hgi6Vx6igCH>}=vr(%tI|5m+3OY)OBNM`JjwDYFNPr`mll?nHU(+1 zm@BqnJ+aMN#R6!nB`Y}pZlv7~ghH0=@IiN2HPe(hjs#MlpdE5e3)VI1x^~`Rk+n~T zS8fjJO02_%v)&x1W<-Y049+yvEEOi@P66#66H zA6^O*QOl&YqU{_p)P~e}X?#@6t)>^TI1EvzW~L_YqVBegk|n*G1Glq?0+roJ(z#RW z@MS&0XXl}4{6XRfb2gtQEIKN(T2+Nb8%8 zR;%nPC&vlEjf5M*wsZ`hhaX$LvVj7wS33cf^o)9^OB z$*S_oc40$0NMZyGLdlZyGDzUwq2VSVzgJ<=4Hhp9Mb{WDg{=)5g6;s}OZzW21G;JR z=$FUKt~=n`uMI(c$B2gta35<}^f$8)4H%>cAYs7-P5ojWDk2?Bfp*OFQivI`WY})N z)(eW2?Z)0?bF4Qi04LB;zEM9o{~zmA8Xv!|sMu+;+=jHlf1iqhHfB4Go|`Oq&_U4oLh~{s3c7tdPTloZZZosCEa-CV7GGvW z`*2~G|F?Nh^Ci-PyN%|ks-s+pD0dBLScdh(WpqBQQAPv6ioJ%e2jU~$vLGlSVQMu9Oo@&}3Ee3%tH z`BF;Ip%sz;gg4d#MwF#hj0~^_3kEPTwqopKfRQ_&seNShGj)%QeWvqAcmvo$kgpX2 zc0$4)nt%|KJ~n&7{t%nr0`2*YHDxA!zHA9v$9s1I281QTM<%mD&+%k3I>sQH->Lss zd}tw#hJz)XQCuXOv=v4&kI_#Rj-vmFd!WUAMDV~z7znHyeA~jrVs1%+33JNP3p}Ho z+M}H2VmWQo%Q#N$QBIG+Y2gG;frcR`)=SVUdL6^5JIW~?%jpfhjpNiE<@5}kGAD2f zDkS8@=O^eL{T##Te3Vl*meVh^6UV73?}D@20H^0Ca0<#X*_XID$&p5%h#Mw4g`$60*IDg}`AM4h+m%Xpc0E(*XT|(S-30MHRBh#v+WB0JP3*x;vLDv&nqsLx zs+AH&>O+4(e^k{oyK8K0r>Iq5c6VmZJ@?!@_g?$YzkdG%zz&{{VgRXO3}%tWkic@q zb1Zc(ow}{8AoP`VPHcT{DL2$%)7n#2Ed_?wwX4GofmFUwHVheKJ|b|KFa0yYW=$Q) zP`kCY(hGSeEnB)Z>oD}&JE3-zRq`s*-j<y+S1a_+v=W%8HhlW= z+(T9I+-e{&-Qn=D8>*V}*9`TwET%9mkU7&;<^AZ}0#hB(M4Lg*WHF0(1+LM<&lJ4x zdo^DLfg<~4Y|;p|ZIyHot_jQ?G?+9;m35$>+iKHwy)cqQ_&nh+r5{SGuKa*_8$Hm!9=2?xlP= zq_qvISnMnE2Ui_=d(v$Zk#S_buBs240%<2s{BT7E6b`-2+7qX`9G|z`aQEiDYj4bH}z`$jZyWb1iw1M{AdBDv>o+Jy0jgH%T)wCb{BE2+Oi znfJ>;p6?P!i0&o4{6VOk^2=55e}$GDJLxx{sOGMx z-HVZzam-K4v94l?qjrEZAjKyqK8M_UTut&l$DQ{BT*v$~e86wc?cURc)+uK-F^jYv zz>SDwfnQ7Hy%}H`w|;_9Siy(*=o#`T5SJkve+Bt0a%J%?_)Zt!K>QZbtnz7&LAu*~ zW@7;E;4W9j*rE$#{0jz&1aigI+jrhzxHvcX8l%Oz^lMDExu%E$<}u80#<9R~79-w` z*f!xM3qHmtk*^#+CCa2HhtKdigE3zwO@=8Kr6i3n_&t-R5eNr?AOt)#BEQ5vJ}*c3 z3ZuXBZ~*NHm->^J&lc!XaW%IWQfl}Nwve(03JnkzQkhq Wj_dCUX1^@sF+X+#JV7bm+4~!e6G?$hVL!FvS!SPJw)Wm@-_Fs$|NZ?B5f!MCrWnOjl*mw$Mg(1bW1Jeg zX;=;Y;N*?0dV)rtC`)rKXhbWI?}32L8-Q5*OiIc-O^=JKJYQyn#j;3T@^IZzED*@YeUJ}a3!6V+h>{K zzN+aA&Csl%V0 zWm&doc*?e1K?{KYYB-*ux25AM*JIc9Vhv);kw?4Il8)iYT7@|QHi{r(-8j2M0``2z zAW?i?*e@5$zX)0_DO-2n8jXf@^wUk9vM#-9a~N`m4?{l^ zrL9-+L5o!R!dBL{ZFX7~bS5mLh5V(%f_lw~8nrs6X&@hQlr;%jISi^zGhElMDh{#s zR(Z`29E!GAusW?EF|fB7Nr@pL^^xQ9QE`7)(9KBX1@XyVBwu@e70;HOrr23Th$@MuZY3GYT?(1qqscFM46b`TDs)%=L$`FD2>lj(?R;_HOufPT;T>DERLI ziswdThxpZqQ?zu~Nrb7VTMOP3&5;1Oys#Qt$Hf)Z6L9GZycaPvN&;V8;kX5^V|9z- z^clt*Sl^WNIW1xx3U-5$?J?xVDnz7$d8aO=2_&x&n zO^gC#GXEEeKQM{WxA^kg%?>6=qsN%B7XZ-{dWw-1JOhFim>3fnXIVl1-Z`c6tLHSH zUpuF%p1yfV5ZwlHiN+9z3AzKyE57c0ARp?6rZ8$e<7hCGS>M4peTVsY900-*VUx*x zPh0ph869Kb&7U;)J2nDu*jb`nq$fYncIXLv#I{D*i@6^6AQo%zVBOa+AD}Myj)Wz_ z+>*FSF$j8&KcEjer3N`|L~`1p-6&3}K~4|A>ERGgLFI*r&d%7pxJs^2M zzUE_oNV3*t-L>)|Kj8kTyY4>K)eW>gLGM~R-PN_vIr}`T_|Jd;{ELWY>6bz3q23Y| z%2cF2K`(8{O=-K*YuNYJHdHMV)OX$SoM=)|@7Vat0QJ*Ai3ZDbh{}TAtobclJ&~=p ztL!ielx(fL&J$aCkrQp(_XB_3ah0HwtkwLE2W@Xv|6XJ&22QY{7s~V^y(DNrHq?TQ z9Iqj0{2=)Rsx=%f(=i$nRPbaAG>%qx-CK+TDAcIqWqO%W{Z0#(mIIfKkCy2bdKJdk zoFHn>$O!Vov3%sTlwI{}(&f)u#cO3cNv8z$ICTVZkD01(jGQjh>-2`8Bb$!ef@Q>} z5vVX!VJgsx9M`ToVWd^QS*EirFt8<~T9ZEu8eKpTVSU93oi$fYd7d93Fun&RBe42d z29dPeDhL5rUQO9^^$9^|a<^_NPX%zGz9QX@0<0ey#JaJ4fCTIcs+NSULvis+8eU=IP|?e7VXq=y;7oA6f z-Y}(Z$uRV54kyCea-!ybw~Mn=w`Lav9Y$rYB0H!Yt?leoLs{|q?-iWLrI}LfbCuVK zn#}2KLC5CNCr^dG|F+-stQj92FrZsxwG+4~xaEav25aeUK_{9~)V^jXqv0a=yxzLF z(MA&w>YiGy!)FBi1~-_+i>x+(3l$3Z<*QdOUAS}^|G;3;X?QZ~1Q4BqZx~l;-&{Xn zIIQe%y@|=Lm4TXA^0_>K=x@n(Th(u%ElRuPSd(E*)3%)hcr62koZx z7F5gMlrD!{2)goYl?*PZfck=3R{=LRcuhQ~_YaaeqWvyR`^^5us^h8oPHRmCOI(~h zBzdx}$iU&X`B;dW4ra_j)7uPuko$G)fLtVl{vcR7&tq@((PUVFnvkUt0xjjsU^`zkHNxul1d|r~NEVMDb z!9~9l)YMIVGc22tagirE4i2>2)0dc8eVs(IUd`Rs(Zg=V=(6JLrxmC+ZdknV8kV#= zA+8l>fYWJQ@p>?U`4=}goX2lrb&7gv49{__D~it1dAb0=#gJ^e6w+n9{o-`nu4hwkXG zFoP<1-wkP=?mZ%*=sx|E79P?fEomHXs;SKKGszqU75vG8-xU)>foHMuZxVlKl@IXC zf1PlE3-m61f;SF5K=dg+#FG^~0)iF%hG!q17AvTn+o4kB{0<$iT-c!_mC?cu4Od2s zJ2bMZ`y`cse;>N9&;U~Q0$oMWuhA$Hc8acRdy}zFV-MO*`uUAh%IqwiDn89Eorr>d z<5(BzGrSKMAw+-^fCLC35lsGb`U1a9-o{hlS*-kvPW+CS9%An~IE2*adA>?t8qc{{ zm`5LbePY+YICj%uF{3pck5Q>FeWvc2V^LtioFuK`H_NG%<#a2TQ;q6*oJv_vcfhII zg;U&Kh7-q4kfQY*PKUFc?&Wf7P&1Fy;Vh>`a9ZlZDQ;fFiSsDPp^Y3)N3xtA5+S9{vjXwq_w+p9u<{3_L--Q$zwR`){%5w5^IK@M;1D7^w3j>iG zdN{X zDET&@^_eW|Z@~K7F06Uf?`M6MmUmg7O<8}3uzZhI5B;D|gb@VhN34GW80XV`az6h2 EKjRE9e*gdg literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/QualityType.class b/target/classes/com/example/streamflix/entity/QualityType.class new file mode 100644 index 0000000000000000000000000000000000000000..3a97ff11d4eac571f5cfe490ed1538736b9d0376 GIT binary patch literal 1122 zcma)5>uwT36#iyg*cPc=+G?@3wOSQu;y!?;iBU8ph?f!)f0+SBI(09&JDBoTnrKY? z;REi9lRpCQgrc{DX-Gv9lUI@{;R$FbwY7x|ihD#A+#a22blen_k&Dq_c{Hyky zdfL+u{n~T-qk8(x>?VY;NIx*k%)FQ9ecsFDpMU@S3&2Cv(&#~N3Vj*$V?bc^wcM4K zBi)*{y8T*}LxF+2wrhv?1bTC`r8EXHltMa#6UYdR?TbI~JV!}aAhj!37@Y;C%B!dgMD9~dww$YB* zik@3D5|=ZW=?F~4cr~N58RT(==?X#_HUoj{$HZFD+{n=O+$Tn3PT=%9*=1-YJFvGM zHSfA!Btdhhh-XLop|l#x59}aRZdqA{3e_$qUOiT>^2tu6r0aoVgG?^>wY_6Bh-hQ4 zVKfSh4_7t{8_xyKCHh#F?%syC>T2Qhi4sem?Jjsuv+h!)&y{s1aXc=IEg99tmgalV zZ90x*)V*vB2;4UGRV4%U-_{-LwJh%ho@>cADb^DAg_`U6YR&h^N@%NqUiwV~&|*Ud z!5h!lg<6%iqbfzOX1nuYsOpW7l%}>Fue_rRd2msG~-?`X^TmTdJ<--1^mC)uLys+DGaTlpKB1$s91U6Q`7_wiw0_{t{o z+%dJ)YFe_9KTfvQbF(ED{jv&Qd6hNkGf^a-t_D4Kfipoj-!Kccyt6DDF_`3xuQ44_ zh3Z_?R9Mh7w%X2&Hq%eM45G`4964-_J#(kAjA_-b`7ui~|8Ig&Ut90e-?!On-A=jv zvmyG9x>DfAu|AELgPlA?=Pd&_kITRzeI5RAJN#(!4Y$LI_NQXfy5^Wx9jEGI_pCm7 zAOrDh!3>Sl?jhmH&WyBVwdwQ|uj!Z7BX$!Zw#w|9PM<(>D?Bvs2tA!i>=aW=ydCu8 zhX&J}rahbi16(BDy>N!r~K#WntH{x?khjdcDGoOy>S?W@;BFv2gG$rQ6V0;>akHuh08vSP%fTl2Ea z1A!H+a@9U8ezlK&`k4NO|3utqQ7{|(eDt^ZSi|>}(#ksNrm8dt)B7CgA7H>OGI6^j z?tymP191(;abKXYz&f6A?Z!0GjpX*y zFuh<49nRfUH>UMOOh4kMBuu8xqiT$DS0bioM_`HuMU3gR!SoVa37CevF>NMd`WZ3_ nQ^)j(Fuf$Ets^i+Gb+Y3Moc}}Hfz~*qaJq^?iEpTs#X63)E=$z literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/Role.class b/target/classes/com/example/streamflix/entity/Role.class new file mode 100644 index 0000000000000000000000000000000000000000..3892bcf11b4ab9f2e791a6cf34f40866cfff5b6a GIT binary patch literal 1275 zcmah|T~8B16uq4bAebf<4Kz*UZD_|bxI2rxvj+Z^CK?kT z`~m(bo*bYkXt5)Mv9cHP@W2cCQjrR>3Y)i-FHVP zs%r%mUTUN5mY|V(qf??$Dix_*p)slmTI~kDtIni<>M1v}p_2YbPoKHUSZ&YU{lHU# zaz^?HQ>{zx0~>17f5B{bDpaKjL5}XB{Q#yF03m38KL(kvIy%xvo_b|WV5QZ8i3H67 z{z-;bx~D3PbYzw3D!17~S#9Xm8)Z}|t?G587c0Od+lcX zgP_HsJ+=cc_6>8J*z+VbkHBhzk+IRA<0KPsWjpA>m1;{HwHNzGDr_^BeG0nL>&Q@Z zo*m}wvBraIt=kPp=h^yzkD1qv4(xra>~YY0FGKtz7M2Oh_myqJ`KfwipeFM^AElSm zgM7&yvlUUgm;ALeSBkTS0H8btln;V9?5bVO4=nLmZ1OPNWZ_OSK6uE&P=^ywaGMK3 zL@O8~2L?O%VUl3@CjgDMA>sh zftX+aN#Ywe4lQBjuZ7Sf2mS}xF&#kkkd_gdUFgV3PDe(j`g~x;&n~SLp3!n{JNrPz^hbK4Vu<&(Da{A X4y{9(LmSB;RIz)C{25e^MYQ=32{7>u literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/Season.class b/target/classes/com/example/streamflix/entity/Season.class new file mode 100644 index 0000000000000000000000000000000000000000..01f8e16976b61b547eb0a1c5e74eb8deefb49830 GIT binary patch literal 2434 zcmb7GZBrXn6h1dEBq6~Ru(Xw0s3Im%S#4{p4WdAy2A3C0CNtI#oy+DDF5TSC>~0+R zPkzvucAW8pKfoX5_}tyirj45+e#pH!_q;v#Ip^Hu?|=UMi->mU$4N?2I!BojWod%Z zwR8RpH*Buj#^K4iuzW@n>rzR7gHgItZ5Aj`g&a+ms7NJ7OP13y#0BqkZDDx6D|qMB zmKTOlzVt7RW5GQ~F-pl6LYsrwrlZ<59qYXkmFXIic*2#!W3+5u6;j&;zB477rS}6=;k!stqdmV+C8O1w- zQ6VhHOI^3T>e)62J85(bF?twO=#+yH_o8DP=iGXQDjPf|&)9>!h2!VqRJfWpM82Jv zJWf0n7V)Kus#rLdZN>eb3p8KD6Nr!WhCBw-9ZVpUUZ|X{4C|^*Ow7LU&z#l~chMCn zH_{dm?Kqm%3L=g*twBS@Zq>?oUK&9;NT+YoA8))ub({lt_`3AZGnKk_D8eDCKhVZ^ zj(Z1$)cQokhySOcipV@j(B~}nyiVH)i;zIq}D&A1R}DaLZ_b#t)lcAT1f3T&}C6Q zYz)D^O~5oYjA=O$(|5Fq9RI;IHH>Kmm{vz%3hNnRDhHUhXgdMZ^si99&C^;Urly=U(F&%b{A1HcZx%wqzn9MT13kQKPq z3_M32Nw4E7CyGNQy#rStIm(Z9+;etSOG|<5y7qOvA&^=rH*%O1n0+o^N~arZ*Vzn1 z*{kU&&LfA^6sA$g;X(m3xF~S@&G>Z{Y846OA|<21=i8;)5ZQoM4dKg%ZBd}UQhPJr zW?SvaSo`g20^>@*?bPFNxDsnmIkliE-EA4GYPn$uUnyV?^8(W?6*WWMiFM!$6vr_H zN_$1VFm^!*^FN6USY2VTf9mQF{75y>mRshq9A?RtJyAni*}F~X)@*j5K7 zsn%0^f9KnMfvLJ~`!eo^w0by_hxKzJuiFGxH|S^SOd8+#YCqVOevcNDp6ql~>rs!T zjZR9?lu=W*=z96g^IOUGey^j5klL)(1TF+&=TQ24$_rjfH${aBfln;c1Ih5iBhPi7 z%jOH#){%o$IZrqz71>s;J$0Z$lP8m)t-$!dSku1xy6Zht;l5;!O(jgh8Zy*o-d{|| zhnj-9b8cdc#E7o`ckv`8I<2vCgH>c$i3kPC=a3ufRUpF~XM&v$mCIr(qfddk#L0+k zsiIvK9|oW#|0=}|R9;ZmVXvA)AW!%g}k|$uIE20-q!0e^BS5shZ zZ1F$nG*2D__}M;(A-Z8O|9au6y?*hlqvrqGuMMmobi<~4tZlxNCUoEQ9f7m&@Q!VV z6wv}RHGah{Def9lzrRl3QG}g zVEG3Kg`2pA+u!3I+@W2TDEvzB`7URVXekVKq88-d%b%ct6oD!?9FZy?gJNutUg7Dg6qwL*OD!fvfz!6!}|VXp0v7 zK?2YRClv1CzI9lFLjoo-CA^3CEwPeKP#Pa_^>Uh~pb@wYZWwMK;sKwAo5Rt>nyLJX z*`N6`0TZA}1(>p7V3|Q+AK~MEU~3$W=WJ#5wJ4I)OZIfaEjwVI?HA8*%kb87U?;`H zKICaW`B6U0XYfhN{3$`!P-XNcsM={>wrr_WQQiCpUPd+o>pY*7GRrbw<4T|D3^x*I kpAmk8vk81|)wRr`eZl!Ajj6*M6p+AHgl+x}u`7@N25`d_FaQ7m literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/Subscription.class b/target/classes/com/example/streamflix/entity/Subscription.class new file mode 100644 index 0000000000000000000000000000000000000000..be3231441ba7e55639526c6f1f18c4affe0adc01 GIT binary patch literal 2840 zcmb7_TXWk)6vxjxzE>qp(%eX(DFkY}p-L}6OF~*VNn01^)(F$|C8O9&x$# z)3M!SbLZ#{Z-YHhD;@^^Ok#NlQT`rPjR=!kbF zcS8}Ln)|(@pzVv3PM z;>h83*Y!eMW-VYe12;bb$~I59ABZ62ZkwBp&I+SNt>`ItxeuyN%XWGkYDK&Z&c~-0 zSRkHtdZH{E8=Ko!!#ZGeO&ep&c26yD$Cb(pT8-zPaMwMj*L9(ib?q*$S&rZLP*y_G z?KzH(+m4_T35k=>rzG=QhiA^p>WvONxDveJ36h^8I1HoIm+;e$bDLfFI#3*K3YTy9 zx<}l%WK>4u>QP%ZOmL1rX2aJ4NniROMTGn1j(OO?oyIFwuqPd)>Kno!5>8aovT6$r zvBksJUT4?#k)jYUj5NVpT;PnVaQS|{07FDNGptqK(@s(p5K=Y+qdOPI8)zVyBXDGX z_oAo+)-t?-@cZYTH=U$0)q7AxpgzT!IHf5)=~5o@scK$ z9do8K)&FXVh76EPY7Op^J}#*Th-lZ|M988W>%adkSu2e6^}eZQD2i)N z+YYK;AvJ=&AxqaWi85%X0#;0DwAH6Ljp4M2{WF|D=X8TA_<)HUi?8BTg}8?OW9lu7S*poBp=D7Xs&=Hj5>9#D~P%47W! zm?n0HJy-jk*f02zp|7w?G2!}OW0e<&0^O$v*cp98D_H++{3{iHhOpmwQER+P-$F|o zuVI(j&DU=JMuqdx@=uhI)Ev?%!%sz8ad?Rqy~P@2IfjE|dPt8nOqbJ4OIoHnJw}WH zrpsxjWiZ_y!W0!i%rvT))@eh-G?r$%r)AotCpxCFG}C=BJs83im0Zj;p_rc1GY!*Z znrT(b)S%}&rpYwZBQVv6FhxZkGfgX|@3c&_X{HS=Q&Z10n`U|jrp6GasI_9IImNU^ z+nPMhrhUw2)@9v`qVYriC=qOE4V_VY2#} ZEHGv0d$qRIU>wEy2b^CbbC^vB{{T!PByRu! literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/SubscriptionTier.class b/target/classes/com/example/streamflix/entity/SubscriptionTier.class new file mode 100644 index 0000000000000000000000000000000000000000..1cfc0717a87509f92268965dd6f6faaff2d77cb7 GIT binary patch literal 1879 zcmb7^YflqF6o$`~mI4;a{URtRC@r@|!7E6jfTo6<(lqfCnYLpY+)Le^8u(Y5XiWUz z5Aa7B-`QQDY!(b(W_ISB^PXqVoS8p=fBhz+O?uHm32I4GGD9h9Wz>Ddzi`9kcFou= zABl=$)VeBdsn!^^{VP@SJJVIQn-u~vWg^I=UKKKySAcLyE2reZbnI)TY^zB+JwUfQ;!xc(WB(J(T9;;!OGUAUg~l&~wpC{z(P6>EAUY~gYxswHkVw0au~hII{C zS4QaK1w3tqjm@26q4&@jsdD?M) zFTm&p#=czCmM@`swuL%$s(ai;pD2``Q5tGHe%JJ0&1b2rqU;N<~0%uMV+)*C&ad!nrq-l;{$Xz2`19a>=4Ywj*OMP?$(fTjw ziZdFE?T=0W=HiG(W6l1_!`kCDUF`#fyG?g+i4%BKk{Izg5UvkaJy_kuJc{+0pj&hs z8@TA$O6aTX&$y?ddltLn*qgvQ>p`;}A9$HC-o>_onPE=N{3Q0xkDbJ*%|vOM!l)C& zf$q_L%#5Zfhw)au61Vi2c^$b>-SUtU5dthg~7lp zOs~;`TFk;L#jwy!b2J~r)EQ+Ok7Zgw6=Ik=qfC=vn!1EZ9{?W1fGO)UEz*M+rk*I% zbS%?DT8d-pi89TCY3>px-3(_;eLmANJ&Iu(h%ylILLdTiv=U?!7QDp3xayp{;u{97G$Ud4aJ^9T~l_^b7YmbOISrJm$LL8T^5uI4|X9u z*4|sa7T8e`B}hiP!&$mQBZ4|~1(|CKsOTXacwR#Ua8%ZT-mI!Q*Y(0E7lEM55PvQG zP};l74|EVJ*HLz%0=3b^qb21kAIK|ZS*(#2njdh|%2;6XG z4czE8oZR5+zF~N0$sq6?%@lF6bbCtd5MwZh60LW?eLBaCWbfKOYz4(6BV3dN0beyy-r zn4c>ZR#%Xhd9MODXN%fZEA`r@@=KgGL4#49m8GwFZ8lTkjt223+gDg}&p<%Gj3JYa zzGS+^JIplNLadlj{O$Em7W#M@wY{vu9k23C`XEAxdPR;w!UA|OI5ysd%@lq*h!4*< z20qIL!RNE(t~0K2pq`((j_%Xrz%{ z_H*4eHP$N)@Cn(4H2gG6kC;FbgxPq8lM>M{cUmwf?*?j|sYq?&lu%A;Md@4j>b|2E zHPiLdTkq68rkkMT0*-qRXn7^Ez9YKhbg;SQZJ5qD`Iajyek-jl&$nd!mYrypbOX;~ z2l`MNBc2=dqpMh5r%t*S&1h)~LhsWD5Eu>UCfy3?Hooyf8{u=9&FL7Qj^h;1tT`Qr zsT_7D9BkcDbeHatjrE5wX_BS_nuadkn1JqM6Yr~0yMWb4_=d!ThQ!C%!*gmRMq&69 ze1EFwAkofn2+~!LBBP~FJ4g88Zjd_?2{ug9n!^PtHW?hbPHB*QxA>N8D!=> zaxp{`k(c|iCF2D)O-}iX^T(Kmqm=MVF&u@S!e159KnkJ+A;K}?fI%+O5=I7TV~(Tz zgNA;?MjRz4fsp;A2*0F)iEtJ(d+y@shK{3&b2J6a(~*TsvD6XzE<+#2mj-(=33@_L zF}7mLv|@UYh^a`+NtiONm>vStOdCuwOARK@nV=O~O~BOOifJ|x(=+-i2~&S7rpLfE z-v(1$8wL~Kp`bNdPrx+LifJ(sQ;D7@VH#+~^aPlmw!st^p25VN5>%$I6EIzD#k7)$ x=^J{HgsG`IVm_?_(|Q|Baj6?j%<&F-8I9jYJ?b**|2wR|2TF8_9~xa^<3DXY(MD$(8d;J>Dy ztH081H&wD*jCw=jw2{qv#pX*k%F#g-fapSR-EI2Lvb3W5kZlv^~!+l z)eg3+tfnl_#A%GqBCf0%j#WHG+$B#@Sx?029Gw@`@3^|{W{oQB6B)&ji-nf43VLNx zcO{FI#d~qOKoet8uzU*9CPMP#S59n6b?AT#TkE z6{Q<-nxR=i=i20h{Wh%wK^LZ`+olXUHutgBRhcs!W4j_Bo2KQe#F6WH*zJKhU8_mk zF&tN#1*xTrbAqmfik?VQf;L&q>6Lv6wLX*-*5$(v7T_v-SW_11#ig}uI{RGErO+6w zx_Owj)=gGUhH9)>hPhx>_NyjTdQ6>nJ?7VKo*BKX%CCftlJ@IrMcdU2dyZvldLwyl z1?zcy-jpS2bK0DYs$S)x&)lz6bnw-UCKPb*m!}e53`NyI6jQ~RXVO7&z0UFnUZLfk zdZmqXCFN^=DII?wFr0j;Q9?Znsw;*whAG$ftJ~7fGA==9RB`8Y+u*hTvB%vpu(4W) z{tL7{a{p(lgGs&JJE(b`-9f3|!r&3JETmIC!HFJx_osSuP3O>0tJ2-EiW|C(%0LBx zk4Ra%X%xnpdaEecs@WiPJmJ_>Qw$|zZ!wtqxeJ8raA@lZ0gzhyK$QA>Ko@}*D&muUEPjD#*y9D@wkoychmBszX>5l!Nv&~ zFSeQ7<0m$^KD%Lq2<>bpn)keJ!ht3bDAPLWA%M3~!5}QsO|$~x8OttfI|hc73EWNd*3lMrWML0x12|{-&@fCF zjd`=mHvvr5WO^&Pww_I{JzhgGo$>(*UB*euo}fm!}Au_w2n#;1L*FM2;wt zO7tGlWGWFoqU3R$Yt#>04RLPa?KTa8^D%%*fago#`6}I`8z7o7YQ@o3ev6or8a?F9p^+O{JFpUN=<$&o~8%$o?`(J1`XLn4WDwJCC?-7|(_|2n2~1WSOx|eqG4VVQv`vK&o{~XKb||JI$uORp z=AkzWUIEiV8%*9*_c8H&>Y)+_kVpRto;-?%Q*UVeZ;GXUrzuQe&beDnsv$KSf5PPZ v8MF46CX~beyeTVPF{I8z9&9`K&_#y&-&J4vl-t9)0x3?XDw%k(9IE{fcEWoG literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/entity/WatchList.class b/target/classes/com/example/streamflix/entity/WatchList.class new file mode 100644 index 0000000000000000000000000000000000000000..e249455270048b71280ddb43319e164115685662 GIT binary patch literal 2491 zcma)8TT>f17(Mb8I|c_Rfixr`x5joZ+q6mBgoKoPO-x`C#+~$q5w?IO>s`-!-N3(U zXTr48hrV{EKdRFs?P7?#sqMXG=_G6LrhM_ zq`PNr>>Q{@Adq=#yLK=qkSvw!ISe6}MZSP@C=3PGGWBjtwDj@l}_kv{c>h*gKAzcU>=#f$g~+fhqdDm3|jVOSuhY zRhzQ{H)D@hm8*P0Yu2UHRWutS`CRWE4e(&Tt)sT_s4gwP+^TN95x5$gV_mvOTi%AN zjb~yl);!x?^qg+XrAf+_Ei#yo?s;4DKIC>CN3z-^#Rh?g=K6Le3-k|Lj&&d#Z#$lA z$sQTj8rKWlex-JmuW8W5ncFuJr(=QiK;+F`Za2+krWU;>5fy8;t6p|nJIdeESOmt* z)z+nN>vgo53ifSYMR|b2dXgrtjnV+)6Dbcc3sqaLQ>g1I*!P;7(kGcDiGb33DyWjn zu@hyP<}@niVJAEsFOip%#x-X;TAr>E1QtgBPis6nGHlPW@~KtoltWBu%3+@mOc>U! z#oKc#4N7*(Re`DIPg}RZAZM0S5hEYA>8!!%xr=HNJicHcg5nA2K%2 zRjx+4n&Etn>+1?PaFZK+iKF8wByo$!UvT$!2PKq$LIroYlhE&Iyw%;aR(ug7t8<+1 z@xwUxnSxqFhNHz`Uvn0m)0N*Kel^j*Zmm(!f+EuHln ze9KX1wK#{c-(&O_ZX}>#>#9K7;vj#A#}VW?&bqvehC6zSG%UKz;wmP>h0v;pFcaOL z#+Bx82Pc6inB~}yDcg_fUM!}kcov5#+mGpipCBCWJUu3+Cm-P{6ljDgyt@TliNQ3{kLg(~rbR5pVVdaYX`YxCK7uK1hX~Uo zc}np=Qzi<###u|JSb2|$zmTu|j*G`I@smbbBSx(&$YPZWuAV_zjP$WW_awxKX>KjV XRc<8kgBh!4x5)Jx*DrX=Uab8Ku5buD literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/enums/MediaQuality.class b/target/classes/com/example/streamflix/enums/MediaQuality.class new file mode 100644 index 0000000000000000000000000000000000000000..922bb47ec541775141043668cf688908858f8f44 GIT binary patch literal 1243 zcmb7DYfsZ)7(H(<>sC7$6G0|;!6_@3RTR8rh!~jBWQbZ>Lj3fy6)5Ri({%*D`IB@c zLNppa`=gA{+s!zlCT>al^t?UiJeNNE{^RpE0A)PTATgM=pkX(hJf;B2M-t+ zM211jS07h4%QnO8n?5q=$l|<)jDcZXU{Gsb;5!V%1+%ix54h!Vf7jaB-gjzINyjKI zY8W$+!zG4+--I%?O(h&~ujOp)Ff0~&)TqP^T#jR$SkC z-fC<+;cLF_(aEE~RAcrrZiZ|nsBy2#LsvL1<9A}!Ut?oc20Bod=v>Z66_5@oi7 zR#~_$HxAisGlY zk^Y3CQ)E97Vwk3TB7P?mKZhAeF@J%gQ0Q4UeS(owFu|A3@Mr17=&&${A|Z<|m0|9^ zpwhNMj?LyN%JfQ`o*6&J_(yvDmy?EDqKnumC1IXAxlPxdShA4O#U@fp(UJv)?!ai` zE7+FYhHP&sZD{tE+NL>*de8Qk#3B~D;#Xa9@e66QSLI_|JAxd8NhJna46ds&&`*#* fLRwVb@yy101!gtTtfUn%D6yD*OIOy_|2cB zBN3v}@Yx?_Jhz*1MEtNN?ddu9InO!g*||S|ef@G2txib9ry8oA-Z6@cCg4W)J@gNT+dZwP&Arwlu$@L^TL#pW2ET?Muw&3g# z%dS-?{?xxhF(fy5aOCZ8Sw3L`?$dB`aAcFwGGZ4q?c7A$@%(+;wH$_c**DTzGEp!>UejD;)IhJPWe;KCxBrlDKivf_O z^$soVGLTG*{UwA#@hm0?pCfOatoUgyB);JG6;hwbW0<6OG(3|CpT-m-A%2dm$k0-3 z@)AQ=U;-bxhJQv8y^X>&^5hxxD0E3b3Mg$Iq>yZutVBoZ88U6eAgs=?x4=zk}^aElA}ZxdpYnqqIm#(eL&7VwlHVCwaS* zoc~6-sfW@99-Tu9opCvI)X;gXgpPKJ%sCRG^EPKH%$JcMozv9u1PvJJwD}UR2p7TY E-ztb0Yd2eF&40_i+Ph<(0E4VU!2m+SdSSwdI7;=^Vi1LCdt|K9s zlAA@#dR^UmxwBQWN;PqGLx+w*1~pqRR$i4X2IEb?)Eb76QZcAw7-pnmHYvesB_AHV!Lf#-DgH_>vGDT-6ZPt_1Q3_ z)_Hj7?`_%vVL~2IOLBPVkkPV2XxUEeV8`);J;$?MhJ@uDcy`zhh*!!PbrMdY;dW>F zq9>7S=l#HsEX{-t|^d0Ng^9;`k6T{+9wWJ4{ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class b/target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..890ef4c9fbb073373e456f580c5587bc5c70b36b GIT binary patch literal 6839 zcmdT|`Bxj)75*NJCFF_i$i_>YIAemtV#72sjbm`AF$kj+L?U4v>?Y(PJz&geMwuD0 zv}v0*ZIbRwy2Ra^?rEFGX^S1ZNt-VIp?^dFgZ>LSJ-u&c5W|)mj-*-MyYIW- z{qB9sKmT*}9RLaZBZeweM^K}n7F!s0T;Q`@Gq^dW4NqJUS(jnU3EkA)ZiedSmeB~J z46Ru!uZek{FBn2|TwCz`q@mAic`GLjEn(Z1ofb~PG96Ke7`8^BD7XXL81A#|Da|R^ zx;Zsz^Sqd|>=|v^bqm@6ePvwk79EDV{^9hgWM5w*#c*hF9bLL&G|UbKcOuSEJIjrt z@Zct+*d@(b_Jp3x2{VS>*b~8C1^aLp!~SJwGa_5Gb$1~#pA`jHw@ilElyHZHfuV*VJ`zPE1G#%Ynj&ac(1O+#Vas^f7$UYP7(7eZ`-McbTTw7 z$2(~lVu~9*_Ea%1Ot&NwY4wDHN2Jxd)bPl7|L~bqUlbpbx0b8+TSYS$Lyxq0iUd+2 zZ^V-5H0tE_DUA?CAH)4glQga8R8QGz(;=rxowRJVz-11(!uB4u$Iy?{5ez6u;!%d7 zFqWkpsmrvL)3|9`lyIbVZOD7F9)v8#0)!E zb)max|1-Ad9PfYi{eR!J!+3(SaTwv+flhKI@Lpt zG&Qc^X>iIn8eDekr5fjQ)1{(S-`o=7L=0KvA`l8DF}0MZMOQbp9^2*%)Gz8n2gL zzZ~(3Le+9krrNB6IhktF-r-dLV6vC8cR}7+CQUzCH;Ruj9Iia7mox6P?x+RZn$!&u z#m5=;N?+8HGiu43C_YK4C*MwRN2mnTacu(XsG4E5J)Dl>Gx8O2W}DnlGs2#wy!GbF7+#QkzeoyRmS>5}2tG%FxM}4$ zZCDfBDD@iPX3h{c%^9))lJEz0Q>2Rd31N@$34@-*2dykOM!Bua``}@Xq=zze@D|7G zB0)}jLx)VL&z6UG)%);jzV`Il)lzv1SUHstuqrXna8G##+&Je`Bbqh6Ihvs()bvg8 zFx(-^mp=?gDp47-SfcTF_@(%Af43d76Im1H6+uW~j$6dkt$is9Y&pEDZ{mFg!;&+Fj@s`$#n;y>L|1n;w z`}w7vUc4-8gEbOWb=sO{XNGtom?!`Bf5q5_HFLMGnebH}!zB=}Bs*Lp@ zW4Qi4b!)@ntM|iq!;+A}EuU#h^XjI@q`oYpmPciH^)|rSFqg56RkX9BpSBCMLAim_ z0e_XwRPzqOmvp5FHh2k{b}Zpyz1 z=v)w6==<|@r_nE+p`o_*GT0@$sKOWMTfPRimSP}jYHUj^L zA@HSvAK}O0@Vg}ZZi0WZBK)op_$>s#mEa#C_-zDVs|f$qjllm&2z+Var}$X_|KB7Y z8Ga|;Tl+Su#;OT=A~RM)KOfFqK`KQT>Fa2fKZ_W>2(>N#k(#BJ^ysN8n2-&xVj3WU zNeTKfY+J=ZXN?r#z8Y!~a8T{(Tm}#5z+OB=r^mNI(&M7Thgnjh zLqhwx$AuFpaWBr}7x*P9agvynnykUE$ZizuJ@iNDgeO#?uZ3_V4gVUy2{?K$;K(Er zj8xlobhgE)YqI9^7ooC~pSflkydRpl$2RKax43IpMl$wu{ zM22t_!$eM+-el-Na}*p;cvN*$;W%2N05D6WnMB%eJ<`m8v|8u^X}r{YRoFsAL0cB;IVD-stzfDdK7 zQyL}0#x|MhxpVG!=JxaJ{R2Q7M+KyiwvgFD7CDCFXa2%%m-`d@G4i$aM0@UtnU>0D z$lXX^>J~%wto!Th>p=RG=K0W|whSlFP~YHK=^0D|^~y)Cpf(HJ3|EfwY%%BF%oTR1 z1Hru~SI()Fq}x5EJ1X+WEBDFVsmJw z|Bq@iSe^(&K8fLLvon+mQfe|6*B428am0!MVDxp8f6yi1dU zQ<%SMo)id)9PK-#TC|&buHIOHy~XFdv@YG**tti#wsfJ05-IXyA7vsP(07)RK#a{N v*gK@@1+04GVgc(fH&6Wq81Bi^_QSOjF14b9D&b5*J{2ca;*=V#DI9(S-)N1o literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/AcceptInvitationRequest.class b/target/classes/com/example/streamflix/model/AcceptInvitationRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..93c66aff3aa21419b3b7fce289b4759f8dd006bc GIT binary patch literal 826 zcmbVKO>Yx15FO`h={6y4X$yrGgi2agt|}uT?fmSvqf7W$#jbEwg;UsXR&&9{E>h zqP_AuHz#~GW`~lo4l#ZXl%Tclob%LMZH7+-ZJmsb*BSS;)BTi>jLC}{PEZF4m&$r2 zCo0v0X=$0{2K-7J@8ogrpP_|I)QK{7P7OhGcdCP12}oPowJgMF%L73>i(F?nqeZ?- zp|jc5hL6WtpY54~rl34eRN7Nk2Y(i<_;U@B2i^Y}4HP$qT#Rfsp7nO<_vcc64XEK3 zVH@o~QJo$Z`kMWDK6tCF%Gk4owD>e1+k{{0(8KmWmb4!@=@Hgl!b1_TSU7A0-ZK2# z_?nJZXcd75i90k8m2rpH1iOYyiBsUL?tUim0Y4=IczTT>t>eF50BD0Y;bB*F4|Xy6 zBSr6jOOF4VyhZmx5t0va2KBYwy-yU)sl%HOa;wywR@f|1Gx{Jc^eX59P*njZXxfG? G(asMZnY|nU literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/AddToWatchListRequest.class b/target/classes/com/example/streamflix/model/AddToWatchListRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..6b8cb955b5824ad642c55792a31f355b24b73140 GIT binary patch literal 992 zcmb7DQE$^Q5I$#3vzBZf9Te6v#@^Z?VF~dzG-*O0A!TJ6Re5$3uk~=^k~q`&S0I6q zc;E-{qY$6VMrl=$P?S5LeRtpI?~?C7zJ4R3=d@QRk5&Szgyd6|(Z;#B5L}CF#9s~1 zWo#K$pQ=pRLq;o|?x04iR12sc(hUk3J&sMv<)ug`TJpl?Qlw{EUGmf6E~H8=SWP!pW)rn^R-TX&2U`2R{K=x4J9< literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/CreatePreferenceRequest.class b/target/classes/com/example/streamflix/model/CreatePreferenceRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..16f0873e20a9eb2385e1c7027fce14cc9c32188b GIT binary patch literal 1517 zcmb7^Sx-|z6vxjLN?FRj3+TnQWg8NWFBlRe=u52xNZ*IsX*+P2Ix_|QR+?x`eDDMK zp^X1&E0;oRG|ipq%$((S&YUwpe|`T!L&u!1uAX@=(i) z)H3qrk^In*1}4Yq*Lx=)>ypuCp@$(EHA2G^6Ha``L5yW-oF*8hFGSFQg{eY9er&ah zsymDpj~bCxp)4t*P6N3YMX?oD#gSpOQBW~Am!evgnqNHUu`paD0z8yuCHS#llc9jA zk+L+tR&}S%XwGqXamBMW=_SE1%CwUKu(t=rqXR}cfSn6%1qZ1rH{Ej|2v!RfS;LF5 zEjEGxJ~E*+MpO~aQWCLeohxNLjqE8c%Zye!!*VPgq0N_EPP*X@?D?$#^8Y~f8f9<} ztjU(S4ut-F+%Ti@f{J9Z5uQqYB9LKs53w(TlF-Vn+nfEirjYWNy>9uh<>Cmc(k2;k z@iOAMd@pldiw0KAT`SwU|1y#IR@NwMGzI>xx8kc+Y&X!X}@wQvZi)ep70`hd7~<675$Pz zyQlC&+hUx=Y7z4kzG*`F0hIvQ6rhLn2rEX9=?TV8GDd|#3!S-kgdx_1NiU$rys zbfzEr1Nx&nea_8|dEqkC&X8Sp_q;s!InOzN|MTZxMD#6fPf>v`6=|$Y<1`^?_C%d3 z8LGG`e>^(TbuVb*xrvQ`A?VWT+TkQ!rpY2rm8nE!LDjmAq<*iWcBp0IomSCtXx_`n zHgqUo;q3KmXOB&&_w`R5ogm&=tRgLFuGUX=;GKz^TO9mKnPzBKP)RlQzVf)X<4&%74r3#pq2ej%$J8I zF-M`^j$`YQ(#DCP2Q_15a;BP1?d0iWX;mUs-#|vXVU#?mw{)Zss?^X)-I+Ff3!2}< zCkFO7@GTv*yWcOUm=OiU-`iFk>{wu11Gaa{c_mL(Xc}pHSqB8~l!<*J_pE=3db~rD zVx*Gzj#gQ8wRZh~fNCf0kM=t#Ra3iV`-akUSXGknf2$9&76z>jPHTf1C>ruNLe zpoLW~U9VX(1;Zfe87wl5Bkya6sNE0Rw`}9Ja){%#1C}d@3L^V@pBoCEDjrq{J!CaM zm?1Ud_$36}%{l@nIQ36uyrtZB8{=9p&AHYn!)?HDImU7abafO{1(@Uon7SItc!KZ% z@EL98@U$?B=}|7G=k!$`PYa`%o&eL+3ovz+nPFN2rUHb(>({B6OF*}R9~$VJe*ti+ BpF02m literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/CreateTrialRequest.class b/target/classes/com/example/streamflix/model/CreateTrialRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..87cb318981e97f1a913503ea6a524c9d4a1614f0 GIT binary patch literal 980 zcmb7CQE$^Q5I$#3yOeC*I_TKOfW5VwgvImFq-g>PDHJrSzPqW{c{uh+oN4?kkU&U0 z@B{c!h|i^?v=k&1<<4i{efQmGfBXLBD-k`Vy*dT7RHI5vAypZzU5G2ewJ;<8a&RG2 z$Ef;58Rd=`EwwxSh?Xg;Q9Y(x6f@$f&A7Z4*`=1eaJdxOnO4_4vqP!*31TOExf1$R zzAa?|v`D0>Esc9VM6+btEwN^F=+ReWYIx|XQ!@8iTc=1@ne3}V4YWKq#ya7YH3g$K z5Z;K~34SHC8jiQY$$*Mb#udD4UANTwkkRfeOz++0EL7zLoEbF|Wn{O^1~Tsnv}4pv zY$|kLU<|~Q&5Ao$=>ItRPXRzA9!YmR6`|Sg%*%kBVkT$9&v|n%W2HKM$oE3Jb31$` za*;_VbMT3P-J9qZ0pn&}e{IV=mCuwPX!Fl|dF0_~5Ap|i76Cq7FYFqkDluAzZ{lAj zwn5`4@VE5Oj1YBrQ?Mu@4$t7j{m;ZcU=h%5SbvO7x&s?e)-BpZ#ON+~}ECj~==UZbj!sxdb{}%u)S1?cP!@P_DJUg0-HU z!*4x3$INHMYk_Os5h}h z(V|9_ZELCZ1^y>mc!UF#UGc!p4~Gq-`3$#kdqjDIvXxeLbjM8XzXH4?aqr!P#bMVa z!}5rs{EFqSTamrT6O}#x zM}_I!LaBpFp%1q(l)ztWY|Il{LjUe!Y9XmYr zy^z-X#AO&!Sz)T9hkknK7B{yIOyCYf;ghHqDyX7}rwn8YDLE)xMyGvA2ES%E!R1;BFFbMW;z>=&DJ8vHA_wy z6^MV=Ir0c4U@Yat^`p}DGcmV+*;`?i##3udnZi)X@$7P~dSC~8)`3gOsIJkz6*zLP z3scdtLy${1@Jb(F=ZaU8bsfpVJ`=KON5}r*wiQ@aJF)}1H;(K`h*Phr5nejZ}QVVq|SJwvzKL9u%&lup458Z2^})^spt7DFI$S~Th>HiOe|$f76-^{BlC` zJAxEF4PXp;(Tw%6h$WuH)_vL#TR(G6bIsD$Sbq8(+pV*xqWvtt)7r~QY^m;rHg%MOG%J4#^dvipR1jum-qY4H_aiB)iU_<0F9f8i zVGBr;Coz-G|Bk_5x#@yLD)|6rTOv$1Bm4xP)+3Z`>FnnhFXc)8r-6IKJf|&8#9B@C zJ-SwG4{D_q4}$44Jm5EpY5W&nwqlxZi)jU)Q>#(I7?WiDBEvDJMPgd&fJu1M#3VUm z8{%ObOq0nxnQbwBiLb6;VxA_Gm>v+*N(W3*+a@NdA-lLOrkNzBhix$x+GCnYVk!{R zqYjvaWlc=NDMi(Gd74dPdfXP%H|;UCbVppfHDX%tfJyk?#1wOU6_4xbZ{_&iB&O$W zFvYXs2`xUwx9lk4)i#Yaxqesj)vY@v#`cofN*%Dp^Ja}N;#twdu9&|-JI9OI;W}G( sgQqS$!*i~KRQis;qKy}LN&BFJ!X~!38>QMN=fpPW#VhRM^^>Xp00_Q1DF6Tf literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/ForgotPasswordRequest.class b/target/classes/com/example/streamflix/model/ForgotPasswordRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..07d40b64996f374ca6b9b9eab150889011db367f GIT binary patch literal 1173 zcmb7DYflqF6undWfKn+?K%TmyBA{Us@tcPD2qY#I31Ghu-C-Hn-KjHEz<;HQ#>5Z) z0DqM6%yumzB*qUr&ok$qd+*sFKfiq^qD@+=Qh^34RIE{n%8W*j#i`)2NF)CC;8?aD zqw+JAD!0LCV0ooAM1wR`p=ynWsmADWTPIwei=-1vZk>@LIgHggPjo0_zNJm1-Hx#K zOq+05zVAv4!jepciW!YJFM{nkqtfUpqnX`q>Qo|I%Bq7{zDQH;gi|`Tj8^dAL>MRd zsfbmW9rHG>oDnK@mTznK653C|Q%R(?B9e^8Hhs82*}y=7GBRYe^e?9D4QM7_r&+8Z z(;OUXlL(~nsHrr!XCjKE;iv0d3(LiYRG!FC3BK1p!ZWZ8htjr<>i9gs`OZr7z~+=9 z3=O10ab8C>p$owCe7lAcGg`P@mi-T%%cUv4Ld6-?n<|yt-Q+-;eSz}%BI~w@Tf!*6 z&ku|4NFm~r=Kr@3*-S29ZQM!=vfoH|q{AIyL?WFuh%gvQmz5G(FZ=fWHdnvjgSL;T z?&+>+%Pr+~&t7YuHP3+Z=UxhQ=2734z?)RT5@{54a>t?JV5d9qYyoa(7~xusH?Uiz z0UE(s-O?zH^%mpQ!0m*kNt&{Blcs^2rb2`ING$u YUsL(2rUKo~g5S%!FpTgGe5m>TUugshqW}N^ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/InvitationResponse.class b/target/classes/com/example/streamflix/model/InvitationResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..ecd64bb7348ab4fba13db001f698a3392e60090d GIT binary patch literal 1095 zcma)5T~8B16g{*3qT80%3RDFJd}<8Hn1pbvK5)&W% z0sbiCncY=tOB2%c-kIsW=bSlr`s??Pp8#ItsS5{%3W{}Ljk}lWLL`%Tc7F4|=fk3-|BlplGIvVc@ zyu35+e0OgkP|UEKeI2Ry=iyK#pX4w|+2WB51~SohpC1;@iJk~NUv!s6PEcs;oiZo! z(MOrcP#KjFrZ!gQ@3jTIZf_CntVL{{Otfcll6*o%rs>_ZE@pZ`wwhl;Q#;t literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/LoginRequest.class b/target/classes/com/example/streamflix/model/LoginRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..51200cd951ede3af3036cb7bd80030f74ad6fbd3 GIT binary patch literal 1318 zcma)6TTc@~6h2cZ8lxbhjw6hr|wJv|CJ^h6CeBm z{wU)&%eED)(QNii&zW=i&UeoC$IoxyiD;Le4pWZ$3e;buJPk0KI1?9wha#@?{gX54 zTSfyfRIKbaqrP&bHbjFoRG{G^jZl$M#n%y+S0ZYLk{g>y5uJwWibpz-A+PGXiVx-c zmNdwdmyu8*qp51Q#*s}_T;Fn8$BH!WQVuqSF_$_C7|kEHVyhxqQ%0SH@>LvbE3DG7 zVYFUVnwv{eugipAY;r9O7hMM)$v_EyyI2gMUXkVs=Zed^hU z^jbzHj0#yh;KCVRX6gnyWwZk9=OVF!Ux-izDH!epkWGY&t>I~6)T{iyJew z8I87M-Zg}2z`TQrF&eL`Snjo=6PX+fQ0}IseG%3~qMV)?`)xyE25Z&-9zQA%)}>A9 zVKh^A{InM>TLO5Pj>l~IPlrkkGkGg*Lk9;UK^qngqJ89TGXxDey4vuOZY92aqg*SK zzwhIQtM9nKUQbV_+nzG)#_ok~cj+M(Glvf)hX)G}IRWi1_0c4LPZ^q~8AG!)2kV{m zhG>j()8s6W&X#j~okf6{FqkY<$||yIuZs}*S=7pTiX3~ z5L(F7LYiUj+AAyXpgGDIJ%Lvqlfl8L+!NF2ZA^0*dc9`nU1ipjuP2oWI^e7 z>Wg-!na=b@KR`cJrvE+3vV1Dha-<~ju#IrNv9p!q)iS@(&nGDWc`3oC= zXwfaYEoh?cISShSJq7>t*o*j9ELv3|8=R`juP2hdtXp)4vpd|9fh$`Poo&%w`UvT( zod)<4zgcXSWz}yYqoOFNe+MZSG+A<+^4V$QSOygbQ8+SZw4&xhF z8E9L~roF}n>~{iA4%5{z*LJ1V155$fWjY`G)GXPp1-B2|MS~@drk3N_-A#%|&^Wf! zYd(F@uA=C4YRSltI@J_`bt%SRu^-nJN)O&UtEp7%c6oKDFD@`aQz@*rX;&U&%k~GW zhUF@ZkW)(rD#z6r0d-b6>=k;764L@@nFB%ZUVpyGAxF#pX;77qU9Qs^ZCY&bl%Pel zLmwuUg(oHs-uie@D2Yj+BsyG4^t6)bC?(NDN}_F)L~|>Nc2*MYt0X!W@-m@qrl_weLxDCrYCt@!;@`V1>U_vv#yyAS_KL%(CQk2;6vjEBFVFJZ-p zw;}n92UpkL(NHHe4{D+X1iz|0T#BqH3eRYqA3>Y28o(7n2->2r3{2xX(+wljHa$R$ z9;R`fX$efr8BBav3Dcxv`kEdZn5J~5+eW5Gv}0nL(wWx4w4TAlb(1jV6jQ;Nr&*oJ zHZtv+^E9h7-BX3brPF(NT*C>|oML)xWSZBR?i-n&n3?8vrcE$yW#ozbDPg*zn7%PG zE$B=Sj7&u{(}K>l1ExX-6Zde!bWJgRYh=2vGd(dfJvB33*O{Jzsg%LQ^CV%qshCPe zrbV4;-^jFQW?Iyl%3!KwF!5|ln3fdNGunqrpHIs=(+eZh0exrcyJel}C751iF!7vC zm~JViLt~!uI@3=^rm{Itd7Y^WrdkFQ|0@!vRmD^>&ha&!scvL?Zl2?7I@4P)c^ORn z-%6P7D5fKNVYs`E->{kfxA;b;@9Br0yIXii8#+@9OkoBS|3ed|yNc;Y75wFYm%qf} literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/RegisterRequest.class b/target/classes/com/example/streamflix/model/RegisterRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..bdfd7f2c72ed914bac9e300d1f944ad150c03cb9 GIT binary patch literal 1334 zcmb7ETTfF#5S}fR0xfb!5sODaYysIQj~Jp7e5y!*^wliqEbYR%)ZH!MztTiw;)6fH zA7%XZaM}i}F{bHkXLc^%%s1PgzrOzn+dS#BRR&it7CB9RlahG+tC5*XDbiio-upL_!$+|M?B$Th>II+U2 zBsM5sQwcYhqS26=Uu^M27%qAaJd%MD{K#*j>NcZdAdRn8%S9vco`d3vAkfknFFEyW zQ+jP9HKRh-5ys&RFEe!$%`#dA_H&`F;1?oPK?;WZ0A#gLu{AtRjC$37IqoO+H70!y z-}22ik3Y6OCgPnYx{K5kzMo(|=&ducXV147J?K_$No^bju`l^<61Jll#Rg*$ zNk(IxnDk@GI{e(JG6;LuPRi3tT+7MO<_j}&qdc8-(b%&&bT z_BqY_5Yk-}jgSXfx^@8SC1{M6X$A6T@E0n4!ex$n!E=3sAJZzlT<{J~H-w?J^{-Uu zm3Dt6gckC&kY<>>_R7jTXl`VTp1>;)j&Lxl^u;uG8`EN6Ox6CF#%^O;0;c7^U~*)1 WF^vOLj@DASo^o5nf0ZU06CeBm z{wU*_?Uquh52oqd*_nIKJ@?!@zkdJt3E(x>`;kLW3HdS#C<=_6%7*moGOGFSj!soz z1&Ysgr0teK&+^J)A9~SOLVpH@X97qN7nc|v3<#s&Nc-~p)y9+l)&hLqv2`eC1j=( zRTWs<*0FCcWv!+Xzp>%R()hBS$`4glOMgE&QK2N*KvkI_(dUkbz*37X0a>?~mdSmf zbVF6WhL#?~be$wJ#fTt}g7v4*HrB5`eiE4NEb`#vd1s;bcGRVJp6S`ie5T{-I8Ho! zqC6P{F?ACdx*;ah3FKd~LjuFwI#N3q;gL!XBz1LNi33?bl!Mw)x8?j^_XZC+$T>c+9DjT$ zTu1mdMLP;=Y%q=qeoPwN!CixUyaEOx#?f}jxp#0oa=I0H-$l;cj+{o3Iry5n&JuQm zR?rqI-yyzmlfwh9?%}&^d5mF>yK>VB9?66I0_N%A0N&H~&<;C*%IY_iDr?^`*oGP- z2uLfBN%XOzA>2(Ny=GVjNq|%f$7MLH?Gg8VP9k^YvOdNFSC`eNb*_cVSCl?;)1>XH z0;aoCUc^#Hd5hMSS4`=)nKp}lz~bgoggZ^IYlg*)q2pEH^P~}2?#gNKZ%*@FIjwZ( lbVI!+r$usF`UfXB2Lc%u5b}EeawaB4}Dnyci!bB*tK3Qjh>C&)e(LF5cbNyKRI2l_nY! zAN&LSqm19VVr!e@!H3RtX6MIeXZC*m{_&HD4(Mrxa+G(d;8KxFf@+s?B)vdJJ@0+z zQu$U;>6MPO-4~Q^G}~obpt3_1mu`_O=!I`WPhH7y5GXIPv6A6Mps&2pbXDM;S{d7O zY5l&AdME1RP$j5W9>nHC2kKoH?OM}*Efe+jxc+UIs$AR&%}67ReRo`{v#+cMIze$x zUOgE`R)?ys6Ws~a>nJi-T5Y04Py>ZOpu6-&GSJ=B?fE#evDA@Gydz_ehC#3=Xk)g_ z*{8v5slprd6I5;KNF5Esj*8DD+6k(+j4y+>j5XWW&Vuc0^ncm<@5ung?J0Xaold>c zoSzS3l9`w#zUkkUr6X*%G1o(7`=)y=V;L%|Vh~kO`*1u^7$`C^xa+}mtlZ!qvvO*N zv9I1@V;FhXN}n?TZDC6}ys11S-Uj3ntP%;j3w;m21rl(vmE#4c8cT(C5QXOlxeUvL z7SP4r?v6)d4nDzij()D`3|Ms zXWz(~w&st7aGxURx&87uipK33jc+Vy6K6#MgP9&>GA+(Ctz0~7Ci!c$crajlRt6%$zyrch3LJ{`~d*2LQZ)?L4GlC<|!~GBC`bc*fsz-Q;## ze|>x=nm&W!r-p6#I}C=ZwMGuEKrRb;4X%R5V58|+y13xhxhZtdcLldjOyfegoR%>4 zL%Gr5rqSZQ;n)Yl3rzHwJ9nKE!xXzM3|Q|(tUGpln<9*AP$2hNOSBBW+oHWO4N6eP z3$Az<2+tR-HvxL~KQdUY55N$1V0ro;If;y=`*95>;2OH;rs;eTNWqRM#FPfpFoPLm z9-d<}D0RVMGDd_qr@_2L@q%`{7bX21GiOk!8@6~ASjWPB%a2XmDb<}OHyhkFXdP{) z{ZqqZusMkTZ)MzOptXg+-zjCOTI=GK#Y7vg$EI9On$#Lt(>>vzI<0-~a!dHaMW#IZ zmW3mc^5`U9l=0-CPlGiK{_iR)SC6EXL=H8r z^i(NQ6&*>Q&}6*3NRb7(q0A5aB&SUJck_o%;5NleY;y)P>X)!itq)7sc`1C1!#Luj z!mku{znrn-<&5nrXKYV7WBcJe0*qcQjtjWbab`GYR=xrIBFz?Yq+{H~Fn~_O?`61- zD+V{QhYeRFphxupZTCr@KDI4iEi% z0e?jqRF~xGuMz1VGnFKh4tEtyl}~ust=W{4=^osVm^zxxTtcP86e>PFfQR}p=9!Q@ z8}LBEGnuT>tdeIF9`^BI4^1X{<_7T4dx&|aCC?+JX0u721trg8wPv#&o=~$T)NFYG Q6TQutXYIq1~||`P*_~GWSDI){{NNAp zM;YJQU7%g2i65Bh%z4k5XU~N{e}DZZqPMiuK`ClVQ?o%W)XJ#)f>*g|bN9^LFIVoB3>l1;i=JbOOYW3yVFuC{+&Q(aOVja6!ZwecQen@l zTH;UyWzP)+qm)&`BfB@R?0W9mnu^+GP?owGwMa|aQ1;e{j-+qJvR;Gwl&sCG3Ll|A zE8hWw2GwOp*$brb54b#6*TV*lsB5FZi=RrqSH3cg88l9J7-g!S6q^Xd&k`^0c$I=J zLa<4Lrc{)SFKodBvBM?ABZn1NT8_wDfd%EJ>w1z)3t!2|+*N5g<%;FfvCRuSA9$|G zuSMnuoDV=GeEDS!A6?~k1yM862b`4g1&lhcxZ|^-a8LLZuMq>CltQBBZ6uvhcGq&n z$BI)B{xJuc%A{B1cAoo|I>(pI^4tRIN{IUZ6;!NeLcT{8dvgmlB?{{ZV-Okpa!K1l z9);f%^4u#OaGyH@$pJtcq8&#S@2?9O*BlCjj1>d5QDcFMn?aEMxF2wW!_BD^wu9@o zKvH3?J!fIxs_7485Q_SnE@<+#tx}H~Hw&XKc-HCafMb~gRsj!d+evxWU346I6~8Fn zS*n7AwO%P3^$y1o)G*-+bex=k9g*&Ds+%Xu$pd=_5&EW5DK(i2HG;SN_V8CeH zHr#^&V{qFrj~7vQYQ>tvD<*b^y=Czyv2P*50#;?lB`sn#;@?YjA16i+Xc=q0^LI*r zg)v1p&cj+~lUATo&a2o}Hd+^#eo#93^hoGE7%3=vT4AZYF?tBU7PJmFMvv&RhNm;h zGp6NvLQi!(ok^bY20W^M*F4#f=b4tLC&@FR<$12>=}GcTHsDcxbj{Nj^1RUU^e1_y zwLCBNJpD^u75_8Jh=uu(e%bV;~~!*ZGI+_Jd0YMO?`eQk~~Wdc%pwAG0#-UvxPN9+y4MO C&t1*{ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/UpdateProfileRequest.class b/target/classes/com/example/streamflix/model/UpdateProfileRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..43e625de0aaad03137513ba7607f97cd3e2fc7bb GIT binary patch literal 1486 zcmb7ET~8B16urY&ffj-CC5W)1LJQatc@slIL=#L!QlzhDXs7Lv-JQBS1%8$$8WSJ< z0sbiCneEno^58@7?Cw4H%$a-7^!J~izli7!?M>1U4JT+MMWZyvXy!^>3+@WP%0FCQ z$&z6-_DcE695NctWQ!9tP7?{5Oi_|jjP^>}SbZ% zS)eP*l?C~^Aw%$u_`;Kn()spQ=O$2o^}v$fOVKpVFiMK5EC_?oCuPX$0OxfKO{Zwi ziWpZOz!!nbXt~hvjq+qsh3e9kN4~EONVFd^+RZD?!y8ep%79<*b1gzH+5qm!vJ(8f zRFj@KfPk_LOM$A}YDP<^^2V{k9bIuuO*-}FsTd{V<3q7Db@!d;D?4)D$|hpa1Xip)rZ7G^f!B=QG)kxx_;RAZ!5^GX42% z5#xL>O-+~2L;xNm18BJi)}zjmr@P3bp#uf#F4SD-4iU)mCqZ`d&lE`%wcjSsbt5Rr zx3DDqGRUGiYfMHwcC4eLix5q&VI2)mI>LA4tqP;(cv|!b_8r2l8b-wJAV%|eo58w( zFN+~9(fyE?@eGIeXIE_Y0kSZ4e|FVoA5sDtTvWt0aC58->*)3mVqftvMC*t)ZKH!| zsQ`eu4XlhF(-Xw*;%}7rf@EiLdSLNW+62lLA7Zs<9oyddPKox`_V0vf5gRT=42w-v zyuB6G?93KWqZkwx+!o3X&@|PnX=R|MZQ8*eeVV3vHLXF@`W>2@^N2M~N1ApA_Gz|P q)5bte&j$BtwpY_8G;Q6Xsp*1P(;PGnQ7(!+vQ-itTfh&tdj1!X%O!yT literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/UpdateProgressRequest.class b/target/classes/com/example/streamflix/model/UpdateProgressRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..985e3ca8adace145ff85d834d3c0877f9b927c3d GIT binary patch literal 1076 zcmb7D+invv5FKxGX+zrdLKl)!pl?Ylu@d5~s1o8LA%zgNB|JO3L*lZ#4qk6n{t6@z z5)XU;AB8v$y^v~ygrdxN>^XCeXS_dueg8p3&uOPd9xYU=!oOs%_NeR z`$mrpXW1ct$T>s7jaX*3uQO$pPLDX$X_R5Ndqw>+wLD_8@7PxA)NoXOlpE;~Z>0^# z99;sNhk0sM!UL75Va!j`RNHAHindYoy);$^r!rPh1AG8NNR=wJSt8nNK zoCw;%I6XW&4?jE~*i}Ro60`=tj{hQw4Vp%toBBU9%D~~x!U948ynrw7ek1W23y*HX zx?^n8CT#tD?b9tpf^Jh2b{71Fs-JN9H+ZEG+@dXHIq(6zyH#a(?<-ZWsNI+4ZdFE> zqxb)0!8>{<&GkUg9jwa&fzz~IsA>7SCcjYA-C|A4*EO}EX={!q*Y!(Hjj5)4upZt2 E4e|=!%K!iX literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class b/target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..639769781ba2c9b042f4d6d4b251cd3e01aea955 GIT binary patch literal 770 zcmbVK%We}f6ur)~Nds*YC~qNlNs;PhRe>rY1QOB^P#dy$W^Pj#j~zUos{N}Xfsk16 z0elqVI1LIBuwmhIec$K$`umTs--zf1-EUKcs!ghOs7?(*o2TkbNu%skzL}hI8U!_- zYOBM6plW|GZqXXGn$+&lI&}o?r7o9zuJV~-Sp?52KQ{VY<}PC+-_538Wqedliqz{_ z(9Rz6hmwm}HE8y)GIUIqEfZ&_PXygLEN##^k9DCZhM!sMf{NLNpgzLiD<70RQ$}ZV zvrOR%UTGT&IdoxI8iO(qR?CboW~-%Y&%sjAMxreb%Y4Fqq|i=KH*u*lW94u^c_Z0xMX(0Ahu}YtS0duS0I&tdZ!^T&u#0Ujf{NT|x@T`py>;A91PB z7BISDq%N?txcBG=G(k6M8@QVMiJF&i{+_)4*W_Dt8&NU&05WoJ>^%NV&1LKOcO&OI dqGJ2)t0=l^AF0I&@t@$U3n-D(U0{Xo{RA4}tP}tM literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/AccountRepository.class b/target/classes/com/example/streamflix/repository/AccountRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..abb1e5f18645b279f38087ecffff02c951cda3e5 GIT binary patch literal 558 zcmbV}O-{ow5QWEu5K8&!iZhfAULaOMNFbF(6;z-n7*Z!VICkVrpxtsT794;>Atpp> zBO#=U#mJsnd~fEx&#(6n02sqCgr0!2#1&F4uqcg^6?uh4YV<;SRk})(^GkV^B(Apf zu$qLhEnt}A3rd4FlhemsB^1JrfOF?FS(RSfEcIBZSLdJQ90_G!Vi!6oaSaqONVT0` zFUJLHBVaU&*Yp`RwCJQA)ufG_mZY6U6Y(0iik{s34jVO6z7i0qmeQZ;%p%oZ3Ao>o zcoF~Mhq9z;X5)QvWLcKu&`#XTcpUKfT=g+YN literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/AgeRatingRepository.class b/target/classes/com/example/streamflix/repository/AgeRatingRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..685a76f3649c09c379b6478cb4209e93c4058ad0 GIT binary patch literal 711 zcmbV~&q~8U5XNV#HP%|K|3KfMBDg0{5ut(}BvNU>gSTxvjaxUnVRxhL-KX;41Nczl z#6X)vh2kM>cG&sN%s1cH_s1sy+`wS~H3k<$lX9GjG*if(ww^e>@SFI%Y?84|B(V=Fef z9pbH2H0kk@#hw?MGdStzG)0P!(#e6suGYqr1&k)O&z3gsXccJuEJME7YDj)yutr~< uELS7X1h7fB2Hon^4HPzN!Zy?@A!rqEVTba{zg=kmM)qKz&JD^x0QdpzDBOeq literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/EmployeeRepository.class b/target/classes/com/example/streamflix/repository/EmployeeRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..f643ba98af2d40c6902ec661044426b7bfeefe1d GIT binary patch literal 648 zcmbVKO-lnY5S>)pwbqa7Mf?X9!CdsTA}G{@tW;X?K5eIKYBpK2S?!+vS04NU{wQ(Q z1-GC=@en4NB=5a>Gw&a-ZvfDT{Qw#QPA4{zw7?|QBs1>_lbKcv=_s|C^41ly&v&*U z8h)n&SQpTp;}c4aCYFQooF+bimVh(sVwt5*nRw2d5X)ZH!0SD0y;Bg zrdLHjL8S%sdQpu&@+De$RHbsSv>c>fS%W%c8Movg?DP&xHcu`S(4-2bJyNkjpF0vT zToLg+`oj=1UisRL@PfHtmQ+?Agwen1Uzv4@FtTh)sbX_vb2p)DrMb?7pWQkstrT!L z%(;Yw?p3D7nl6p89!ne9vEyH5XKW`~U!)}KR)yST0c*UPJVt^CA%G2NK$}lZ7Q>;# Xs{vd5sM>AA&R1p^x*S{lbr0YZ?Nsea08PC=U_MXd78+{W@C#C3tI*rbFH$EX?n~=D(shRJRtFa%qjzX5 zgGoDpWiZv0p%N}N$l&xxxIO{6*B`h+%n3p8v0eeoH#S_a0E@%yd(yrj183d*$k2ig-T?pI20Z`( literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/InvitationStatusRepository.class b/target/classes/com/example/streamflix/repository/InvitationStatusRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..1455c14211bc06f807372b254db08b1d9587a094 GIT binary patch literal 682 zcmbVK!Ab)$5S>)pwOX}$^W?Q6n3Jb1f`WpwQt5&RZ)rPSQ?tpEWNY^u{38#3fFC8! zcEJ|3pdP{`ljO~t_vZcM^$h?nVLyNxgYM9z9A_fU6msV+igc{xj9bi%lit{ZU+by# z!b_uL3c2flT?Metpgj>&!IjVn?+qq6^Z_&&oEn>OmszQku@xyk8~emZ!V5mh#4`F# zCKiAUT4Sk47e!TxL8lWf;A3CHg-2B?_fqj*R`L=mBp$cuAI<1al;u2H$DrZngc&qr znP}o}k-@_+A-mBEX;6FVzsnMy5s8;2x|JGX^e-j;Tz#o}Rk>t4@Q)XhZ_ofEqOE cR3}mtwrJI$MUSf8HtftZyU?cCps#xXpIkuQ(*OVf literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/MediaRepository.class b/target/classes/com/example/streamflix/repository/MediaRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..d530c238ca2d44aafbbaf4722615acd95edffcac GIT binary patch literal 951 zcmbVLO>fgc5Pj<=aoeQSlolvoy`h#$tPtwS2&qLNfgFmOst0bHc;jxcz1DgS$&H`H z6%L3qKMFCM8k(X;syKMZv-A08-v0dc?K^;%*z(~qTpNjxf~O)+c%<{BjRu-NS{?XU zWOy>r8JA;`6{+NS(E_l3duzNQUl}x_cJQnSso$vNitjdC0IL$EvjD!Q@!kl=5%P z=I>dDA>4V-(4;tL-=4LfVZMAm42>5$rij%@Z42J&+kIkc@;2c;KREy}KKpP>E6PU#r&1QFIg2!3XeBh|NVJ zDL@E{mF+2JW@r5V@%jb;=P>cX6L6f^O5$8qb&0ZZgsRF)J(q;FZM3trkTGF~geJke zFVKfo0h2;KDOoC$%iH?`6X(O4fFnz}Y--Xb&q!7HZ0S*^$|+gYYRG$4tDaE6IMXIQ zTf}5DUE*abU~fO_6(Vc$@Sp=`PM0#$jSJbtHM$3z#;VnFNR5D??m2>~&W&<2LIF3w z@|{FK#DT_XH;f`YW$l-Nzb=6=`q%b<&>cuFBWZnXOzn&kyx`mf?0(nRVH+fuBVdJR f#2-L#fj+E*2LZoEOv1+|j|W>k-EA9omdN-M4ZgG8 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/ProfileRepository.class b/target/classes/com/example/streamflix/repository/ProfileRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..61d9013bf86ad74e1545fbd3dea795541d7fc469 GIT binary patch literal 640 zcmbVKO-sW-5S^{Iv3|9B5WIR)1ot4GB0>cPHDYPO`!<=xt;ufKOtiiFvpo0%{88ex zg{GjQco=4P*nRKKn|c3ueFK0C*!G|%;5f9I#F5JK6lFmURTiguBpv3q&}7|McAbrN zioJQogJl8jp?Xp>RVI;lgCU0GK|{cab%`u;r%e(&mEp5>k1|rEI+tP)2$-@z_^JPp&6aNJ4 z_SjLK;k_>OAjOU`mK14g*sQ~EMHlQOS{mmls~Pz`T>(oRbzUFA8{okT)S$_?I&0yy Y#!-WHel$C6z~;2lf;Q&{f87H31d#a46951J literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/QualityTypeRepository.class b/target/classes/com/example/streamflix/repository/QualityTypeRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..6021ab877608760f4014a350f3c2b8e9e1c93556 GIT binary patch literal 461 zcmbV}!Ab)$5QZmJyS7^Js;?lJlXzGWJm{e;h^=^^uG1xJHVMhBc3;4U^56sbP~zyp zE>bTZG7}Q`=KueB|9E`^fGe0}Fi>z-`bN_Vn_x*Nj)cw9nw5?ed@}4~r*B)dhP%5i z(Cp_mgKY%|H9n!X=qi1)sHx-(h6+x7taJ*|xM~@(p=Tc-^#d~MI$$3>4Y&p>7|l)P zklRQKUW(u7y!aRPy!gW)ITllWb9rxO;a!zq2Y+lB(cv)(Wzr zdy~w<(xr2rk&Smr!SP1%i7Aq2$yKj&$R$AqTXJ?}O(3^S>Hv0O^xem>Cw(aY`v9MU C#+Ud2 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/ReferralRepository.class b/target/classes/com/example/streamflix/repository/ReferralRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..a5eee1682a101888cf5b1eab1575d92b31d074e0 GIT binary patch literal 1241 zcmbVMO>Yx15FLlMA?2%-eo)}V0kuNH7wBaxq*SF+s->+`i8Jo*Wb5FyE!$g~GyjSM zKY$-qb&{poRTH4i!SZ^>p7-`?-u%A&`3nF(!K)@T7`#tR&hbLzRw4Jnp~!nmUT}xj zco~cv@GkbyIib4aP!r}DEcL~?;7aI>AD#9w2~AjF@WHr@d+VgmdQRl{-MDW&6+!U6 z74@nIR#XKUZ1tp0cLyDPE(5xDl9)n=PTJOK8NzXh!S-fcVaG;i(arN#87P8O{Ky7r zv``UI{*2+wq{m{&j9@&4-WT-@rJ`&Hh>0&T= zJ%L^S(^&dIujBt-nw?BtlsEsxsmXqPtBZFcwOuN-C;g~V+SV$G>Ap}Nr?gDRADVDj zD8*oTbBba+6Tv%?`B7`X(h5;LlMQZ-S_2&Q|FCHmPfXzw+?R@)bbZpa?}naXu+b%B z$sE5(FHaSzrA-h+hmaZGPNnV1DQLpLB8`IUsSuIZ8Qh~aPt#B3v}Zp Fz!lPXh(iDX literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/RoleRepository.class b/target/classes/com/example/streamflix/repository/RoleRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..fdeb0ae759e8b2d105472ccdd79a4685a29e25a0 GIT binary patch literal 630 zcmbVKO-}+b5S=2hf?ud7{{hCtdhxWzL=!!j6$~Wa3k++q+itqE2q*uS2Y-M+$~em= zf{7-?Lo=O8=e;*?`tkYp4gfcB5I{x1+0Z2tXDUez%8Wd!WNh?IdQ4rW$@#hLIfK3L zG=Qpr)a~r>UM2YA{IeV>QgV{2Rm8hgEdd2I$J&mr z=N)bl&~8Ud@IDn_;c;2YNgLTsNjs}d$SUrUp4{j`6=Z}y6Hv7akVap}R#EO5u(KxM zMf8UiXh~CPLwL!wuPXw}>BHz>ldr|NLKZppqEOK6yW9`)RvVu0@aKL`3-<&Z^>UV- z;G@p;z~Gg&j#S|sn{@K4+>9MWNAncraycQdRlo+H8h;wWs}EokD$wAp#zHu3@u|Qz UKPqoKu)C=2L5pLZTlWFJ0D*wVJpcdz literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/SeasonRepository.class b/target/classes/com/example/streamflix/repository/SeasonRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..e8aad4b75fccd14db9ea7524cc0979844146ec62 GIT binary patch literal 567 zcmbVK%TB{E5L_2ZTAsb|0dRr~mXJ6FaX_LHRiKD`!M521hs2K74zx#pivu6PMV^d0+U|AI;tz*J6E!0FZRmLi3cq(H;YxM0G z5WuQ{UXBka3)Gn$j&d5i0M-PY7|x`vSm`WfEa}nky-d)d%qv{x98|a<6tI^not#f& zVny~c8FE70tc~z>U5>or6&B*91Je&iyQ-zF_b)mO)MefcdH`u5fv7!W+I2p;qx3qlvXPuJ;^bvFsgthPsA%YzT#Ly4)N zEk!RLCKEF7&Hw*-|9E`^aE*fu1BJ87b(-hY`IdE*z|_rJGuMH=i$+{n=#qosIQe|&=cvFY_0F64Ofz3sBrE=t)mad*0VqzKfCa#A4o_yp4OSSo>oAGQE6&R(g#*} zDK^o0@h|K}@rO6IV&vObzP7G#w$3keg>mWnQ1Ojvlc1x`08WR71TqRWJtdG@|$^ke|ZIfE7x@)Wss21V9S<%k>1wUthrG4B*T z8yT59sa5+c?ZGmG#y|`OPlZZ&t2e;Nc~E0;s!hUeZlp^3Mr8P;%_EP66MT@1dF7j2 z%mf+i_oa$2$3Yb7Lb+xfsJLl^$jKpgO@Ry!4#Qc&P^-j0nkEV-Q{I|56sb?XZqYsH z_)e7N9U9A^E23oyCz&m1P^{6@ZGaB~@ZBu{ literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class b/target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..e1438c5b11febd1ff3f9de0036e46a4c0645037f GIT binary patch literal 592 zcmbV~zfQw25XR4;CA9qO8?crIPfV#8kYGT8BErOWO`PE1*pcHv`wYAh0}sGMAufqT zBcV#baI)_#|L*g5{`h=*2Y?Hh_~0>^#irn5iAAXdubdTFq)INi6{V@9Gj_$JdQrtz zmQEUdFNMAPKJsC}V3OlAa)mnM^F=OV=fgIGQ)4q;l~(F3wOEK3W1o0}4tZW;pMF*1 z8pvRrN}ZgqZs-bw=`>u!BiF!!s)rmR$-D_9(jDJ{+`_!!J!>zlQ(- literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/repository/VerificationTokenRepository.class b/target/classes/com/example/streamflix/repository/VerificationTokenRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..dda4ba173ab44d519c6614f848c076dcda6bf969 GIT binary patch literal 525 zcmbV}O-{o=423;~5=!|y0!!F1D^>v(NM(V7Km@Tji4!JGW}=A;?HxD~3l6}c5KfS2 z7Jvkc8OyTt-uLXc_m@`yn7~c|0|h6Et2E8As!P%hd%`L!&0Kq`U1Qk!g}x(iGLs-1 zXK&q&%1wNuK(dPQDi()QZU(%mg+ILYT-fj12b7R{0(m?jF-d@F1^MUg~>MxhTLXRFp5oXk!PM1 zJonr_vxL#VavcAevF98)NqH*N8?l@Dgw9PVY&iv_j&VTav^XvTs01B|@K#GA|RT0Owh>{Swu^L5G3uT*YOq7vU=?;IA z*phKj)+*3w%)?M|V1YrdE}DXeA`W=zxsFxqz#@Zlr2}pfP4{YAMA%aLmHWa9UQfh0 z{5}yQAcNzYjQxCvYSmqpF^Y1NXH5qhV#pJ!z~< zjTOx{9|HBnF2wQ+(Eq5=E9lnpTG1Mw(*nz!Ib&OJ5lx(Y8V>1*;^j!TOJsP4%RW Wp`0Umb5h}>Dh@mV4~3YN zEn}r(zve#Y<}?iu9s%! zv`H~$sGqzb@L!0(wXmcp=BbsgqTIyF&lQ{qxG8n^l#OceuU!1T^payQ7sYK1ADfO1 zX^#UXVBwP-uNPtB&4Vxj3Y<5$-vkJfB5BTDP zR(UHQ`~W}7vS+g-F`ANxol9?@)7__ge*XIY13(3LvPdCq!mu!eVTRl*{+in^_Zs$- zomaw948wP&C)GWMbfLJN!3Z)YvKEfPVpw(jrY-h)v*ijqRDs~lU03egp>U!=ssp#2ln96@OFAX}Whj!WM`{Og zjcQ;-bTnU55Yx zq8P?ynAEf7dzJl`40N~iy;jmS9)aUE#htxP-b%EcmSLH^_0)4W?1j_dR6N}UqRQ6% zC~(Av)IFN|>tU_vamO6J&lDIg(3jV5lwNs;&O=xr`z5+<(4L|_TlxxCTKWw0Bi&M1 zq;(YOcr`Fas2mD(n`(Qbh!Wih7qc=f(RqkOpp9V}muc1XHtkIRDt&_afevlv;n+AH zGg@tjgppx|K<(UC60q?N$NRA}Jy;{bUWxfv2}MtuR-M4rgkwY}@CD=jJZF1&MmjlM z9fUvGkDvc1{u*YJ_{&NBl<~f=`o$h(suTHo2U$swxpwAf-oiBA!ANVKbljh|ipxDb sxrFBi`3xa7dS9!5vqL#JnbjVWZZfwz$UYg*_ZM@s2j7+Pc3kq>Z@{!IlgeJnz1{FaFy91He3fFwuiv1AP|yF~BhT zly7s}$~bELvMPrIEWJ%G+ay@&ws$A)FFPk6~PgyvP zF~Vm<5M0;(LA6}-9j$B^YSpUeiXdy^Ele1A+rk;V!;pwPA96$C$za|K#p-4tpEZ3% zLhT0Yk+!gyHE@>UVi#nDuUxg;K-NH#JaiEag;NW(aIO|AS!p5G#5r8hRrDT1Cf$w5 z!>+QEMFSTZ?#N)n4yzjGa=niJbHut{%B+lfFX5eHAiX>dsIES)FeaDNN~vE;F3&q(ZS@VYv0` zu^&AO(_P^@w(cj341GCS5)31G*B48*%DM)twdoQ7Gu_xB zT#rAtS?UmrLT$>@1FnfuA|QJSPx6u`Wp1}Y4a0IL$nviZ7J2R$@!?FgpG^j%-cG0h zx@C8y_882A!l5M2c)YS+;;HsrQdb$C{72bzEL+OWEXZ2mhy_=3ceZQWyrO#p!^8?T zRktFFZs-!bv%W7C*G+};NO#7Vm=aR$R)t-NN!C=(NvWu7^J>g#IEs0OQ|S)~!p=v$Q3kxso%kLZqRMQ}Ubb>qSBew9%l(ok!c<<$ty(qPURNzEhZI){$X4-x zzZttcUbhwhy5$`Ayn?GlmLPd(qKHQZzSf;?jUm+rN!-EW=C{xQ<;VXYhI;3G+yYzt zifCM(QGY#}5`%Tm_eGHNco>S%z_)Z&wdK@_F$3SxX*ph$9xySzI8XK4LpQ_+^h@aX z3*Ftl^xY4e*00iRfyN#h&CDKHX6jcMKhvxS*XaK+$pWM3qgTct>3>MGu~-_{@e$2P zFS1JS82av~OPdCUPw*+JYV&0pwUK1z7Z_8&2@I69go~uv1H^#oBbjR?iOZg)N*q)=IECo$BSh5HlcVAtF&_;-_W{A LUytzw-(%u$GA1Dc literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/service/AgeRatingService.class b/target/classes/com/example/streamflix/service/AgeRatingService.class new file mode 100644 index 0000000000000000000000000000000000000000..174f0911d6fedbdcda67b03f4cd1a1e004ffb193 GIT binary patch literal 2552 zcmcImZBrXn6n-ucwk%r`Q=p=?q%|!8v|FrIN@^7%STrq_AhlMlH_1)dvf0h--th8I z{24l&3&bIy6r%kE$Q9Q*-b8J3Pd^lKO}kisBC z<|*Idrp=wExmJBDYLa2_w&hsz4nu!odUFV8Fr+~@Fbspithp^y?D1CH7N##f!CMX6 z+B1FO?N~KoE;hwFmzLA4gaU?h{7|$m+OBU&*V||Ku-pU4J5q|V;)}&$j3BKcW8gJp z8E(E33d3;2a_UR_WnL9FL!nTPS*S>l+%HatqK>p|bFD2c*Wq?COB9Y87{fS2inrS~ zMKW1Psx~@KO(~lz(Wa>5bxde@!@!%kz>p0rb-@zRE#79h5xEgOJPDLElkkY)qJc@| z2&E|~dvh-d<_Ngbp#Uu51s+~D@HX;9jO#7izS!9E+}#+f+oPk4kw=N*DQdXNFcPy_ zcAch%0>kB4wrbm=$?Zk2*=Y$!F7MSuKya2SbH>0d-eEZ3;@hI+IyEjIT5^kazT}Sd z8Lsw@FK{HvpN=V9Q#EcfjGcC6lVPFoid8sGmj_C2T`;7}mLu+WT2YpNY=sh8~-1-CV@s^OnBbc+Qd{^TF&Yu=wqNY_A2q|TU z^`1>Uis)h(w|T2t=lL$^{B?%;mw@+5CmGZ$ZpW*M6-)6lc6>HmQzgMLvEFf{)e@VQ zZxLaOj^j!iA(V0I%(Ck>O~0*LvZ3PNb-itxJHivL+;0mr8Z(NfCD)ZSk$5}g%g>;6 ztXjeWci7s=vQ4M3tYZbAX}D`(6`wOq^%yNwv6kGAhZE6CeYaYt#2-$A06E}1gEB+0 zXPF(x>T)dtxX&=!1sp|@9>fEkhVz#OinyIYMFpGe$ewvQgRdBJN7={`-?@g{aq78U zYPFn0G%w^B;Xl;zH6CgB#=v8I+ttICv*T_HvmDe65zZUD7EbnID$@A$cd~e*OJRQQ za;I(!KVNp;?M}ORvLq9du8SL#_oVuNe;As4@!ceY5ps#!cEysSNR)i9;|YGyz}1|p zGUVg27HNkK6^^czLsEYH|7nOC`Ei1_Q|kSANDRiR;|Q;0bKe)f25QtRCt^A+V;Y+D z>Ups$eM^Mt#U-lLK6-mnm#7a3+V#^uh3h1pqtgnl>bXAi3~XlhS7<-esSoeb{|M=- z{{f^igbZ%bX*|@%`?yIbx)-1_d_emYy~b#vj||L{nu32qtAd}L`32hSbDZtN0nP!x z;r#3Y-cp-Of8xqdq!QZ9k~NS<8l&|79>Yc2$95-=>`vkq7RXd&_ZE=RRX>tQ*_{u< zB-dgzHc%rCopyCJu!V_#0sTSTaR2}S literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/service/ContentAccessService.class b/target/classes/com/example/streamflix/service/ContentAccessService.class new file mode 100644 index 0000000000000000000000000000000000000000..c6ce7dfb5eacc64e8d8994b8fe26eebba9a1ecc5 GIT binary patch literal 3330 zcmb7G+fx%)9R5z&WJBC2qEPXUS3n zJ~-3z(ixjh`_So=o#|u$9(DTdW)p)oq=nf%mz>|_`_Autd;WU)+wTA_Y$FK)`RqWHS zADs<>l6k9Wau>sNGoBS_CBj#GJYkuO=}kC@Ln;nyID(@BXX>QZa1R9bE*NI^(rOTN zJUeV=$DQGfYb?t-yGQ^>x?T?+WK(pFTx!}c2t7w?6X;fv)X;-o=IkfDZi#?uIG6JU zmsP{MyyID%+4lqO%xOk=Aey=LL@a_Y}$@<6af$D>sUIj`b_h+c1S->D+2TGWrEvR57gK5=I0P z8%M9PETf*%^W3}jgwDD)PYgXraaqF^Tou^D90S`4xh!)6ZMEASSR`1%<9J)eH4PIR zXH(2ODgW?PVJjw6iD3%UDz0m|;n~+nRyl;gCfB-IC`fxmcl_0}8s@xH7hTewr4cJD zqqrpynjQ6A-PZ6f?oj9C1G4M9mUU1#i91oePm{6HC_ZF~-5eVgINw!IEE=Abbb<*e zUMl2m!Z77jF+VTuIep$!rPc{6qvvkvw&9I~NyS|4Qv8YX0GA9Cz%wt1{UQg z0`VXQPl&IRVw^SL`=}^qeAd-7_b2s2KqjFFMKxT3ezOtuK-M;Z{(8Zd3D7GV+=zfm z0Pc*kHfQ`Z4^^X01D4)&yet z8dYkoQTO1uv%FuesK~OWQeydo@?*c#Gb-_7$0%=E=O0`dcViIe}U*u{tWdejzZA+-i#)` zx6q;m&Ai&obJSj5jSR9J(a!fOAlVPI@Yx6~V3Eu2vj3EAnC(FF8PuLX5pG&bmJol2 zSWgMts^b#chr9{4{mPy;IB z<#wtZs+!vXmS8X)hQ~eJXDSEKh8*(rnIsn`dkQT69!Df9;^GB%3URRvra44w(9Q!? zz=JX%af)q}bMh6S?x#=*YWJ@=5W-Y%30;ppoa`L?1AQf&8dN%zXV}!ClyJs}?Dt9- z8Vq-Lgi9Fhk8~6LnAcO=)V9d?HSiT5exJW}Cm6bq@J|zdKS>zi_70*KX}-_#{{px3 zBEKrbxQ!8?l9_U{RZ1jmI6fs3E{X)R1CvnEfiBB7uDW)1Mb3KmlFS+tE1oN$&E~E2oOA?s^z~0RSTh>TC*s(G zts1uJ*pB-Iw!ZyRn&?VzENxmDDi{j!`_7`H>^#U9mJVNHv$2n8wZ&%53KdVD>wP6a=8Xnbg4(}1zR% zz(fc#-;e2}on263ayp&@u}8U7;79`$Rqmo(*KOoLWt^uYk7WTpG;Ta&TIre)lntwd zpd8*+6f}H5$J5HBhiqrTaC54t%{yjBuGr2Jb3IJMl_Tl ziW+jE^^-b2g--`1rBZAJI!lS=7k|GHswx^)m(S_=Jib6gvvQsW3Tz4yRo3z{~bg5lw_%f-Y9AD4QI9|k8HGECSOL$peS1qAfUDpX? zD_WM0HD3ry)3E?(z1#*>zE%{;{35=g!ja%nR)-qAny1MzN_JTI=+t|2yA&< zv}|3zi&T-7bET{>sslZ?XgWz!KxR{odeX#-OLafOD;j>R<0tqjqf)Pq>`|$9&Mc&D zle}18K0_`M=wm$Ou!qiAt~HyMBiXFYw#*jA{f8zSF@9E>S=X3nsj!`bVU{--GiEw3 z6Li?JdLYmCJk6%Jj~T)edX42Vc$eiac=X`#1O6W;o5onA1==UAtenngW~Fo9Bp$MJ z(oUJ_8Pl=UdpOwSEvkk;IC<9{n7G(Vvy%N+`UxX2TqG!sz&STO1Oju%AkyN z)UGD|Q{jQCJ(w$>r%3blh7@uoV-BxhdaO!nJ*qHp$7xT;l=K$uIVJ&%isyH@)+JdO zEqIc?Y4*>LRdu~|o>i$?ucEpG`IZaZTh}E6wokW@?G2+K@f5q@8zC^0cM+)z0Kk2B)-rst^hEHC9FsOWR}E*g*aB zT>?EU#1VdfMEKA10t9}>w>a;w^7nHWzu*LqiuDt}L@NjRslw~8xX0jK@ZLOd6XMst zNOe`yKqs#Ass{`38@$Fhf#2c=rK`>V;#W%(?@!)>cIhVCriVIXdyCMcxVVPSp(5^$ zV6ccC5!}X>TiAVR4SQ~4popRBk%8B+-}Hx%_`?G?aP&Hk7jY_zBF+RjqfoF05aV?l zZP-q5JNW6clTW*NSnt76c8zKD;u7}Y3C=0#0|C4cI)UP*?=AHd#~RIOfD*Ac}n zu2-f8BfsOw?>W*I;TGjuO@#QP|%%Ik<)=)PjhQSA86U z=sM8v4sYObiWKU!z&W+^1n+8Bd|>bf=8E8Hw<0iz%TSfDc-go({@@#@ zU^ffCs9@d3A49n_v?$84E?@4{-p*KOrU*OAC7FtX0P%hj;DE1WSFoq7q$^a?f{pas zpD25sxKd9fnMoDaT)b~W^|m!UPif~M%JTogn%&^=7uN2;RN zQ`X%R>ZVY*c}WM{eDk6q~CX9Z)w zoBaLE4SZIi31SzxI#K27?lO|@dRIr(X*Sfb#JkeayN2f!n{VSwMSLYfhkc#Bqlj;- y#~-TTSQXE2r8O%0O*n~mjG&vJR9i7dR-dTC&|AjP+X%zoefakpYZqy)*0y%nw!XD}PuutXotZm#vP^>S^WGo#&Y78W ze&@G-f4_6iy!elyr-*1ZzaOO>%GD@Or+g}4D!$OzZdAvOL}&H-trwbY4pYHNR>E>l zVai>&sHKo1RH#u@ry|msD%$MsYICR2-4i#fQ%=$}x;x_5&gztz+-|j*)lFu*Wi-1t zOjEk$cB9#2r!2=#_Awpb7(yWFcXDZD2h|7*lXaRx#Y|Isl6Hp`H?yyqmK_4k4I_ss zswFy2qr;e{8J*@v!?6;bLAoa#0^Qot0~O^NI?bfR!LQk4rR?^P%9ZfN&BB#53t8cl=Fmjk#8G2K;w<0rFr+HMybXc36 zaLj~L+ty~LQa-&<8)fL#k1pV7X2eRLL|cWw!Mu zEvIj@W5rW&?J#P+cwgNd>|7J2#Z;})5}l5vwzM5&(E z=yV#bWr~W@T|L8EkYjubUROwTI-Q|WqfSj?<_YHYC&m>{2&q? z6BI}JaEN~~t<-2M)4Y&LJaeiEqSQ{NMjbkJQdf8wjE5$G5G$2|nRwjZVImyXFPy*? zJZGwzpqpZ{X3>@iT?nmtv$#far;2sh$zirwkct7(Vo8;DqDXOZow_LjT-w@e#oHCZ zRgTTHc_J3Qxx+Z@M*4}nNp+R!7{EG^g=-tsqtiu{#EkIQ)jJ(CiMSVxI4l$Bsx0t) zBT5eSYP4Oa9kg>;-Qad$(yY|#?j8qO9)-ge5$R%`K0%)Z6qt#m)z)>U<#b8jSYpG73;H-KW)<6x9xJF_*5^=^DD0DbF!e07fTNl8iqp6J?T6n4yNl*DzG} z!FEFu6_un72y;DsPNN%i+D$hy&CH_hFn%Ew>Lu8g>BKS7HcK%Wro(7J<~+Jdr<>^u zO!)@j9yV_LXhH{x`_ioOa)=V`x59b4jUJ{IV>0Ux2$CDlYm{!MFKhG_o$jDJ zGYA^AW4#@Zqt3OHfZhVf7SCXsJ(k7&o{GP(>hv|b8!p!75@1>q!Y-XawS&Jkk)-V-A@m|1}RA}s9Q%vZHsh%NT+W~U`G(xn=B`8%HX{^ zJuJW#9L67S681*9-&~u|1H;t5u=rrL^8HG4jw^`k2w;6GH0o>R_13EoUgBS!# z!v-Dh@iSt~BE{4`ot~g4nRHC$7iK8CFj0s`ynygKWAgp+}k} z3QyT?#Cy&49b%}{gYKZRVw7H^A8Pa?oqkL|VJZ(1CIcaXf*T7f=&;G&ZkZmUGu4fD zg)p_cjwbXJiH&Be$4;cgvDQzsi`+on~_q_B=@M==2-QFY^auJQR%Zc2yX(IZX7$F110SS!b$U+}Qv~I&#~e`q z4q@ysI{j4`D+0Cb^Mmv+I{mBk7h}~KZQI(Dc8{x>0(w&h{)bNgDFY>rG94h3Mt?`K zovr-V^>%MHlWXi`w*j~Gs=q?|Z~8!^4|Vzn{d0Ia?t=-sfum*;lTUY*Y1t@?t%L&= zyGcv~3!KBb8t3V(3c_h>#^D{|!#y%Ap3qo>8)upF8HsWM7wQ}pNu|tg!muZ(12S`H*_ZKq*)If8)3?fbD=QV!?ScgQYv~eCDJlh#SQHdo{bV8 zYL0npc9d%-D(miE@V3>A88L(+dd4i2_7RThJcsAP>n+4i((yQAIx{pcjtTHc^C5BO zp`S;%Cc?y?@_yP()K2GQ3c_~VhPzD9q4zsUu0u2r4ZzN^!_rbUs@!Lh6?QgbK-X zbv`cx%Pm&BX#-j#d;ws=b*vFa(-=$9vhicl3jsDnxrQj$;I$D(F)+gm(@{jN3AYWr zkx}jvs$I0Vh`aeXjj&S<|Uuul)-Y~ez^!0W>upO4&0S;v!#vp)!|vHcqA1&EI6{rlWMYS{eS?uS zx=qx%AfYO8$w+=Bz9EQKs?rbCv)+DKh>MS%q*3pvoF^;3;*qpuP2!$y$m)26ENkNB|c{Ks&|E;KHA}V)C_Fbl>`WR*bEliP?0zp{e|x#j%}5Riqee9 zCn*=%WY??75Nzt+)r?ox!!AbVkPLh8VY(Vsfr$S}DRT(^C*8z6z!KEwC;}ObhH z7@?W&G26sZ;h`+X@QA_8{J#mjWY4pjC^INRM!SQ&rg*h0Y6So!&FBErWh6w zWmvj|zdUxlm*8;uI6E%kITrjJp5;e5fENK}m@w=Wri(V)y~#Fn4Nh&5n$Xa-(J39n*ci72oIC#dw%$6lUbpV{}Y}-jW zNu$R_=~OZA8@UJItq7(R@RL%NKc(|y{75lBf(OsYgKz8nsJi=(+2$RyP7< zv0%$$5!R{hsot?{$r6t6k5G}0(+g)kofh`&(Gwd;M>Qm{-(aa~b9KwMtYPg;3pKTRsH>ZhsYrFs1{y}UHP zpGwP13;OA(@=~pz=9HI4`e}Z7X<+ zBZ_i7t&ndE@KHb&&QBNfF7)nj=jO}#3fzGUB_perNGX!9kk3$Xt|TpyG*7ie)_m0x zX$w?K<2Mh#1^89q$NX8o%2kf+6DWea z%O9gfX{uU>>!iv-IzESn=tQD4o%}S_ugojWOVjE!HB_Z(-EA~!A8l$aX&Ioi_M^X6 zF-T`i{|mhS#>#v=o>Q7vnxCe&Om|Clmq*)H7RbduK&9%MrqpdT8T$9``brD-g3nx~ z^yM@O?WGWQIV3s((yW9bYM`o9aJYXe_S@^|T%7fH;?Bl_{bh6pj>gt0<*#*R^Oe7w zujXsu97MbMTC{RN^(wv&tz6K(h_C0*LHX;cmT%zQP`*ub_(uLbK9sr%V`ftBhqOYY z^%~WGK#MgpKA`y;c1J|_g*kjRRC_I&%s2BFz`g5yxyqcKo_?D|ztES`@24{QT?e&_ zb}Pry?jU`dX^<|%89~($T}5bLmqX=4^m%E1F$ae+w++%=IrKE`%Brng`Q2s+cqSM> zi)Pan_{rH&;(1We`P2eyolO_QKhIODDtFoRRn@5!grgID2OtW{0fh#opj@SlBH|Be zxvLDNfP5C=`HN6M4u1*#ahmN#;I+6D?_Rc#_OuSrH<~Id(sWOn?v?9buS_y#H575*K)fl3mi|yW4 zcfU^4?^NSYY5KFgem~Ri{UQ1{>G$vV(7bhz(0^49iSMPE@1bdOUzO&_5Cc^3=wUEG zv>B%Qo4R&!ffoUF3JfoS47&i0m&2S_z+0~b$XpF`UV|984!(aq#@>w4yQ!6KBopl} zc!)(e(N3J(if$wtBGkmUD1R@ZdcKu!gV|g0W#8@mWoXW#DQJB~>9Eh&VV}~RaNfsv z@SRYY8x8p%Qk8OmgMX(5bsGI`y~YJoGSP4&oZ$|PVZMvM>M1!2%Hjxp70=P+eXO@m zXD^X`XQ_&BDzQ{_CUUbvYg#z1QtDPj%Ae(fp=+sLXx3DEqVmPd(bz zYA^t*a*&Ue_%O2Zah%FDBL3~jHg^DP?*vHR1!%b&U~@0k(tQAv`w=}4&;|4mZKJ(P ziA&)nr@=5^13=^ecrq{} zyDGvtqYD@<@IMX0EgR(JIdrRtbRyJMGr%V+eVmf!k2US%`qlwny{<|+rg=?OWtvZu zo72;LhFlvT=M8DzEX~$5pHr2U-^GVCTx$SHVhDNV3E;z%ko_s({(i{+G&RyQ_+sc; zCAW(YExwE`o{VKUN7=()hb}6qoWB9jcJU#Xc-C+kac1Djh%;@4HZ^0*c>V|(4anG* zk+EH5Gy{UTsPH@lc>#jF1VLT~E3ZJ1S0Tu20p@Fb=4*WBXZp-f;d}YMQ08&g<1r5b z74vmtGv6_S`7SVjVTSo_7&EY-3ECvLP|qd zlJf&|MEyCUDs74$qH4Ar=m-l-kLo-n8&Q$yXc~XgQxFHpo`nHemySK)(_;)c6$7xc z9eco7Gn_zq+^>X5^MET3KMw33MEe{n;0HrfV}@KqJ@E#eaq#rOODpy+?^%jWy7 zE*q#p5H%V?I%=T{G{@2-E#^LBB$G5-vbJR!BSXo=zLu88n{gN0&bUB$>9Vc@t-8{w;kH^V#u5#u>o@~T1y+^W$to~llCK1ULy1)Uf>F7} z(xtu~9OAaQn<{NLW$Z*cWhC3eWPPKKDP%agdQ(O9SnM^WaS_EYU zS~RTGaVB0$W0WODLIrBm*7{`D?B8Lf_X?cpDpN+F9}HJj4QB}~DFcFp)E;ed0jR}l zoTFiljmg zZ>lWOso?^Fg$18?CX?o{k!-hyb0aio{l1u~91_HZG@;wrml(;7L{qtuLDPz+hoWwl z$VQn^Vmr)ubSP~_?H!41)Nn#Jgm!F@DeEGE2EXckw#9<Q*ROPRX#BGZU1-)laU; z0UL%uqlRvQGs>*@{Nk&6>6)|uVV`aq(6;4^yle*c#A{JS0>1g)mRc{F=>>>ZPRfXUMUcg zX~EH-`Gk=+4VJE{A*{mXI=0I!R7DY;@em9QYKZBGgGxE%Q~OJ@vPhY`bB4@!MQrfY z-KK$aPA(AA%X>a1CRok3rgKrlGNY-q9W|KE(iSNlkJ@R;J2RVgIOwu7gduDQqY{ll zj7+&Wmx&YJ35$D>){&6}*0QGEV~m(#1RV6TI&9E z4QeT;bA;t=iJNpph2(!W>gcr9K>u$}K^z#a&rT5j=rf%D1(I+Bhj zhDI6bUM0*Hg4_jI!_~~}Q!}Pe4?{SBH^`WJqd;Ohq}({1BBpX9S#}+sa{Z$jvqW6U zG;~Hw3P>IPr}?gB~((48+F_yOM;u+T@yRwVRlN~ z;pQpb;d*>3cgQ^$;Wd(@2XL#7cj8@6gu7eKB*H!5B|R?>;&vw40xHpX+RUop8#I#) zYZ-Vd>t~#h!b9>iVj1>GH#829x(T);SrTB@7#f4P?n-27B{ zEpSqI`=#4A_iozI*|olJ`{wn%+uJ)jHf`zY7Z9E7u{zSJtZk(10VA0+E7?+1389tc zToD6QKU{EsSqAzRxe6ZVA}<#l-?9R(Ggzk@950n_k^Lr12n$C?I?fzDyDO10dsI#D zHwKe@6IP{Wz_1c>?7j?04-2%ED-ETO5^X%Hbeqi3@y&+C+-aK@h1A8^FxY0EkCwl7 zO|FHi&zWg$ET^Aar9EQP{1o+(#`?;Q)0Hq2GYLCq3fZ;R`=%&e*wZH5JS|W&n6T^} zvM@8zIx{EKYk_&PN=XeS_S_l^mOK)%eVImamt7K2a~~Tywok4t1k}|~Ia)P=R4Z=+ z<9#~Wp0$*&WtBENk#S{3dWjPo zp@Bp;L0Yz_m{6D~7~%r0U1@8$C7Y2=<&fmr-n6x=#kP!8)^N@VMt*urf8jMx8r+DT zOZR92%=|ehuQn&u^d*KOJYL9N0#{6$Rh}a=EB58rs{>kL zbGmg_rl@ydw^e?1iQU=u?vb^H^|c^~wX(dB$tpAy_)t)dobH(f%*3PK^sMvxqBa>& z^`3vcw*reM?ct+{Lq)nwFW4q|7D*Z-gK=Z=L~)DHl&28?vANuNlFF17vQpqwcY~6^ zEarrH?t~&L`9FB7sVeUOP!u9kVNX=?2kvp=Bqe0D`ehXrF~6+h50n+^OXsYZxq(+~ zysvTt#Jy}=ABkGwX*VYukdU=p5=?b$rpdu@k=%OryTuD$1l|AKXUYfj-RX1i*oc|9nZ^=5FCjL z9Y0f}fE-on_^BFcaun3@oEnAXs8+{M)TmC5cm?=lHL4GZIU=lyxw<$(G!)%FBvO0Q zyLir3?4oJL5OcS>daPyRP#8VwC#}24L1LqkiYLwN;;wXhS1wcHAmC$n(ZY%6hM9kK zw)Af`d96Pr+^-!*GTE2lS)S3=nM#>fN7Be<&8#MP@3^GUkrW9lHcu1t87fmxJZWz{dQQPnRmJ=HHSDET`d_u+otuDqYm75D%zTbb?f0G}V^ zn_Ku>&EII_A&AE12cb1K9YkGY^FhpMj06s%p)pc*5Rr$}>JRe&r}6{^Eac&N5f3Me zF$+s5^K?Xbw|E+s;zRtp&XI`^<0BkVy3$psC#kKpAS$nYd7PI|QsaPnk~&x6qkL|t z;v&#Ea3VMbym18t9>T}CjLM6RR3*fB`x_rdWE?MPt>Qynr0OshRp1dU1IE$(I99b* z^SvhDpIw1gEutL;9TvO`bw|*?HQaFs>mTEcIZcPLQ7*jLUD(wcJc6FBk?Kfrd2=Li z2%B4Ll%$%&=&Qiv=#h#BAYo}#nluxV7DBO-j#|Z=%(HlHcs4q)2EBaRhBoZtwVs4% z$k99x(SCd!57RnYgM-vtLbM-;_)bE!AD_Ts?o@-_)Nh;+Ex=YB!6ymy3UuL9c$8Rd zBb1L(tsr-qjZfnK#2R;lZg+!AqPo0L|K&OUiRtie~6Rzdnu>Q})P^A(nr3b|T{ zV_2<_>rt>(39gXvd<|cBdAXLqLB1PvwO729_$c*vdFr=TN2(nIB2_*MlG1{vqo|2g z)38+YQ7oXGIn6FPcjw8ek(k;ztVrM}LVOO4V~@lsQthXw14Aoa4)l`{1B|sxNR(|f z@G`XGaz^WRT*wQ*jTq#Y7)No^bBH7whJ~F9rd}Gfo#c`9lJP&52W-p*Oumy8mQEj2 z6qdvtQxulO-RBZF%AF)Nt6d@njxi)95qoMD)Otjei2BJ`OvG>Cn-0<{6G)!4yGR%D zWYhz?6i@a`lqFik^)(etDjs2+{FbJ@yGa;}?$41hdq|j3l6k+6H0^l|+ViAYn z{u(4Ju9ZpaKp{9BTVG4>Tt!xlkrh{y71xjz*D_qLQ?|+^OHJ~&4&?nkfT!>sCX%|(9t*2(Kpf2H`CF#Q1M&;uTFOI+Ygu_euy7A=7`z+ zt){uQ>);;0f8Xkv(;AR|mHuU-%e(h&oc6a^(hY#-<$8F2K`7CemcFS9X zEP(IvQ+96^xf8?M3oqsy!J&_9T3Q?JVs?I;vRfwtR|ByHK2QpBVb($Y#wdL<#)g5YF#k__4GEHksA z@m5gr07X$b#2djIl%_3}Lq$}?`>ywe_lM%n=Ohr8!1e&kZ#b9f5|8M!|443Cw6|?QcXJ zjS3PfnxG1tku^s%`hIP+nAbD5W9i!HVBXlDv2|X3nVSSrAIHC zw&9r8xWL)n5n`(a zVa#we$1n@Me3b04_)RAuzAJLRt7n0VlW?-Y0=qO|XDy>BHHCGwpMZ|unsZ(Gr>Hm; zrwPoo^=!#9obihtBX0|=iV|C5f2KD)xFdl@SgateVhNTCv_`mU>P&&uknVJroMFA- z7+KBHb3WQ#ITE3zwY$Q#ZnH4d(S+q#pZ8RMFBB z(%`YV!&mBByUT@J%XcIh3T-M@VKqrkn*|mJ*0}=*ONFfTOs2O~EanZ}A}D7dqhPIy zbvToYaaU3$RG`VUw&rcUZ`d-g7Fg#S6Cp4RhHt8ZvjkQ|fFL1t#xp(uO*k9RQgDun zXQN%fV37x3*-@Q^T$kO+)sN|YR!J9F83xsLTTMtMKpRz@3+4^Qu(ysD z9R@lx$UX(<3oNa;zAK;ChqQdBHB=hqsaJhn*vt^#YnIZZCEH04Yh!v^E2K5o zmUK8^(vF#S165C(B%5U?a2_s@32m#u{II>fj%AQ&t^IL4hj#7O3psu_p$pp;T%@8K zI|MdPk6v_G3#cA~UcQ(_!a%>r%PPK*gaT~}c1I^w-{+H+E9jwHB8>Lu1bH}tJ?N9j zTufyxQCXmxT!c$byF|rvu~(qc)}3H+>!%Yj*;ZH9rMOJN^Hf}pE10+|OxSFid0i{e zF=JZ3r0*JJVrd zFa*wu04)?J0dV1@9J9&ANs&;UUPAk)YwL)LyiA_;OfFryB)8mx3KKeT!LO zdc4GNhWTbYTEStCULIaTIM7_-K9dw!0-1 z8D1`##+}WLSUI{YOa$M81g^og3SOz=Rd}_)tO}PTV3bhQEL->9YbJv2Qo?H_6<;eb zw`F@+hl_a}uV)!R47I$G3$jd>z@`UHYgBU-+`!$UqIYL$bU?SZxqYEj_C`vtAw=?V zMG`a6CK-K`inrjcw3;jlBJ50VL=o~m;_bMd#hrdt zNs}c(6+8i(_r>u}frS+cr42h>Fw=RWt6OOo6lwXpRlEo9WiU&A%aTXe{T+k_!?Y{# zhjzqq2WcNtHmuocEzhDdH=Z8Qb(#F-=Ah@K&O24yRjt$V1tqW^Bhshw& zYK4O7_yW@7X30vMR~I~oy7gTWF|@_;F;);1<7!DVL|n&wT*W73#+<`cQ@uRtzym5K zaF992uRPrK2e!yMWNB0+yBk2aVLL2+bR1T3pWMBxlDbc1QiXnA2tKpfwacnVy)FT&%k9>F zf!G!%7=hW{MnU%yN}o26XG^l%%xd|5%`)V--xza-4F>SqC`2O8VjWD;J9KB*%9wLw(*`XDE(xiGA=*FntZ`cN@)G4#0n+M5(_HNS} z%GgC&D-TL)Ty0t-8OPEJwuGMNAUB!1!!v!AW}c+zYbFt9&oF_y!}-r&ZwLXG5!3B* z0g}{^2pl4*B^VV;Lc(h9ztSuZ|4KS7Q!r>^gJ-&)3)s@k?QF+*Whum_d~B`cZgQOy zW#W_uHTD`q1!i4~hHVUX-}2z;{8nD3^x0fjeb5vbwV+C9`3x&?e$9?&q%oD(Mh9}* z^2t72&NJ?^K)hQ@)ZBe&bEJfIvS1FES5fD_;|WN!!eMc$6}=p4H{7V8s_I}(#+xI0rrUKXt<(k?z3zQm6SIBg>#=Zn`jfmxU8ogu zdEH*#ZJHycVvQ3(DDbK#_7SY`@DuO!R33kBm_@$iEm}U`%d)eBker$jr-_A%Si~ci zSj;-5Ql0ywy&UA7y0!(N-2DHC!Owk{hF}XS?NHO?CGkq3t5DFbEqTqhbz2cUNUoT& zrdl3T#B$!~9DgW}0G@k!y%*=f6fAm^@^Y7NNqM*GK6xAJK6zW|K6$IiXM$H2_4qtz zeSu#$@!7yjt)Kng3dn3sTGWbYp#(I32 z@AK<930w>~5F7$tivj{)!B;)w#pRq7XWLC}_aZfch24D4Ty+@B>R6Pwf))Onhp>JJ zTj#TNLmhUmnLx)an0W-}?M-$b#FhuyHz&F6FfNo+cKTCxwa1QN&)(Gs(c501s!zox z(4VTGaR~b+@O;itQuT+CtHVRs)83G3n820yVH9Xrj^L`jsfLtt5cb`)E9L^$PKdyJ z$~abY6>A9KS{k>Gzh|Ny8?c@CfIT<|m+>Aj%a%Mg@-TiruES;*%pPxzKA1e);%oRi zaae>gd;{Mk7Ofb;xA1L(*~2;C!FQ?oGD`d&juOll5!~G2(D{BtpMhW+ue7N_t$CBG5$Zwv&+a_@9t^WLX z$O*FjU61%J?+aSqKY4y z_@D>-UAM*px8BY5g5+L_>$z4*`+m&l?{-rDB2s+^`}VMR7h15Je(Xgj`gkMRkG;5r zUff5IT#6E}D6Yj7xPg~mx3l#QdGN#_?&nR`!!YRG5!b_m49W%6^Go`51|e_(ckRA2Zwh+H1CmLKIw~@Wa~f{-ZnNPsIPv_zTC#Bo(FL&{PEu6_idu z!C#vbax;K<(O{Ri=&G+eljmjM6_ay+;4spCoPhe*E3sDPGIc!dd8CH?H6+o$xsE#gr|idL{QfWhZ|C0-=MsUZ lSeei8-(o@$r$C6f|D7oJ%q+h(b4;4^xub|BVwvC`@IP{Rb{qfz literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/service/RegistrationService.class b/target/classes/com/example/streamflix/service/RegistrationService.class new file mode 100644 index 0000000000000000000000000000000000000000..2c7c99b2aaf923a3e132b71150b593d7ecc9db0d GIT binary patch literal 3405 zcmbtWX;%|h7=A8`6OzFt;=bX61PF`^wv`|jAt)M_LX2V;I!tcDz+@)QOb}}AV)wn; z)wX}4wt&=g+VA~QJ$>&aK!Aky9CJ=)x$~~iyZ-&pAAbUf;uk+EP${8GMm4rDY@bqR zRK-xuv@$d?#Wj~{q;xZRX|A9r6xyh! zpgHI2hBA~DK~@d&>==$qI3c3}CmDjCacf8zYIJAV<_7fP#J5<(2eR^8|~44zhv zZabaJ5K(ki2?el#L1a=4XYS1C&q5)_7pGC+_H5wt>v z`O$_-aXBO7tdQt)3UG_4os;pL2w8w)upHF@Uch+?9Wo;5WaxaB)S!yvUXoE(6noM$ zsoJ9=0ZffYO2k|k+$H0pFgQR4FWV;0yJcKL55sOc@1wNg$(WVa&2HD_nXKzDRE7GR z5`r=+qYsxE{6u-lu(WBO6kl)UzxK^Qy;Krslxf3A$haY*;d8Bg?n@YB zP&SYAhN}1DWxOKcRT-~goMDe=iK}P060E4?Z&l1BNjn<UWYQ^e$44O1lZRNT@|-yxs=L-dB&l9Jj{SJkEm)NBFseIvBbQG-nAj!u#2 zHciX*b}E9eqhtcmQfNAq%(OEo>I%6MUp=N(g0Sz@jKccqAEo}O@jjYf40}s#mTkge z(e+ABUy3ESTxym|VG)hsC^d-+YSmTPFFFN11;twH-D#)v?zH{Uy%Gd0-Y4o0=*^&e z72WHa7r~la<{>qQ=TY0-I*%Q{(s>0wr0Z_t01go4LDb+7w&F1Dl1K0noi^wB@i9K3 zH^6Y!w6)l^zsb zp5@2M_7em|BQ1s@w4w=J1Vj&7a0Ox7AKGXNox(UygQFcYv{K!tb?N~+J;>VgUamm) z5MSU+LZSf~e1)$GvT<709^fJAoFT}+!M6nRcb*OGkcVP5BOc&;`fnl5AL#DGkKVHg k>rpxrznY-$3Euh*ws3%X4hV8!7b$r}_X_;9BIIY_UwmNN+5i9m literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/service/StreamingService.class b/target/classes/com/example/streamflix/service/StreamingService.class new file mode 100644 index 0000000000000000000000000000000000000000..3e78ae1c40f6a7c58fe5f96772cf2c876549997d GIT binary patch literal 5228 zcmcIo33n6M75>JSJ+>IYm;e!EQHTlNK%s=Bwa8+ODQ;~HHaNve+l-|rF?ggAGb6xs zOVgxH(xhp+CUoC-N@x%PnuKm?o25_3=i)x>6!Yn2Na7Y98x7ow z4h`EQBrO#n4J|pVnB6raMRa!7NC=RX4uvXhM@zot6iqwbPFUV%U^6-iIO?q7=Bjpz zFjp>SBx!2ATq@a?aOsq8^yug{umxLbjIyNePz}o*chL64!6UA7Lc_LDnTP{xoe`?4 z<8}?3BAg&y>d#aW8b=E6F|ZwXXoyRv2YzO>Xvfkggrv0SPTZyAy$0SV!+Z_7vjUnZ zZ73T@8as8|ZQvfoSxj8HLZ!GMPD!%jJA zx*}P0{G>@(Ij(g|WRt#=EPEp9AF;e-nCNlbhe26g_RJH>+Q7n)?wVMJy%^H*0g1H_ zGCC1Xs=?a4#MN+Htt|?`5#S=Jj(r+#jSv#XQVGzpKN|T3Co7nqs=iH_wv{y{D#wIZ zw*5Fp(I`L04IIQF4TdNDuz<;ASiMl&2}ypxfe%UYILVa;GMb~9H1Gf(BpbKK7(L-J zeGf2*M4g7MozbW&7rc}@D5R#d+Omalpnb>S>hKKsDA)DfATKoN z$4$E|MrnT?y6cOl>eOK-wX>Ca2v?qO7Ja6D-C@-ntSQdn zR-E1SV+KBsPp}uVu#A`|LyycN@8G+2%o25PfoEihK4swHD&WV<`Ft(lTkwd68>u8m zjU+uJ6OV<*E2}=8E8F%=D;~vTIzDUQb9g+uON!!r;p{kv_U>MyB`;)QYkw3^8u+}F z)UbEA{Q9(kFQ~8MyLEg~!^XL4I%L}-Z`%ECzFc6bsVS;o;t->-uh5)hYi900gTWSj zl|xflJm%hMcq7vLHHC8F)D#8M8YZR2*zD*PnXyaNh)5>YbE{3|kQ{+nlr+Q!SWoGK zVXG+il?zkCJ!nqZ+-V{i+RrfU|2 zFI*n8q6(vcT*D&^>T^wgsO}OacoBd^f`6$FLPcA{{wW(q+kE+~4h1>9s+aD9p0HN7 zyy=4cb@%C&|jC5dCmH8>P(sCcuhn&1*+{WUDt&FaW>EhVd!ms9n)Z)T=P z%utF4B7nP8-BjN}Sg7I;u>}JFsUSl}Z%m>+4neH68$2eVE z1u9kEj7<2 zvySjoVevRoJFcJ9lE;GKg8aXW&J&a2ns>H%Tuw(bP)nBK2D@e*i$ET1kCk_$Jq@Vk89>24926 zgKXczw*w`$x5?1Ty{p|XVoe2!z8DvJ&qdtafJ@i}RM2$=slKK}Qw7^9*pY};(03Zk zFXO($?Yk~u_p4-B-g^=6mkh%p!$@EAWsDxafCGJcLhq?yETQ*SFpBl+@)oW%E39h5)5T4?`qehgTE4N#_i}tA9^r^UL3$yO!5Z&5O;If2AeKF zg&hpyot(z*!jrt6K8v07+uisfZ>Z;RFJ9&i@d^g;8g}C?`I{8>s9-%9cyu;cSMeQu zR|V@TzQ>ge)>V8Tk1OB5g&%M)MvW>w`9}=&0X&8u<0pi~B_w2ss}7}YVNWAxwHp;KEK^yQ5d2?_(nNQ`kD4gyZ!H7W%D3jL)XU>}3sg%`}vN1HYuTp9? zY1~FzTvm1ZLng^asw4m6m+{Qu_Gd5P%Tl1GV%DsD)=PS}g+=MVvIu<#eJRgqzzd8L p=tT{`;qyiQ-#}?s_=56-C5Wb)eq**opa7T=Xai$m;Sf( z0)V4g3c}E$s@d>(x!&JuFb&(mGLxTGT}mQ&0~@ zV1L@mrnK{FHfLxl$F(&zJ8kIaQ;ufO>S--CmY;Icww`l!%N+Cf1e%=C?kO#2Il61v z^8(#N6~x&Eq7sQk-X5vfEebZEL10^0{8Y{T{_ZdLFq>=3wf zZ8%ryB2YW6o0X2p5l!sITv|quUI3Q4`JEOMdP-QO*Pe*Z=z|Nv;1`R`-QH?%(CZ8qI(er7|^Gq!c z3A6>OBvpg?nVCz?!)lJXwk{Q&sK-4r!dMM@Zw{i; zJc>gg8AYE!R|PSFd%^`oiiqL}ExK`@82V8ye;reBAMO{3Ihq>)LP0H+lB+a2Xts>>j$Hf6|fSx%zT z&rF_vj4OB$69NjA8FN+J^%=JYZd<|8I#I(EmH(nKjNr6_*W(S$^_-D+2Gn`hnXN6y zgA`i9Di3?3f`{c{61}5lMjjP|im52l3NoO=8;jA{Z&`+>n#5^VHS*fYX_ow!)8YIK zC_Fo66r6!hRV1jkt{Rd$RCbnWeHWZlU`XmZQu|3IGuKpLAtw-V)Y+98=C`rs91E9+ z7;HFExC-)^6=3LpYmj{TcTd8TC&5<#uInID) z>RKk0pN|L}n6&aq)z*@xraXEfGvzL*So_nW)cgf14O!>~5ke-r&yj#J0@LqugrSwr5 znFwjRI};?0Yzy%cv7XU!L}Wt0zw$_+Ef=NKh~*w*-wea>L4nQ@t6WJ+PD@h$wtW;- zzj?;+B)V(y5rOUD6id2HOAknotrj0+WCofSw*_9Qn73#;+j%}i0 zSYyOyMpawQYOZEe(v~#)jLUAvWDYw=U}ROYgc3byg%z@}Oh4f1bwN1DsBBm%i=(Ry z2l*`;&2~`eLgt~jkvS(6GPIl*M>47=RK9_QB)pTTg=fI^F$W!CRZH{bA@u0kn zTcfI;3EM$nO>ItEZ_KDDHrwMZx>HfH7Rw04XQi48yOwBT~qC7TxK{<2}^FugU)ozL-J=ilXMVgtL1 z%bTB?^9zB~fFB8LUUqJ{-)>lk7h}AqT~n}#=Nj-FH-0KNex_i-d;5ia`=x?Q-rKL` z+e-?b_ugKXZ@*RW1MltkG5i64jN(rU{*1qr48yuPYn|gT<$07!snbja?|xa&3RQf2 zIb4haCf|npRWoB~PV8=jy7$wySf7}H&i zj!*_o4kSkm)p0aVHGk*Lr#O<8C{k)(6#wL<<@%(oq&^n7oldCY6d6HW{sn-mxV&_G zPhN_>C$F)5#yH~F;Nzsez_-WvloRN-wnd1x_DhJibzH*ww$4k~*p`S~LgTaiT7^$Y z8m#kvN3b1rxD~hXHoFNskic#DB)=T+CE-)}G~Xn>)I@kIsf*Nao4lfPGLkE)PXrga z-WVY%s4(~$`~b2&h0ls{&1k}I?BV~tXyL)F zyyCQDlmqbuI`I&?pz%8E@atQ#4^QweDd9Wp+gO0_0zQk+dGKAp=eeq))JO3Je39qf zhhcmPUnVZ27{FKXRYLzY%BY3-5B5fRqJ*`siXuQ};OqQZM`7RKQ{bC~HsbmJ-xMbO zpQTle?F)GGQxdxX7tzpJ?L*cn2wjfgCyBLSQ22GwrN2$RB%%oFH&~ZlM>pX3ezWjnV9f3AG7+Vsts<815F6;DpaKd^`oNt=Zi`7*G8Q>150C`L-QO&kL(x6;{9D>aXOK N<@Rs*2mXb`{{hMLLBRk3 literal 0 HcmV?d00001 diff --git a/target/classes/com/example/streamflix/service/TmdbService.class b/target/classes/com/example/streamflix/service/TmdbService.class new file mode 100644 index 0000000000000000000000000000000000000000..69a39c40c0f54bdf3f9f064d469f1166d3c51a73 GIT binary patch literal 2884 zcmb_eYf}?f7=BKGEQAFS@q&n=jTQp3)M_inOBJs*QA!YNZEH`G16i}#O?Nj@#_>b{ zMSq6QSUWh=PCxmfzp2ydb2b5D2~MkiNX})?`@YZnyq9zS{QK4K0Ir~%KnxuQI!$yT zE->&=)}`f0cg4D$f2azfK>V!j+TnSDj`4}59(1F}K*B^XOo3Fvt61u>tW+Ik1);BG zW!bSGTY>WHc0pN-m0~{Em<#kgQu#T@RxXvE&3OKb6;yrOU0L>JMLqKTvc&@ybreFo zuB_#n%ZcY&g-FJ_yDpvXML%{J7%;IDg92yXT@{^LQG~NNKJgxak_+m`nhL_}N*0wL z-0|&PRTa{jt05D+v4{=a^Ddov~eYt5(S?e^JsA-H*J2be5!V>veOp zGc4KkY-&4t+q8FjG>}zc$t%uF-OouYHN>@RH;Vk$66`2+0z20O+z3L{9g}TByqP7< zn2@`QE(@mWb^y02AcE}6g|bl2%4*ZqM9!=E1$EWdWDK<2SJT?AKzrtO)+jGrttzW| zy=nh1d0xm%P*xj61l_nNFei6{`Dr)q3yhV*uo|4PEW09C z)O3^%y0$~BYCSqNGb47y@m&JnFey>4^7ZKvM?a!xu<&JJ|}m^WNk`B8&WOqJbj zFHt7woBNHXY_7LNy)>{QaCD2Qc5WKrJ$3NyN13$^PH>p*D8|dV1B3dx!&yvUtPr@! zc`wIH{LKZp%n!Wgw|=-nS*9zl^1wC9=uvA-q<)1+P5uJod8G9^|ND6s*o96Ep@-^j zaJ8p#8eifjSCorz2xK_#5?p}l*)E&ve?mMGm`%Nav4%cA&|YBIE9~Xz-w?akF`2v9 z$*qyx8V+_~4N3BwTEkQfFLB}zq@Gb>3>wXTs^YVR91P^A0`$Z8z5d!wnzmpGlBdX1r}mpJFMwx4TknXh50V97j2I5{ zjdz5$labZ2hLR1dqfM)Ey7(0qXe39uZiv4yVzAe@py|XSKgDn-3SOL0m$<&mUq5%h k<@)RB{~P4FcZ2{A@k=4z!{^h$Hg6R6!{r7Z6Pf$FB_y$eu>1scLS z>Y)pqJ7!PC%|pgiI%&pT&oPau@uYPq?wZcDHD<>5TINA3HF2?HPdKLQ4rP}GqSM72 zgJ#-xEzfoi3vBJHK-9?t&8AyG@01KK)UgPQ1(u{8d)!Kzg>7;?59(yM7)?=)=vazV z1Qt!%EzQ)TdtibqB$QvduF34s?X4|3}?`k2Mup*vd?n8 zyxQ9bL0 z5!g7liA#(iP&aO+659^E%$0i-;VzWS)0(6ay*_jmOXr>$|r{IH9qVw@g4IMf< zv1QJ4^9R)MG^y)IPGL%dK07r*iWlHQ4HxO?!Zxx}fsay05YQ}l;9$yh1eOMEsK_?& z*RWk+l~Q@ivg0xg<79}rW4oEM%tX(jF;khg4m$+a@3S*8!!cti+lv_)Z_;+GtIR~q zvtx!rig}Zk8}rk^`Y^h&OD4Krfm4Hr4|xu=RA=*E4bK)>S@5jhWYU~4l3mV3W{PGB zoZc_c*<*Q=rV~^7pf?e-otPiUoJ?h0FE(n*v!|_unP`oTW<15`sO?RXm1kj>hKmK- zD(KGlf>{|fi~&^3KSMf(aS2_*H9g-`1X`Nr@CYB0lGfu=8IaGR_f}vakZxxsuLu}` z8ZM)yD&YFeD?G{VJ27{-Yf(oYyi*K z;ed%YO08|rIEKtqDnpwIW>-6v;9g_Uk8{Y9g#l!AOiT1eqW2lDcd;z7B;n(RaR`Ss zTqU`DK@peRY&&ThDM@^PkT-u_@%BO;FTypdD0sc@4z>vHq{(Da#7_=Ey4y?eQVlQD z@p8O^p;N*10iT=Y_`!M`a*b&kbWKq7Y`2oD0l{1=k6sl)HJYSSUajLbcx^V7=Hq7W zRGJH%I=o&WKJT=nx}NnBgwPaW-f5D&T(9FzxPh^gt!6&u-o${DFr96OhdjgahOH@c zyXlTG83l#RCdojVlhvIiu2#DE%>v5`o|Y@Wfs}O%lg+$2mzg(=o3KkZRBzSsHoTq2 z&c=1WkusDQuOcf4$gUAjn~qCf%oLlx9#y}5pw2qHL!f)8r?01bIMx!|F*wj4V__Ld zXPc}MzY`pBJ;QUGy1Rykn>zeGrqQmU*tXuCy}O5-o9l50-l^eTI_|`~1y;;?G<|xE z9jRyTX5>tpdoreMFXKUl<`REdITQlu^8GdA4UHM^tfhM^rCqChGA3j%)asj*sI(`h)T} zg-={ISm_KIIX@_<&qpZ(!NlSN)!|_#knFi;wztinT!&BQ_Y^ZD9%E!=(#|+;9X`#y zOv-wG#vEZ@q#JrUj_dFU_ZVs_Kjn8-52fL=0vjr%A~V-%#CO}?4z^su-s1#y=LfE; z>Em)gj7PDn4xgu2%t~Gbm^yrssD7ykE`OOORh6-@NIHY;^eE<=Rj;aF8 z>Qc$2j&gA!4!fly4tbqIJhNh3FrR$eS*fQ8Ap5!~Boai2YAB=;JQCFlNPNcNio=T$l+$JH$V@Goec zytMV3>|zu7v_WDki=809RpbqAeYP_Zchj*$7j9|>FG82f{vh%qvAG1W{nKdexuo0`IonY5Uc+JlN0&C~M@ZCb{Z#LT` zqmGpqnFT&UIoAA)J~wgSw@G}FU#qyH3OkF*9E_DJp@H>%>;SW+?=j(gfu^ZD%39HL zjFf9+Z>a%G4i}br&lOmhNhetB3d~KM%%-_kF`%#9($lwWn95Wdk(ynaEbTS9TIL`o z=0t_IcHxxa9Ow%4l||Ll?}I93?(F@!jP7OMm?$V{DQjr|VIOW6+ZXlzv|z}ZNEu$n zVUQ1%sL(H4DqG0zYs=I0fD34;MxIM9WQ*k=PN(o_{}m@aj!r}58WuU($1I~lOY?uW zQQ)H;pW9SdP{Tl%q!(rNpR3i9i{b=!IGsyk=N5x*30ThWmjWw(>6T*coQwN;Q#vbY zOpPXt)#dfHny)gOPxAOtxp0%oGYX_dV{6Ri+K<8_&RH(v(?32 zjC&W=iX~yxiHI)3BD7e9xNxdmXw=05H9K9-qPnP4vt@F2hAuQUJ5$ci(nYPBt&p>o zx~NgJ)pB;WE~?dRZCEskW=*u{Vx4F$dR(zm)Aj+A6-nBfoEhUxaw-Jt*)kVqj|ctA zN6HGa%Sa`Xrn|b&whv^|odLHjb<`rnVJavHKL5?^ktfFuwzfXyZX=l-vOJU4(0fyS z{_19ybWK+iaTdhFwXKMeV(~OV&pGqtpbY%1F0g{vwkp1TYKX|EdX6IUQCxlbjIF+W zd{$pR7ppH{dHJo9cRjxFe1+p$e!t3Z1HU!+8ZXgb=j?96X#B2eISSFT?g+G&)+1;!lNeG0p+-^$Dc2hL7*BrWNXijXBPmIZ8cA7d@g0t%wL}Mn2T?#^ z@O>OS5cn>Cm(=o4_DA4*Y9R1^{D2fGtGJD4NLf#`+=qsB$FQP`SD>}P4Awo2_Kpyj zmq$Z0*f@jpX0Y`wXgH4UeUYA{*!fWFqtMzOL4E5ZSi&!Np0zL1cNDuHI*x&TN3o}) zCR!tr2MMv7l$Nv}!(Ms7KL3FI9kqn0oxyYO!U_VM+I9?ANPsaPAdv$YkJeP*k1J=8 z&J8>@I5>l=iKsgt#8taVvUoJFRsOdhroHXdXwOvfm)>e27~6A$~*~+{)39 z@e_`y#k@^V;4b`B+4KZ{#*wt?3H+R+YVz_?`~ttE1@^+jukdTy^f%n+L&sCtsG(ic z7CePb8U~i*w_FdAPPejkLp8~Q1VK{ByYTNg7Wh5>kTohE;~~;b_DIdpsMkuBzS39e zK*6Z5DzWS9q+O$-)){!wP#d+abQ>zw?U~AJ_E5KjlxK+EJ&etKbKQdHP=ouGo;GEf z$m?Ve{^;vu51zoEluq{GNsgpWI+P|tN)vSuPhm3^P%8f)Dc2MHeUiVq5YEc~XZ*#N z{{a$|0n-}ADHFQ66UPXV+I@RvBf?H)pAED@1ppnkgf>An_NzupY zBnOq~E!p$)qWAftH&RK`;8ldI!e2S6E(k0wK1<*lLB!dD{tY+yg5E-OnIpDPq!fDL~)4s@?kK)#ci13!kZO3qX z6?V5p-a|jXyBe1=gM3gh4}7E}bR73F3(d$Z6ge6VRc|h(J`oMo6lR}_ zhH4A5&qPC7ekN5FQmLhb$sKqFRdp>@^-8MhRTTPl*oxO7W8>N0(7zmYDx~N(lmK_UGWQAL)tO3-JBaj1p_2m%`0*fEKWDRvA_(_@jw_QGmc-Cfx@ zX-V(A+jM4g<5#bVVxXb@wB=aASq&|{eN&z2K&OtRfi4&taz(eC6F1Cq z#TGd~@Pt{Ov8@|9UwHFYQRFV0L2<5N`N2fEp<&0u%GM=OaeXUry+sWt3QYugHKMw# zI%8g%v(>;hbZgjN@!T297WK~>p4^1oi>uj`)Rci8ctFFpvM5<*T;{P&$Q)~AQWAF> zcnI&&ke;{1g5}Io@2n?$KQ8;sCS*@FV^>;s8F(0vFpvy^B^rc3u?gXfQv;Wh`KWjfyc35!?BIyy(TUiI%X`VG`M)a zq@lOBusT)qec_hUK3)kd*D>vUih15^pbuGQ@T_1$@9B*b&QzVEB+X4!D;3)kUOtIK zIIQD{fgIkeA*J**2-VQ#dc(FaCg(hNK|_CJj3QbqFj5z*s=SV)8Xj)~f^Y(YoQnWR zq90EfcoN4oB;>@{DzSYp=s2fgpMu=6+?*`v91UWfoD+^E zO2apbLWQ;iBN~oOyVZ>8iHze08M7M9xt{fsC}jdSV=AUhFlYIhN?77Yk{H6M>~!Zf zJQ!C#5qPY~eBV?Toyn0(&Rq7h{#|4L8t)G`nVjb_Nw@s8pX`mEz zVFHsno;NUs%j|1SSYme7T5r6+q{9M=GV6S4z$q#5GRnu-*d42|=6=X(nKtkOKA@q~ z@-MQb(QtKIFqj|Iu(w{P=WScen)ZM3)7RwK7cvxAIF>RhArNxZpWYkfSv3YzX z@ghcbT-9)(iRTemHLsH}QIeq+8peA!sFMhf{03yoK$RpDm^Cm5OGB$~&QnE0(8H5+ zr5(k~KE>0vS0ZK`C`-gx48!G0u&Be;aI~3kD>pNg;g#ixCO_&CA$vxkX7T!%S&1T$thG!6n7+g@z$%L=%eNS$0f!C1l%ov4ohrEAY|gL9V{JxazTkS- zbAe|%z8Q`?W{o^IS>NWs#BkQ6**1p9(3J+*C?|RmW$twUubxsVf=~? zmu>C%Rg$-#UmN%l-sr{~-1wc`_`QK2s<%JNw?7&9fqHvWzP)AOb@lewB;Ll7j#~zn z@wbLE+H&UI>mpZB_|pt?h6$rCXe)$?K+i@{*{YyvC^~uU>q1fL#ZlAonNv_8UHR zKKEyrp=A#)LC+pq!j|meC2Y^8TbJ-)Hr=*_bT-|-gxznEGJ#L=e=p^L@>`KcCw5^g zFN-^IiqTLyK7-Hlje3-|np#PFr4Re%1(}0@TuGZ-)k<33rdHDK zc6^TObUWEWM?n^l7*q?Y0-gFizCczwrKR|6=ibfi+t_mOHXcjh4h{gfaPVF9U*PWU z!?$qk7Eav4nb)!9E{3L4!^?Q)9zShAbQ{k~hVh8u;;B|{>j$}isf9AGye2tboKAgc z86UZK7v}Wgbn7yTr`jZ0y6rY*5_lKpo3x-sft5v)>wUDl2RjMP0Xo{xi{#VD5}?DF z#t{^`$*bAnNC%f;YobeC%*+UmqK{LTslGXPtvic2+t{kaR!6D8Wwmhyv&u%rEF+V4VEi4 zSgzDy8H`|&5Ol6Uuxqp4e;+SL-oHxQWJaEjd%v*a{TO$5A8MLHI&$1o*sh#W7?I# tPikrTQ@;ObJGkA8M;Vb9I~aNou`=zvCUG{|56XWrZMywB`k|NQbBfH8a*M-!S2L`+1{BG9pD zFWXk$F3wwbXBTBw3A9{sijKM_(A?WM6GJOv2I3}mz!d1qx&=$F+J#bHTE6n6U6{)| zt5#V#d23o`%bug2Kjg$0h%c_FY3VIHSt&4(+2W*M*j1k+NElMdf5|<+w$GbK458{uE={j{^n{nmB~R0;fJ4Q@sI! zosM6#By$1wPYk34x~l}4$miv}ou5{=l4Gk`S<*4L;uC@7L&BJH{8Z6ZDZ84J%*D}- zW14H93he34REyB>%DP|qe-G+T7Bze` zqUd6_tR%U(73lwAxOzNo;Do^S?FObfpKlOFD=dM7Q*J8EAQiGU#SV1x_H16pF^JP9 z&fsi?vtc6a9@1jbUa~!9llg&pYeA`!HO`^lU%D0N1==*6uIIc6c(nr;amm1C6GKP~ z^la0VK#bKKmv+!Z#~Y!y8PHZ-6)<*ZY^iJ#+0oZQDX2E7|+hCk!+TJ)dZX5*au3IamjGqou8Yyk7?G0^_-%F z33LXCZi=oI4^7PA5nHNW9b~S?Z=&vS;3ba7V8ecC;wwBAXs>3jUsKfCEw)zWx+bhh ze!B08z{%eKq0oIZ0+CTS$3%8!oT8j67iOjRKyz3ik#Vzje#Z73Jr5TnYQdrREw6#i z7YK>kF)7L7+&x>%zLFjdwAZLhpqOX2A?7vO#xpb=w`;WFMnemYl%c-UY6ODFhD}XQ zZM#!-9Bk9|21{(Ft3WiMIvbj*n_Zf;OJP0tyibp@0!4&zIR4$0qQ3NY*OD{fh=#eV zrhavt>ZtJ^>=qCN^GMBdL;YtpV|KMA;?r)~%gS3$z=XOlf|Ht)B=&uFk5iB{j_;5k zBSqdBfl3kG{)SNagkHMpc}ZHMZlUBBd1Iz=gkL62{H7t{^tTD0B7BbGYp#C7*)9I- z_woK$5d8x`!}yW2CVb1W9nBm&cw!e~Ja2Q>RoRVMWI5xxz^Xuw&r!jNcE#4@Nb_PZ zO|)>W^crn%uv;KA@D}F48zcq3N87LHew;Y+3P*o`jpKh{aB}d^aPg#G99+YxsS}BF zZ*ZXrLy=_UE!vWi#FaH%TR(hL?@LBHj5XXEiuV5mBN<)8?H}l~IY{D6kn}z#u%E#n zKnjO>m3Cu*Pv>!zT7L{TaU9cJeag#qj&F3L*D7&`iN-Jw$#=XKFu~^qmSG4N;9!w{ zZt_N2LY{@1V?hdhilRsxNkreF%fSBFJ9HWt{2PZ1Tr@Boqal|rgCb#>CNu^;A1V^- zc2A(1TY??|i@BX*Zl{^i*&2{~Hex><#;#+Ep(%`s-b;8!&zc9z{MTr=_pjl> zzIZv={E~Yr7M&X$ X?ihq2 Date: Wed, 7 Jan 2026 21:18:26 +0100 Subject: [PATCH 02/22] working post movie and series method (still need some fixes) --- .gitignore | 4 ++++ .idea/.gitignore | 8 ------- .idea/compiler.xml | 18 ---------------- .idea/copilot.data.migration.ask2agent.xml | 6 ------ .idea/dataSources.xml | 17 --------------- .idea/encodings.xml | 6 ------ .idea/jarRepositories.xml | 20 ------------------ .idea/misc.xml | 12 ----------- .idea/vcs.xml | 7 ------ src/main/resources/application.properties | 17 +++++++++++++++ .../streamflix/StreamflixApplication.class | Bin 994 -> 0 bytes .../config/JwtAuthenticationFilter.class | Bin 3517 -> 0 bytes .../streamflix/config/ScheduledTasks.class | Bin 1829 -> 0 bytes .../streamflix/config/SecurityConfig.class | Bin 6136 -> 0 bytes .../streamflix/config/WebClientConfig.class | Bin 917 -> 0 bytes .../controller/AnalyticsController.class | Bin 6881 -> 0 bytes .../controller/AuthController.class | Bin 12055 -> 0 bytes .../controller/MediaController.class | Bin 5553 -> 0 bytes .../controller/ProfileController.class | Bin 9034 -> 0 bytes .../controller/ReferralController.class | Bin 5433 -> 0 bytes .../controller/SubscriptionController.class | Bin 6910 -> 0 bytes .../ViewingProgressController.class | Bin 7209 -> 0 bytes .../controller/WatchListController.class | Bin 5231 -> 0 bytes .../example/streamflix/entity/Account.class | Bin 3279 -> 0 bytes .../example/streamflix/entity/AgeRating.class | Bin 1893 -> 0 bytes .../streamflix/entity/ContentWarning.class | Bin 1597 -> 0 bytes .../example/streamflix/entity/Employee.class | Bin 2067 -> 0 bytes .../example/streamflix/entity/Episode.class | Bin 3291 -> 0 bytes .../streamflix/entity/InvitationStatus.class | Bin 1337 -> 0 bytes .../com/example/streamflix/entity/Media.class | Bin 3878 -> 0 bytes .../com/example/streamflix/entity/Movie.class | Bin 2560 -> 0 bytes .../streamflix/entity/Preference.class | Bin 2792 -> 0 bytes .../example/streamflix/entity/Profile.class | Bin 4750 -> 0 bytes .../streamflix/entity/QualityType.class | Bin 1122 -> 0 bytes .../example/streamflix/entity/Referral.class | Bin 3264 -> 0 bytes .../com/example/streamflix/entity/Role.class | Bin 1275 -> 0 bytes .../example/streamflix/entity/Season.class | Bin 2434 -> 0 bytes .../example/streamflix/entity/Series.class | Bin 2198 -> 0 bytes .../streamflix/entity/Subscription.class | Bin 2840 -> 0 bytes .../streamflix/entity/SubscriptionTier.class | Bin 1879 -> 0 bytes .../streamflix/entity/VerificationToken.class | Bin 3199 -> 0 bytes .../streamflix/entity/ViewingProgress.class | Bin 4204 -> 0 bytes .../example/streamflix/entity/WatchList.class | Bin 2491 -> 0 bytes .../streamflix/enums/MediaQuality.class | Bin 1243 -> 0 bytes .../streamflix/enums/PreferenceType.class | Bin 1276 -> 0 bytes .../example/streamflix/enums/TokenType.class | Bin 1200 -> 0 bytes .../exception/GlobalExceptionHandler.class | Bin 6839 -> 0 bytes .../exception/NotFoundException.class | Bin 635 -> 0 bytes .../model/AcceptInvitationRequest.class | Bin 826 -> 0 bytes .../model/AddToWatchListRequest.class | Bin 992 -> 0 bytes .../model/CreatePreferenceRequest.class | Bin 1517 -> 0 bytes .../model/CreateProfileRequest.class | Bin 2050 -> 0 bytes .../streamflix/model/CreateTrialRequest.class | Bin 980 -> 0 bytes .../model/ErrorResponse$ValidationError.class | Bin 1054 -> 0 bytes .../streamflix/model/ErrorResponse.class | Bin 2857 -> 0 bytes .../model/ForgotPasswordRequest.class | Bin 1173 -> 0 bytes .../streamflix/model/InvitationResponse.class | Bin 1095 -> 0 bytes .../streamflix/model/LoginRequest.class | Bin 1318 -> 0 bytes .../streamflix/model/MediaDetailsDto.class | Bin 3147 -> 0 bytes .../streamflix/model/RegisterRequest.class | Bin 1334 -> 0 bytes .../model/ResetPasswordRequest.class | Bin 1497 -> 0 bytes .../model/StartWatchingRequest.class | Bin 1171 -> 0 bytes .../model/StreamValidationResult.class | Bin 1910 -> 0 bytes .../streamflix/model/TmdbMovieResponse.class | Bin 2181 -> 0 bytes .../model/UpdateProfileRequest.class | Bin 1486 -> 0 bytes .../model/UpdateProgressRequest.class | Bin 1076 -> 0 bytes .../model/UpgradeSubscriptionRequest.class | Bin 770 -> 0 bytes .../repository/AccountRepository.class | Bin 558 -> 0 bytes .../repository/AgeRatingRepository.class | Bin 711 -> 0 bytes .../repository/EmployeeRepository.class | Bin 648 -> 0 bytes .../repository/EpisodeRepository.class | Bin 834 -> 0 bytes .../InvitationStatusRepository.class | Bin 682 -> 0 bytes .../repository/MediaRepository.class | Bin 951 -> 0 bytes .../repository/MovieRepository.class | Bin 357 -> 0 bytes .../repository/PreferenceRepository.class | Bin 566 -> 0 bytes .../repository/ProfileRepository.class | Bin 640 -> 0 bytes .../repository/QualityTypeRepository.class | Bin 461 -> 0 bytes .../repository/ReferralRepository.class | Bin 1241 -> 0 bytes .../repository/RoleRepository.class | Bin 630 -> 0 bytes .../repository/SeasonRepository.class | Bin 567 -> 0 bytes .../repository/SeriesRepository.class | Bin 446 -> 0 bytes .../repository/SubscriptionRepository.class | Bin 683 -> 0 bytes .../SubscriptionTierRepository.class | Bin 592 -> 0 bytes .../VerificationTokenRepository.class | Bin 525 -> 0 bytes .../ViewingProgressRepository.class | Bin 911 -> 0 bytes .../repository/WatchListRepository.class | Bin 899 -> 0 bytes .../security/CustomUserDetails.class | Bin 1631 -> 0 bytes .../service/AccountUserDetailsService.class | Bin 2678 -> 0 bytes .../streamflix/service/AgeRatingService.class | Bin 2552 -> 0 bytes .../service/ContentAccessService.class | Bin 3330 -> 0 bytes .../streamflix/service/JwtService.class | Bin 4267 -> 0 bytes .../streamflix/service/MediaService.class | Bin 11945 -> 0 bytes .../streamflix/service/ProfileService.class | Bin 8622 -> 0 bytes .../streamflix/service/ReferralService.class | Bin 7331 -> 0 bytes .../service/RegistrationService.class | Bin 3405 -> 0 bytes .../streamflix/service/StreamingService.class | Bin 5228 -> 0 bytes .../service/SubscriptionService.class | Bin 5597 -> 0 bytes .../streamflix/service/TmdbService.class | Bin 2884 -> 0 bytes .../service/ViewingProgressService.class | Bin 8407 -> 0 bytes .../streamflix/service/WatchListService.class | Bin 5284 -> 0 bytes .../streamflix/util/SecurityUtils.class | Bin 2963 -> 0 bytes 101 files changed, 21 insertions(+), 94 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/copilot.data.migration.ask2agent.xml delete mode 100644 .idea/dataSources.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml create mode 100644 src/main/resources/application.properties delete mode 100644 target/classes/com/example/streamflix/StreamflixApplication.class delete mode 100644 target/classes/com/example/streamflix/config/JwtAuthenticationFilter.class delete mode 100644 target/classes/com/example/streamflix/config/ScheduledTasks.class delete mode 100644 target/classes/com/example/streamflix/config/SecurityConfig.class delete mode 100644 target/classes/com/example/streamflix/config/WebClientConfig.class delete mode 100644 target/classes/com/example/streamflix/controller/AnalyticsController.class delete mode 100644 target/classes/com/example/streamflix/controller/AuthController.class delete mode 100644 target/classes/com/example/streamflix/controller/MediaController.class delete mode 100644 target/classes/com/example/streamflix/controller/ProfileController.class delete mode 100644 target/classes/com/example/streamflix/controller/ReferralController.class delete mode 100644 target/classes/com/example/streamflix/controller/SubscriptionController.class delete mode 100644 target/classes/com/example/streamflix/controller/ViewingProgressController.class delete mode 100644 target/classes/com/example/streamflix/controller/WatchListController.class delete mode 100644 target/classes/com/example/streamflix/entity/Account.class delete mode 100644 target/classes/com/example/streamflix/entity/AgeRating.class delete mode 100644 target/classes/com/example/streamflix/entity/ContentWarning.class delete mode 100644 target/classes/com/example/streamflix/entity/Employee.class delete mode 100644 target/classes/com/example/streamflix/entity/Episode.class delete mode 100644 target/classes/com/example/streamflix/entity/InvitationStatus.class delete mode 100644 target/classes/com/example/streamflix/entity/Media.class delete mode 100644 target/classes/com/example/streamflix/entity/Movie.class delete mode 100644 target/classes/com/example/streamflix/entity/Preference.class delete mode 100644 target/classes/com/example/streamflix/entity/Profile.class delete mode 100644 target/classes/com/example/streamflix/entity/QualityType.class delete mode 100644 target/classes/com/example/streamflix/entity/Referral.class delete mode 100644 target/classes/com/example/streamflix/entity/Role.class delete mode 100644 target/classes/com/example/streamflix/entity/Season.class delete mode 100644 target/classes/com/example/streamflix/entity/Series.class delete mode 100644 target/classes/com/example/streamflix/entity/Subscription.class delete mode 100644 target/classes/com/example/streamflix/entity/SubscriptionTier.class delete mode 100644 target/classes/com/example/streamflix/entity/VerificationToken.class delete mode 100644 target/classes/com/example/streamflix/entity/ViewingProgress.class delete mode 100644 target/classes/com/example/streamflix/entity/WatchList.class delete mode 100644 target/classes/com/example/streamflix/enums/MediaQuality.class delete mode 100644 target/classes/com/example/streamflix/enums/PreferenceType.class delete mode 100644 target/classes/com/example/streamflix/enums/TokenType.class delete mode 100644 target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class delete mode 100644 target/classes/com/example/streamflix/exception/NotFoundException.class delete mode 100644 target/classes/com/example/streamflix/model/AcceptInvitationRequest.class delete mode 100644 target/classes/com/example/streamflix/model/AddToWatchListRequest.class delete mode 100644 target/classes/com/example/streamflix/model/CreatePreferenceRequest.class delete mode 100644 target/classes/com/example/streamflix/model/CreateProfileRequest.class delete mode 100644 target/classes/com/example/streamflix/model/CreateTrialRequest.class delete mode 100644 target/classes/com/example/streamflix/model/ErrorResponse$ValidationError.class delete mode 100644 target/classes/com/example/streamflix/model/ErrorResponse.class delete mode 100644 target/classes/com/example/streamflix/model/ForgotPasswordRequest.class delete mode 100644 target/classes/com/example/streamflix/model/InvitationResponse.class delete mode 100644 target/classes/com/example/streamflix/model/LoginRequest.class delete mode 100644 target/classes/com/example/streamflix/model/MediaDetailsDto.class delete mode 100644 target/classes/com/example/streamflix/model/RegisterRequest.class delete mode 100644 target/classes/com/example/streamflix/model/ResetPasswordRequest.class delete mode 100644 target/classes/com/example/streamflix/model/StartWatchingRequest.class delete mode 100644 target/classes/com/example/streamflix/model/StreamValidationResult.class delete mode 100644 target/classes/com/example/streamflix/model/TmdbMovieResponse.class delete mode 100644 target/classes/com/example/streamflix/model/UpdateProfileRequest.class delete mode 100644 target/classes/com/example/streamflix/model/UpdateProgressRequest.class delete mode 100644 target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class delete mode 100644 target/classes/com/example/streamflix/repository/AccountRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/AgeRatingRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/EmployeeRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/EpisodeRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/InvitationStatusRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/MediaRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/MovieRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/PreferenceRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/ProfileRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/QualityTypeRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/ReferralRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/RoleRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/SeasonRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/SeriesRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/SubscriptionRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/VerificationTokenRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/ViewingProgressRepository.class delete mode 100644 target/classes/com/example/streamflix/repository/WatchListRepository.class delete mode 100644 target/classes/com/example/streamflix/security/CustomUserDetails.class delete mode 100644 target/classes/com/example/streamflix/service/AccountUserDetailsService.class delete mode 100644 target/classes/com/example/streamflix/service/AgeRatingService.class delete mode 100644 target/classes/com/example/streamflix/service/ContentAccessService.class delete mode 100644 target/classes/com/example/streamflix/service/JwtService.class delete mode 100644 target/classes/com/example/streamflix/service/MediaService.class delete mode 100644 target/classes/com/example/streamflix/service/ProfileService.class delete mode 100644 target/classes/com/example/streamflix/service/ReferralService.class delete mode 100644 target/classes/com/example/streamflix/service/RegistrationService.class delete mode 100644 target/classes/com/example/streamflix/service/StreamingService.class delete mode 100644 target/classes/com/example/streamflix/service/SubscriptionService.class delete mode 100644 target/classes/com/example/streamflix/service/TmdbService.class delete mode 100644 target/classes/com/example/streamflix/service/ViewingProgressService.class delete mode 100644 target/classes/com/example/streamflix/service/WatchListService.class delete mode 100644 target/classes/com/example/streamflix/util/SecurityUtils.class diff --git a/.gitignore b/.gitignore index b8287c1..73c4f5c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ backups/*.sql.gz backups/*.sql.zip backups/*.dump !backups/.gitkeep + +target/ +.idea/ +**/application.properties \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 76adaed..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11..0000000 --- a/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 02dae28..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://localhost:5432/streamflix - - - - - - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 63e9001..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 712ab9d..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 86993b2..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 8306744..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..ee5a86e --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,17 @@ +spring.application.name=streamflix + +spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:streamflix} +spring.datasource.username=${DB_USER:admin} +spring.datasource.password=${DB_PASSWORD:admin123} +spring.jpa.hibernate.ddl-auto=none +spring.jpa.show-sql=true + +logging.level.org.springframework.security=DEBUG + +# JWT Configuration +jwt.secret=VGFza1RyYWNrZXJTdXBlclNlY3JldEtleVRoYXRJc1ZlcnlMb25nMTIzNDU2Cg== +jwt.expiration=36000000 + +# TMDB API Configuration +tmdb.api.key=324eqjwjer2wrkewjrwk +tmdb.api.url=https://api.themoviedb.org/3 \ No newline at end of file diff --git a/target/classes/com/example/streamflix/StreamflixApplication.class b/target/classes/com/example/streamflix/StreamflixApplication.class deleted file mode 100644 index bc3bf41ff7f8c3e5661cc4e1a1ab40815d65ad47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 994 zcmb7D+invv5It_2Y@3F(v`}cdg{zVX#Z*YV1XYwkrAmQT1ce8loQ<<}*|j6DH_dPH z1mb}Y;G+=ZO-1q$2@xqX>$#lqoEiW6{o^Nqw|G**5|#t3luoQPx;OP)F_MKX-lgm?db9*#z_3WZZfmr%lbfN~jE zP+@2o8}W2xm5zp1B=W-8FT8Jzci9HH*)A#iI&1sq4dx&)c=v{qxdliN+~$ye4~|VqerxJRf1&*#>57} z(#GsJpE0ZtWExsE@}+29DW*CwVU|ce?YTNMwmJtQX6uUk2{3^-vGyq%Jrqdd(;W~l%r)$56#v~MWRtKInp*Alp|on75Zdkgk@O*@RH(E-p{dj=ZZ;RPu-Q#_Z+O(V zU-hH?p#Mc@EEPNB$2#NRq>krq0un-isSeD|?%i|G<9E;bo%6@vzy1bb0N*4KLRdjW zMH`|FXIyVa_bZ;|%uIVmnJ>8BoW8&d`m|*W?&%|r$w#?&mp`geGlFFbCPr)_IdH(h7in$d49h`y>QaYtCDAuQKf z)tQ)G5aZmNw@l8E8E&D)uSrNXb>W<}ze`0Yx)_pG61c&IVcGtgvg~%s`b^WUI^tf_ z*<1`sh7Rel>sgNjf7|gI_9}Q?#T(eiaBS9?GdyAFba>w8LN5tX(FbXf_GzOI9YcJE zi$QJ_xyNucH9Tv~8@g>cGy1rY@aNLaTQTfsP_A>s<6d_H2XRQjn=0NyH$&G(1q=ya zkSqRuOOzP)Zf4>^JC5LO1@EYM7e^TmZ1lc%8~r82au{MoclCB|NNtUQHpZ80{oSv) zj?Z)Hyo?~FB8?tG!><-7J1B7J>d+>^C(w)I3QnldCD7>|X9R=F7lmh-VxUZkkiaRN zR&YkeS)60Iwpqse+^l+*SfVWTxJ*?+8uan{Zh!54(6xihOVCBuw-~OZ(!;N;G1ra@ zxTxTgiY#&rmtI*D!*24bv9cJ>?#Q{uQ@I4L;HrXaD&9jM!^LfIZ&^H4jkaqPWmR2Y zlB+}f!Z5N~UhV)wS=q%}W$WuImE7$`zls|epnJFc33rY=dBe7f45OQv`VW@U4-~vl z0c?3mMgzG8p31mK4c{kjEjD5uINOdP+*0s?iecQ|Q4X|#f?-!Z2Z*eM#eIf-lmG!w zRE1^h{jP0uQ<5&Bb`vAA2aL)TUdKEmg;^4j)x1hi4Lck4r1J{yGThk`6WLET;BucA z8lhMvZMW)~Jb^KU<#JC&9{1^D3suW5)@2x~&1}?yc8OL0FV_OhCRIGZN0Mr}SaYX= zYU^;N^Rl)-R`Cg@s5nWyF}9% zwj6$^S}t&J!YJ6ZlN@$U!_FI?CEx3t5mB-z=PtCex%C7<^w)KpQ==`88gde$>4TJY zt+t>cqGVzao5A1tBE)zJYSB9~zFHTDSJKyYt0x0%&S=4J8 zmD}1vkQyymfOIKdZCsNZQr(zzjfx*zTgJX5z$n)u7Dy+{8hnkMAN0JIh zqO&>%B}sUdX6jQA&6G3=MayU%WYKTVh5R!6Url+JFU4tEp8lTb^gWe|PoKC-;J;=XqpJP0;jEQA@NNTj$3Lb{AW|ABWL&knJaQp}oI7-nUBT!Q`*JkLi7w2(;dYewo z;}oWFnnF7VkEr-tT*UXt;Rjp}aC%6AdW4&WS-O%j`Z5O_iVS`YyC*2afyC|!TvP}< z`Sgg+g~|4J@F57k3~8v+QybZy!aO}iu|SeZg#U!5;9%@8SoA~xSfu?Z=~=>O!98?@ Y$plC_NUh9@FM{<;d_})eGW|915AY>7LI3~& diff --git a/target/classes/com/example/streamflix/config/ScheduledTasks.class b/target/classes/com/example/streamflix/config/ScheduledTasks.class deleted file mode 100644 index 887512dd3142a864888076fe3866fff28a59aa2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1829 zcmb7FZF3V<6n<`+cJoRhX~9y#O$(S!v0D+4HWjTg6)-7iYy8OEZf??THoKX-H#Lmo zU-7G-@x6ZF2l%5L&)uYLq?uuu%;dhEbDnda^RoZ^`}ZpVRXj@}hA9K_45l%|aPg2I zaogj5+kU!#C>+Hwv+4S-ddM(Uv>HhykTj6WAdL*eZ71m3V!*pSPuQW7f_GbY>Jy{L1aw}oU_t{w6Ymx|jxAwxG*!gqvS9TzHV#0weB z;v%`?1?@;NR|{m@4!zdhL%SB;l@rJ@tnVr^=lX3kJb^YHPjJ8AGlP~X20d4brgNtBAO_Wz|55A%DhKb>N7eW<7!1 z48^LHfi#TbzT*c#IShmiW zEW<4=_ofc88dQ9hph@x0)?8og^t<~)?(uz(Zm1I+?lrh{^?7s|R|hV&+iLA?+Otem zUl-~iXg=eTcPZbJY|M??r#ft%y{K6j7(vgE(RG?wvXZ4HbUJn38+o(vO`kftD;jR- zl8;-yAE>DH$o#z_D?MESEsgqpAUpP`%;?VMr_yKBgfQZc419+4nz?4)F-!V4K2DV6 zen^R-O0)OQ_S_@Y#HQnoCR%DY=u1azyPC=T|EJZeu3Cos6CI@aro=#vdsm4@USCF$ z5zOX9#mSLXeVx1!z8)wj(A103gpT2=9(}aS>4{706sW3Erk71RZPTjdQl&q^N-Mv^ zcuA)iKB4~$B&+q}$RkNT@sLiJhtl{IpV5i*BC8Brw4WwEX<_&rGbEgLu>HX;GXXI=8w6gLCK3F-%hjF|lbD-H%qz;~nyjr5j zfNO+&9rH8{7h%%-GD#FjW(f~)o}I;xF9$6JSwX(WN6s3NxkXWM8RC!YrtY3#=pGoH=|uv)3X&>r zgUaw=!76KFm6t1q&>YtmygYB{t6IS_=k=177lo>=yK7nRli{w#6?d%aE(p`r3*6N$ z^OSD5!e)3R*Iv?@){LWRvSO!-T}UzX7OIYGmG$QZ!?A(EoMo3Zr()}7Y2M~#v0~Xv zniFUgYM2dK?9n9B*rVWf6?foHhSQy}bJMh3&k$`z%xQB~-6#s%(N4Q=B{ZHPR&eZj zh8G96s#0e*bfN?~ieaZTai5C)(!Sk}a2$%jBsY0Ulqn<%4lqo&HCr0V8v>|ZaD;7= zd(U&nAr}{0#aFJ|r{W&m%dm?Vi+qyn_SMdM_7<#<;GTct!s%z%w$*6)1 z!$X^TFGPiJVRnJ*rh*3T9VhG(yXgH$asET7a z&QNME)KAg~e40Y*XK~f`0p1u`Kc#~)hP@@xl-WM6D77axOU0&D8`lZMkb6JcP@VFw zOzZ?6p}=XpqHD`XJ!ERODCrK>lms4SNH+`_R!KJ#7-iVgKwlQNKEIa0#~BVZ3=lp` zmOEStpq;=c818PEVGBppCnPb3aRpfwpTq>i@HQSw*BLHUKhoys-PjaW6iB zqXOJca<{M`>?zL~_8O&~?E^X(a2OsKcshb&-=CCVEr9(5gYkbdMHDaCMiQs+q>3~6 z6czP~u*PQa&I&J;ggso^fa5K5FMs3s4c(JUT#3kKy zQN?GZn|4H!NE8!zimKE`PPzIs49Rdr${52X70=4@mgMGI0H0oP%qb|SD1s{ViB6)w zKvk1EzSmT;(ht;J&Rd3FAoTU-r^luzaua#7^JGU|@V`r=MqR>!0$s%-mKdJuM7cJQ z2vn~3!Fq;KZ3(($vq5in*2~*iaZ{}TxO)e^f`iiBZg69|}x9OrR?O&o^QHr|5 z=L|x|nVU;V*WXt3aZJ)^IYyIsmqXzJx;KvjI?xyFLefxCS11MKyJ3{ zku@$Aa#^+9jQW9Vq&!*Dl)kp1uv2L^@dR!Vc0iSr9FOG%;=6zULULN zlgVMjE!81tyJFRi7VmMvO|oKF*nfka4waBiQRnZMLPo7=uS7z<-Cq41QroRu+j86@ zm6x1SYv7x1nl=k-lU&*CH|R@-B;`CGpA33S*M!MN!gaSs&+vT)~VO*39GK3ej~CHGCbUJ(H7;+=L(XI z5^B07W0+nok~fM>zuZiivd3K%bD?G=HZ^WpF5S)Wir>4O6uwXOPae9%!*y_k9h}(} z#}AWq#q%Q-Z{W=o-Xy|LB;ltjzKJU-Tp_|QB;l7TzK+W&TqeSAlK3rtr{MQ0{(wI+ z9I8v!&1GvzXgRM`(E5Cy7hFpoNN%J4S^xTHRGomySNAkGi-vIebC$JKtwgT`f+I`B zjt0i*wTg&N?|jf-vORC~pcF{Xa>K~$t{|VQPnxE%y*mp*ZSv0ym$tbDljXn7S&$(U z&gkScxFekGowpSHh2ijqptbV9f)5z_+E1k`LqAyf2mz&wZs_PBF7FHJO`-Q5*hF>c z>l8iZcrtSxEHm^0ly~W?3oiZdL^u84i5TueFZQD9eR;F7j1~GKK2IvcD!um*r0Bu0 zhUbY%ig!qaWEdK{fgQj)cHbaj^xT_yA4>Y}bsW4wIuhZ~#i4Z^{t$!j&~$(O{fN{1 z0s22ky7Up_J@Ntz_hB%i%TP_1A-sSWY5C6+SB#!t2(@~F2rttwPQ)+LllF%AGA$xC zJVsAO&%|FbG?h(X`f)TF2>_KPy+K#C^(H$#-7zP1VPO+0!-3OhB2fQ9_AN*C;dV_^h|s%*8r@ z4iWf37k7OW;v+bO9EI)@UG-IPm?CgLUZpV!D&MFRHn|b>Gix)i%lZj<_m4Fa^PXp3>9bTd zU~Prld6N|`=6zFIUOOxN{O=x?jYfF-==T@>LadIBjAKN3~uwZ9( zlAQ*m_Z|Z2B&0WofUzJUq$h;*klsT=D(N9T`DS@zrFmH!xnchx;s>Y_iTEtKrMO;H2 zG=a@&JEzNIMlPR~y6ZX8$PH%AV|v=QJjc#vrK7i5M)tU8rrq|^oWOi(d8T)~%dm_g z=?I*gI%A0rrNh>Y&j&h>bI2K@G6!jVwAtA76LefsllRlf6X?cA{wjh0B zJL6b_rBN)?upDOyTwpsxx|?@QYiQ6ha&pXeM)YCN%j@0J&D)kMg98QP14gD>URRJV z_21gql$ztt)w}J?@!bV8n~_dy3}*_gOqn^;OXdo$mmHA%=Z#5eB{w7uE0f%?etjLz z!V{u6Tf<5u1vWm8g9NGvc&xz1jkIE=_gd#HI>kLl;G&+6R7ZPnvN^e@`#@K6NP7Kw zJ735ePQP*~z1G;?*3;X#J?pF0)|1@bxwmtFZ&Ongs|6aS^I~s-;Hbm-xFCu~4NYhk zST!ACg}dqCG~wjQ{f1|b%F7GVp^Cf8RqXMUD_hHyZqFL7OP#0V?sK4)Edn*Z^mSMV zJ&N@jHsHb;WDk$kTGw&9{Ewrgm`4uJ)g%M8&V^aNH^>(3pQSxl zR?rU?th8s^mcA$a;K94m9>t{^Ik9#RBq*ZRqSVM??Y!4 zmuYw+QdDEgIqfQ$q&?elsjX)RmO8sa`jBIZ(ieWeh66Y#5KRXR1lE_!IAu>LVp#&D zY)jCsp-07M#2$&_3W3GGfId~nv9!!9tJUHVu8iU;4Nt;hrnDJDc^^|wk7sxV>avu< z)6u=ZE!E%C(cRb4-QUsOeV|+5yqQzZ#CBh_YcyPoejft?%Oj0*+1qYsOQAclyaoG~I<2hxUj$n=K!<$8wY(GKTN z6H?{A2${uT5*_VrsZ=tZm4?+XkL68AX8JvQL|SfRlgcwAD#>I8^ci?gEi^%HP(f*F zuvK-LFOSJ|!IM|yGt5CzTvseOKD754?r@ipk0Xz&7#OqBTwsG3M(O#EK&WIOR3NF$ zB;7(fO|rp4Hha7d$8bD~Cu?{Lp337U08hRQr+k52i{Vs`#;5izet9&2x?7%HC zJfA5=I*#qc@IryJ_ZViD3VU{-douJ=Xpk6QOp9ftM=Cdlm-^<*m_}zNhL@9VPIAz1 zX6kS*Ua8?#cr_&>&Q6b#&>}IshVrn%$jTUQp(T8dy~pz$zK)m;4)FZ(8@N1b%CUC4 zV0kgTiOXupa_5h4rEdzBd0j#FahAPF3~yxu4yO7Qr9byJ&ao9@p;MWln+A>H9Rdp{ z=K39f@?Fdw#^}%?!%GhS%~98-8J+3CMmCa(8^oH$BsWOt;BKqlI z&@rffj}3#s&hj`6d$qZz9-dP}^Q-E=VwS>A5IKGkr4aR#>nFVvwlO841g)~PESO4Z zoaZern=i^s_f*=D=n3t(cG7V>IG*Y)zYS{LN%niEp=SX5nT64>win|$OnYTBA;fq3 zoaVEPq(`_Xl%%%D9I{vi94c4Qq!hK7b33@BSxMT^%^)BY`%Kp)ZrUu%X5AvfY2>Dq zY3uHoF~rVWAKj?ihN~MB1o{DX@;)v3MBPFzXDAmg-YdOi=!;~!h>*IBbkmOMmr;TC zZb=m^H|Y(_WX@*iJZVF6z;KyfY%8e(kZdTW-%v|yh=nNUqi)M-2VapXiY{P1}Tw$pL-9v@&H#(TNBmIImiHCVxG&h`>3O;45XB3KVXc^D*Il&S%4e zw2T}y*mH;1#mEL~b2>3q-U(M*IhM#}C**f0g?!iv^!nY2z#2A4 zGq#0A3`^(}w&b8==aOu$3>D05nkoXsHy^{JvT-2lHY?OE=}^{4 zW(w;57ql3KyfVskww(@g2bokRFt=1UlmdF_v$a9nCU9of$PHwS)l*8^YRb@}irP$B zzWk!Yb7=S)Pu+rNDq%rwVEr~dnR|@hEfoZIE|z6Q&S0xsu|dA+n(z=Mn33P+!vlzl%S`4fKe`E&`_6K7RWljF-(je zC~uv@O@SKw_<)p^?&_3nj}-Fdk04WfhKh}<4K632`(kk9?0d`aY$4}%Bb)6Zuv=-8 z`Ejv8EQ|_1;th&L0;{K(B#@q%V}k%8UFI4X5*8R4nJUX?9YU^T63y#Yfl(Uvt>kt7lk#Rr9+B@8#_KINHNswH|N2 z3!-_=orr#rqX^#5zY9>szh`sDO4K5W4{)?BpvDO(I_~qi3f#``8tTL!fe+yh9zphE z6Is-7&RcU5^Q&)#rhe-)w_(94EKW3xV?_ifn(sg~u_|%yIL@m=S4(2ensKbH!l9ON zY!ukPb{ty;?#9KyINBmOg}vkGir_wM+g{yJeF~SK=(-(!r*L)4NgTNw8AYe~Qj)W9 z4WB%Ese)2y3+D6hT0RP|HpxIWoTgFe1C~d9FR+ z;tOj2i}=!Eh+X(HX>W1y6?}CUzDCNgr}3boj3~-)D9Uf*TLA~*;@kKR&masV_%0{E z=i>YL0Y6CgL;m~-Kjsn2rwSGd7=ymwhR1Oj70hiei~ zJB8~{;@RO)c`Fst(bdmOydYR;NxX>AI1&s(Z-$b9y+Yp=Jr>YE zRHA=~^!Jf&9>g)_t-wV;o9v>WE6Y7RgNuGK7Z?3farhN}&6DY5y689caaCJ5R-yYa zby1;v>5=JX8y>pYW#_F5&YRVagt~Kh2y`L%7^hJ5JlIP2%oL+PF=cv`rf9LbRXx zKJ=Ek%em+L=RfD3cfa}8i(dlJDxV0U1f>SbOq8QSFynxA$cn_R#9*Ye?|>b31r_UK ziI}@lP}uEhACJcO%6xw!`5&rZbuw9ZCk?w@z~)=G?{SI$#~pOM_MxO z(B|BuU{=~5jIpWZ#*&F{JAEh?wFRr&i;U!WEh1eLdo?RzYD`SW3_+d6dF+H6i|T=Q zS_x~=P75}-C)0xwCzXyR1_#pCusxDYAB;G5G?R|GqmlexTG$DL=y_^Q%)(iM(18&* zC-vgfNxePSSPwGW#MwAUFw=@glbM9uWv7x(%uS|81#60pmCiMaw4B;ak2=T1xtJ?B z_mG{A4fqP^NglKl!SOGf!uY+#2hcN}XX1Q>1=CZO<4}?Pt%+!|U#WTHbbgv!bEcVuvYi+y|4L?y_&l*pAz*aNU4L z15G9tV~OCxLZBIP-Be^7|8%>Sn{foj=C0P3p4KgbiwZ9Plnz|wGDHk4H?aaM1&fMK z(q%iTWWupqiD#GCsBruluWy*jM4P$D=EmYJ^M_xsDcL`|DHDs+kj<4?EttETUKh@) z)9w#DnP}8@oPkU{K3a{nxY)oYCN9M~!Kx`PA~=hntdDaFHZ-)K$~RM&yV<~c!Tj+7 z+Q>J9R=g!Wm>H%Mw;qn#DMd2Mwi``crVLxXsb$OluGTAew|4i0(2N!no3NQ~J!rc- zxkQqN;H-wmfLz_K;tm?G6Cg@gE4xaMi-IE!CQx|SiBnBvBN;S ziJj=6?uv{tna6Vp1F=N^rcphhpdqM2UsPZEj2nwbI<*R{IOX4kD-Cp+=thsAR_iUV ziJ&Ux>`L1Xm8aQ$pX$?VVh^t3GZ%5HYP=9ZL+qmq6j_X-QxdIDT_NnnH72fAB5<}% z@nrO%-LEXP-^BIGLRDKkyEe6L+0xoki9W#=zhgM&gk5_$l}uadSbQ|x=k*NtXY8<> z3=e1GZj3=FJU}%Qv2kU9u;r>Qu4ABI&{+tn#R7ryfCQsOmZ($|Q!DYJizZhEhyfFW z3Juj%PPSD}VB8ils%RYM}%>}PwW`*J!Z%%(?VK?b5M1|pJO?*s+uae|J zMLy4&_=F-Kb9YC}?w)O(U2XeXw^ZU&Ojp|yhpc$4KO9Zl{fYssIPv`%#;!@B9dJH{ z&@C~?>LV}ZVp1hO$22FvZHc5C_VPEoe_41}+_oIsdodawO=i+zz2s#EJ}50P1h6Bv&3B_0OEd#Ha_%{Ar!0k}MT~e{Bw8t`kNyXBmTj;h-?X!H~#)j=kdopUp z)ek*l#@mF*R#$S7YyiYdqHKe;mNtuac zWV62H&(*lE5;%Q~4|26SjoFv7i;u&WfIe=S^xeeQWW}awaBJ?f%&LE44NC|^b zQP^dMorpGJ&uEGYt=ZMm-My!?OJ&C0tz2Qz?US9z>q=Q)6;(Qra=|S@vt*-6Q4xYk zVY5f`1y)oXKKXK}ApBJvsgz2=1=&Q5I~YPK9FHXqhKDRC+-KW~uu5)25|U~|OjD+* zNU@@bklDQ=2gweZm(ptuDh*6GWroZoFvzH$Mw=!>e!l3HGK&tCjWVu^ph~_vQ)cTh z#xXnWk*omKa*os+GRKs2Wv*cT-%a&KFh!l(S}KJH<1zMB7^_^92jCt;S8^nJ2inG-QEb&iI5~tdWr7a^WQ;7s^5f zx3I{bq9DztK!r+11vkaHjP{hM9mqo&#Qn^=xfmw1(~f6eMVkh~m`@+$m71w^nh>0V z-Tyz3U?qehUeym6)Y@YSyCXB)XQz8q^1`fC2ZLU2z|^zHPYG5`?6vS zD=ZIvndPDALFT%?f~vY>g9#RlRGb}tyH>Y8uq@y4HgXyDz6bTjoES~kl3!Z|k zV|y$aaYn2`)=49WRz;GQ6S2k{uyUaG3B{9Rm?W)UzFg3h<>Od_+m&#?lwo9Z}ZA>`O z{QS;2cgh=K>QeeaPL&0fmUSsb(0dx_21{N=DhGwV(y+V?iq(>0u69bcsF-LxNceEq z1z-;;io5jhI;o<_1H~DYyqeqo`8!gR)0~ zg(na=Z?9lQz%tzvCy*4K-^Oq5F_w68@BH0tDxFlH;+Q)}Jf$uj&!na?p$Trst~aUUG#9&6OR4j66g+2qJYCe;12V85AMI%cku4U@W7`-|W6uC_htjlc~9HKQ& z^!AfF{BW7~0nLw|;|D>A=S|6^>+oDi%G(q=GjP@P3i)6NmGY=5?~?mw$bEeAcu1a* zCk=VZln=|(c^`mdi9?LSk#=n&j$#d1QSWd?HMf`JPp<^|-tXj1x{X_txa}-xPbLp$ zQhr@ZThtE|qnTG|PH++;BBYE|-mbf0-kf^F3DQ9$QqiO|sc*CXQ^8Rt{7JmM9@x z`Tq(&R`Y9`e%Qtb;pS$$r;d`}QjfH;Y@#~qsE#cs&{l$-Tz}`I2=lW0E7;qybPQKN zikU}ool=$MX@)(fy@cpq)r2|rt zC;T2yW#7Djk3N4CA6KG&(i64AfBdZf=q+!xzdR+*7><@*zqY)-tgf7b)sF$K8@G#V|`W6Kg09-SzY-|6uhXO^|eeR>K=M5a25Sz zACY!7=HVJF;4zGq=p*W)Jj&3A9T-3l&oc~Q2nig7%R>pb^X@^U@HlRuucmPv4!?k9 zIQj@@AI0mqk+a`~H*hn4g!=~_yLk5|8oVBy>3r#y9`<+x zTlwp5LVLAt*UNT9T-HB$w?g*N&uV0~TqS#n)P2;)YXgU>WOV$oalttY9|~Xcf#wGckRzQ zVX5-EQsso#tM``lT@CtUlv4FOEMN{?E`#*0p{R7`{4=In9>_f&RFCn1yADsmT@#Wd zEi3l=0hOXC@LHm>e(?$Xk@D5AE6@D8LgF_)Bof!Jynw$L!}qn*DnhIMQSPUobSx#V z|5o9ep6dHv{gM;-hZ5BOi*g=L;Bj3OLcD*F#QHFa^${ZDQ8eK(^7<3x@h5pa>nWa= zdYT77p3&%O@`TG;mKau;Vc0{Lq~rz)zkx>-HMX=Jt59+h^9;N?$F~I@A~HNeOlw;Z zQ&~cEl39(KQ;wWEo%p2t<^{d0lMQ)Z` z{7CU@5@Q9Ie~r=<3GQEW)cA&0tJe6>b86K@`2zlP48LAi_7Yy*TVJ-6MhK1JH%Ia2 zaVcF_u8c+=lM3?PEHQjldnB-slns1_B=A|1z~^uwj`FzU7qE&40@l&B+ejO`@nx;% zjhY$453XqkV$T)63$H?>HbNOkn5B(SuHDPbwa+?LCFGx7(Lf-#x5l#ysm{n6oQf=A4f3-VlIrB0~6L0!L$ zxp;+|evR(%ZEWR7;T`x6sr$RI@I4as_esk?B>ny<;2KAAHakL_-K}ll`mV797YAG; zgwm6kOV_B$`bHpGxaU-R+^g*Ijw1G$pRdMPyPW?`4zsDF-82I�fpT9of$b(AchvZ?l jCKlyGZ25?MKpvBi@J%hLVl9@*GZj_xQTdoWhx*?G`w}A= diff --git a/target/classes/com/example/streamflix/controller/MediaController.class b/target/classes/com/example/streamflix/controller/MediaController.class deleted file mode 100644 index 5c4be08e52344f9778a14343380937a150f4974c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5553 zcmc&&ZFd_-6}}@o7ItJgaay+xkZjtd^;@!W^Fo7NW5-St6FYHj$26pru2v&ylhv-W zvn$7hP$t&7AFTDv+_^8$eeRvv{nx+$@lPVU zMt>Nh9QEa?zd(m*fYFirR?{+F%d48VOZT}QG8&k3JSUuI)Hgn{JxE`n!8{EW=r9!+ z&Deh31r*X4n^#>T+h22TjN3jLxoR=nz^$ zD~QEsrTt=oM(7B9)dn4}nkbvuPuZsOvAtz>h(_sHo?a@@%QVJl>Hy9#I$Gu7V$*V5 ztK@POD5KZMSKAn_`d)Q*LQh1Y6Rp&c? z@W8BvVZ+?uqTzdjU-LpI++~#WA2511Z9pdpCVJGN;yqEIlVmU&f&0qqWga33!Dv0L zTRH!1Fc}Ed{W5oTvSM}1p$~Jf(;Im@U7$1cCL<7|D_>fR6AP0%Ex!!}4CwvvBm{P_yg zXiIo9?Q@lJ?JP(?O=t6Tu0ST8XY@v9u5W-eo1qm(NM(L$X&z~MvhzN!R%qWOy9`UI|aHTOW{IhXnRW`t%Fg2 z2^tw$nc%nRrQmP1lB`U%ggd!7JFyK@GFDX1s^jrnQN6^2Em`}FMpu2?a<{F(k>hx> zKdd=O#f2(~l)^S+Q+4hXx(>wtK=C%v0W8fuGXWR@JSoFb74vaBH{oXBwLB{R;l`8cVdCphT$ax|&a1Cce^1v^Jq zmL=oD8c)-zTSx5X^12n)TI~nQZa!1NofUtO){$4qmW|`_e^2j$I|Cq+Rjhn5%B?%g z#iVNW6fYZ&hcc}z0vT4xk5KvTl2zLooz5tc4(-R+WT!At^>OagggRk9R^lt=C*3(R z(oSh?wz)5US$yw$ed)%IE>}koBL7g|d2q))fCWZ>{J%K74>vL{X7cD5iLYzi5bCba zXg-;7Dj^Nafm0rLfMqGrLN&tIRn={a<!M;rdqDsNkN#5$UC#Gr8`I)R8k{cIK;hO)=o| zmW{ZFHvK5D`FrpOCz*1x{W5+|zNs>Lr)Mv5aS7Yoq5dFSRb91@)nU_WIA(LkRQNL* z-{+zbT2*0gS@J~g_i!b~)k%eeuMo+VPoOYeypENP&~FvGzOfjVhs3qs{&8(u@jMk->CfJfaf8CRr)4YW-+=ezn$PYGWj=RlT%+% zescN?8vZk;ax{nEQR>I<3H+XUZBJknqsk2Y%)Sk$T3YP1XryDecB+2Yf>KzFJ#RP$3i~#hJOLgpBfc$VD z1T+>do;vehCmz1UqmhBf!)!c)y_rwrArv0x`ksSF2!|4QB9cQ!P1=d!`2dTO7h^Hy z-&*?%O?^o-(|@N+Ick$%e#|7=Lap6Nkz_1^XpA1AXz}g>lI+qCAiV&2l)@uCG$4-^ zAWJY8m%@JSGV|W}-Wv!; zyV!lPTDR`iwyv#h)hfoddu#1x?PgoM@Bc(U{oec5ff+Kyv_A|p@7#0GJ@EAU}L6r%rQfL`1XHIBs&R7(n@-AOB}@uPY+m*JY@+FZ|0W{jg+(lTA!%4E2$4cpeFk>LkI38T84CmZGV zw2|bDwEhxy98V)K;!kL=p*mWXpn8Q?(;7y*mNc8u$~1QeW_`yTMr#_I`sKd!u94CD z4ada_YiV7A)+=;6ZD4e+Wv4YKXB%dE($=$l#{_#%OCve=o(xM_t{`1H0;jLnGaNJw3gHjLs>& z#b}CzcDq97QX{zK_-3weENAWkE5*wPa$C2Rn(fXTnH0CNY6CSZw1ajssuA($j-#i+ zy|qE`Ww1tF5##n+I*+sjwJ6jo!oRyD6ouiBugIus0_!JeH=`{P>U}sAX+6_rr}J3^ zr~v#66lxR5R}T)2U3YNk&|psuolng*w2v-IP`g4M!u!?|;m!h_jCKHL(B(d5M)(bR z?%)h3u;W4SR+!qa(3_~!qgGd%kLWITET|P81Bry>meB83=zv(a7B>2_!0eEn5%OMz z4$>uz6v(>`+nwsc!i-K2@N|tV*ZQrbo)H6*)qM(GMwjCNCgWZ*vg4%oLl8Sb(v{eA zfyTUL$~_H&>;no73cM2sMtZx(dV7RySfML~thT$W=em*JD-QLJj@8hp2*B8hus@Vb zEx=yP-7o%hi=_2th2BC}0f2m5l~Fbhz@poYVzUsCa`!Ggd8`d*ga058Bt!5rs08#fJ4%Dw@OS*n$l& zX}yz)Lg27G6P7|bx`EL;xp`SDBlnPCw=D3lNU^FTMZWnzL6No@t?D;SKA6u=aC=Oj$Uw42W`}XzHpH_pta7Ie zZ2N*z3oX`Kv}6AR+?}#g!@4NbF1KN72Pjl#0*8SWIj=dv#Ci{>s4HN!zR+b*)OUN| zJl&2`{?Dt*`>|y8sF5~NPHkW^S-!I!C%vRtbmK@K{Xv$G8;$|=yG+wUEk+xFjW_ih zmgdap=`D`*8JDMITXhS(}OI_GWIQeW=x6%5nx`exGc2)npE<5mW z*|f+3f>Mj`s;B|XSAqeL$Cc-|wBW3a#?qf^`#Vd8+X66xsn`Vi(-SP5Wp@lWp`9+!)%=)+H}!%NNeb7%j75$deH+k z!zZ)}!%W4vp@9Pfa9K1_jM^7uo~4|`gijvEG03JLqT@$85DY6#km(#H`xagFZPr2&$9gNn_GT#&rzWk$Pt> z&N1A`_O$FqW$u!N7%AH{aEM$>9-$CCDD8nC!(Libam>|8D{o@|igqkS<7QNv!i*56 z&?TjYaF*D6Uyo`{7JAcGHj{>RcPgk2Fm`Rp(o?$eE&r~71rm19E$=RME*4pdk^Vn* z&|}kDuf11w)#Nh)WzZxQ6C=uY$z+No)*9>Synei}r<3$Trd!izB7qty)i85;S4E49 z)N?`kVYHlBMrbJHn8 z?~#{~0x#x^MDh$0&VaaW zrIZU3QPAoK&M|VANToNtO`lC&sgw+2;1SC9e37V6F}5{C*s8dhp?<~hm5esTYm^ot zi7wT9Hx?`fL2beC(%a*}RJ$nLr^gF-f#K~@`IdcbFwKz}H@3GeCF^%Mh0)~;LlwO( z@@{8E*;KRvEnwzn5Wce4*o`w_~a>eFf?wxnph`&1Y@^3bi0s$lchF_N>0{!6O0R91|k2cT{G>2(BUg`=G zH-pj}py46CJ%(>vh;JJ`4j(;ym*eUg!&aOS58JOvY{w&PpA?&YD!{fP#CA@|pDqX6 z<3jTpdKPOuhYwEcdVD>PkHEB6U@9J7z=z>2@{2y4uVdp?kab8nLC+C<4&gICM^_H+ z64V{^sJknqE(8Kp2;$V;NSh>eH{xGO0_rw{VnjODO5#@v^yL6or*)Ku_7skd0qw)I zla5G$_d|1?PvuICdQ`5&xcGIi417ljb%#%7cn{FOjve}>zA8@Y>I$e46aw(SF`wAq z6wrMu0Dm5_zg-Ua^N8)o;yXZEWD`N`ASR$+5R*+JgBI{S9-?c)jPfYd;u+=YHP@Yz zjB*rdaTX!D5jo=+GRjS)A~^>D~&fLmqrlFflvFYiYRd;siz5bS;k zU_T6YKO(~r)XP`VB9vb*Z$aQiDLb0HNzEZM+ zy7X&!E-L8LAfHJ2t-+zN*Gw-V&*T42$Wu@UK!ZH*L4F+IJcH3^!RO~N`aHVG7edJE zL%yFK<9ln!_tuc_t@IjNlrWnQ72*49@ZT$?zma^8rI+6dkbhSIa+F?XPC)7?y}T}w z+*~9_`zibPvNfrsKfsj=`lED11+;&{^Pj^k^%wdpM*rr}--TQMpnqbt2BYVp!T8tm G>i+`5Z61yQ diff --git a/target/classes/com/example/streamflix/controller/ReferralController.class b/target/classes/com/example/streamflix/controller/ReferralController.class deleted file mode 100644 index 56d1986fe9003feabadb00182412e9d71c634d5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5433 zcmbVQ`FGUT75?5}JlGc4FYyE+UJ?)v9Ly(=xGdY zo2L7|@B6;*?P*Awp3~Exd-^x^^n22*2ALs0*dsmNd*5BY``st~=YMbf3&1q~+>Z=4 zWzl0{GkOJvZpl^Yc+xLBvx~P>F%sxK=lX7RUSQM6=t3X1pf8Jl3j?qOP8EZSqwdH` z)l-g+LM1Cpo_ohB27VLK2v2L_WomP9&OU>KZxY z$Tkau7!ug3Rk0Sj(c1OM^|Zivnz0(6JB4=PTt9}fJ&PR{c4C*nv4?Ym2`e(_NL}$) z-AG1m;0v4_8BKGb5|otZw1nrBt_HqVxh&oy@I(-n9bFAwzq}O6in<+yE6#EhRT+IE zH|<9Zvk!am)-2v;VITGjJeI0U`M@vJ=~0YjF>c`qj;6yiDFaQZYSdngmugBA@mslRysS(B0y{c3DnzD0F>o9U z6F5!~$aSU!^6e?BtE~4n6(%OyXswC)xE3cZoWf~=Ot2#GOuO}@QFEi|73qXe?D~@y z&f+P$&RA#GT1g;Mnt6>2l6Q3!Yve4P!!rWgV~s{zH{i9ak)v2?wrW~Idbwe;%-bzo zz@)%tS*?0&0tY%omuh~|2s@YR7u;+L&t@@g;S!#sJ8_LW%#w>LvT8alNooA>UPE+L^~IX&@i#Ys8C=a`*1|QsAaLf* znPGkwjX}~^_pZdbDp;7ubrN+DPJ5boJ$zquv|6PO7=fE3X+)JqyJ_-qjysukbU~me zX`sQp>#M7^%AyMA<)TLyhx0*EdJ8gi&3%2bCt7xizcXo@Y&cbN#PW=amV?qYX~;8T zg;?RR&YS|r)6AxlvOr(KE&JrpkS-tVzAWd)&eNSeb0jLaq8423GRR4vXp2V#{W^VL zu-#UR&Z6s=9NCn0t_3=pVdogmAx2iES2Ou@lDTu#Z5uqJc{gzMZCNgJ?yjD60;$_9 zX=k>o!q@>C%IaFBB15+0*i<~hZ0XxAW47n|D|QqlOUe#N!6u()9&-!U7P)e2E*qyE^K55`#Czm9+dBQS6cwR6b5Sfp5DmS^b!$SA$HJ1SBVo+M6@$y4ZXxn5rN04)A z?7bcx=W~X8Pj$_&&++`zq}M^Gves5@65atmx25N`RYTiXlGfDRn;9##ag7}+9uL(j zi`*hpn%Qm)H{N=V>iU`d;MB4zuGsDpE#8*84uF!qxMu61q5_|+b2!C(!^Q&INNpBNbONeoe6&!)tFOSNq@wVRo7WP?leMT zzD6Fvr;#jcXI`3-*yD5BlV}=IQTupv(rJ5%om7#&ESm;x?!fk4SD~ScK@jm3R#uZ! zgdW26!Cssg!cPPWsf4p0eqB>cj>do1kDucgS^Uz%ukdR^sdYvpdV&?@RWDMRPV)2+I?TF@qS4D-rx?4 zTr0i^p;3b3^^!&z%gJKq0$hsR(pZ6~QGrjv3kYbss__!S3(%B|inwVcGe)vzBv-jQ zf!pYf1Gq&SCNFy#z}CBXbjAcSeitK0?&8?hqp#ycD~xTF1)gLWXPJVh;!wsC?Pm7& z;ttlh!xrZI1Nb2K-N_<-2rpCD}lQ^5DX-hJ=2ki-uTf} zoB=Blt7jl7;pgJOo@Zd6=hLKwUuXpO#YSK(SG|OBU|-rGFj_g+bZwq48`t(Hu08z* z&I5OG@pWAO2l9fKbN|NfR^1ZhUnR_D3E*q&fvatv@9*%u1-_Q&Uv7B*m4@e;PS3x( zf#;ny_!{Bzb-JFxH{ukT=KCh^ztv3BxA7f5{jSFMOwGQJAMmM3@lPrFL;MIo#&5VX UOqm&e2K#MqAAW~F;7{1|e^69o#{d8T diff --git a/target/classes/com/example/streamflix/controller/SubscriptionController.class b/target/classes/com/example/streamflix/controller/SubscriptionController.class deleted file mode 100644 index 74611f8c75221ec3126622b7c30051101e3de7f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6910 zcmcgw33t@i75<(L3^pQ6O+o?*WCA331FAr9Oq^IALAw5!{qFbfy?W37`{L68 zhVidC0SVVCsrsk5@+3#>R`+NOU{ptZB>WE)nZErWIqtDp(o zT5!s`Ja3dMmef7pl}35mGSBM;$M#*vvZSl$t5aUVH7ma9*n_oUfwf-jMP9nIW4S9Emy@n(SQGNWs`LnzR+4^yq2N?)f21hT(b+Xx$8*%NOTiM?XjrfG zw^4f1lITIz7Z~eYY{lZ|t~8LO?Gwrb=zSU7 zAh6AGOS)HaO}jMh8fAITacA^1zF*NNq*rllPYwrD+pq;&Gq_R1P1q)|C3V`JoSXLz zzsi_p1}BF5Cx?dwZX9i50zu88OA)wR;9%U@kivbdl>`?^kM&?~UH9<&j1xMW!U zZmC)(7{liaGGKB$day%-j-3MSjN?)1c}9r@zA_Q>ysr=`RRjG)rzeJw zpBT*;PwX4cco*WBnoN8cy9@i zw@pW%HkncA6LB4qwkeA-=m)VwA$o^~I~AgBN5&@xMuvul$1-?v`tFenvD3#K{|NVJ zT;)ip@~DQfK;_u@p7RNLk#{{>S3iMom!8d0UunDYNh@9}iv;f1a2gL#3*AYzCtszxOiqD4QyL0jLThu}Vat>xBLlx>uVR^luf6IYQ7wOX>6^Qr-NrmL1UrHi@@2f7c^ z_Vp9&9L%yjX?i9D-f!CuOCr+)0{toFGDK zmyl}$+b>~2Sr$!0SDh=xxo}%BFgaJD@mZDARrysJ{H*8L?3TU48CfY-JS5olv}-n)~%#p)58K$*pyX;-SW_U4A!>ztg@r%byTBZPj;@qF^QqWTY- zr3aB0*qEf4@*_dB4cuijB!Hm@>xQnKR3=c%=H%pB`bcPK-o&Wg7;uVnRK7OqG~x+e zpm>Q*m)^x(R(fhP>TP>AmS}>eUG!ovw@Q z$oWpL!e($$8qpVC8+J~fH$9)sONgj>MmW=FXF-bZRutcy*tFHK-CV4kyJ3&&^c;7} zEEXj<8b|hWguPrYXyz37jXo9{?u}|}Ypm3xHJSU^fHcz4;KPPdyD}t2vcY06m@L}d zU{Ng@Xdr(eE%J@(pehlknm6&Xb#Gu}mn+ZZjG)6!#7OoH+h&JH+^&tom4686hp7C#fX zso{5SojTFy&YBfCl*KOu?n=$jjW&7n87PZiEw-?iiS z_(KMN)bJ<#IdRBg+Oy7#)JFr}&{JcY8NgBP<0^g~)SsSo(h;2)E&@wzvdKvtUWxgK7 zgds`yGWdtUrlm#MafMjm2F9U<=Kw7{T;!Q4?>f{;C9f;^ExsH!-#C0a#cM0CYr3C+ z=T1>ZQ=MA1xBD?X{m# zPAqu_)*3TyI~Uu*Q5_qxGcc{2+Oza|7UyWjMy`GybG+kvHFJCMGQ6A`U%^o#a&LxO zmBGseUjVPUgzQUcmpTC@gB4s#+&%=%^u#O#@~v!-2-tL zZ>Q`#JiHU{I*fNy@;wEdQ<5!8^1Vv(eFPxTAUu2kALJTs5bJpP5Pu)$ZxzqBf`^ar zLExkKSOnbjbWO!$KY?34kG)3~fW1WTws{;F+x{dD)nFUGfFp4jUvdGX0gMWW_1J*H z;M$Fm2+mE&aO5C~bBHI=hbhrdxChWp$sP;^&>o~6E6ItE;}f*-DiTS7+)6nG@+Udg z6Wo1@ceJtw_kN=KX?mjI=Gp(<84QkRxbh1i|4aaRnJOd5KdS)y+ydmSHON1&#=p=E zhU#dmm%j)qf_-ZWzUuz-)$MG;tQq*v-i1_rWHA*VRc>5Zz@;UL z%eR`rB?96xu0b-BijVWIRCClN(UhZ`LPzCY9j2n!sM?|=T#Zg@SFUDSSfEjuFz%xV z_jB|#>F_|!<24aE)SOL`@6itADe6e%>wMtrV4VX`C|{pU_!?}cfCJxN9NCLChn~u| zt4hRY6}c@`sQy+d4}#4wqv9ljT%xif7@uYMF0UB diff --git a/target/classes/com/example/streamflix/controller/ViewingProgressController.class b/target/classes/com/example/streamflix/controller/ViewingProgressController.class deleted file mode 100644 index fa2e886bbd50396598e9e951c7015163ea5f265b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7209 zcmcgwXPxAZlVzhgKkxumuofOf*IJr77vI>Qq&OB$Ivi zec$&zlUavt-L0#>-j=XXpUu4`$_bZsk_6Sg*Oh(*K9j@wq&7LIc+=uxP7A=)=7 z>;)q&6nbblMy}(HSSi$>*xNvxsWCxKN!mhN73z+WlD9GWi5YH8b z18b96;X>D0BPWJ33T3~8H#!fqrxq#j+ItnGuO$?-_ z2Kp7+KU~EGK);9D*i+jTIuo|v^VQyJgq#B@z3tNpIs`2xwWeWd%(s?Gr{SUf!Zbu? z;CfmVT*EROsDnC_bc8gYZVb`0aMEoN*O+t}qF#ofJ4r_wgZja-iL*of{R5*3I;POk z7~(}~C2iDl2d$zRUb}}nS>!`WdRU4a9hHc96QB*bax#mj|4Mt4Oij)Q<{hOZCn zjyrBS2IDm;5d0aCyFG%>km-u7ut8xT4JPSPUT+ts_7`oPr^!GR8Gkm%%utdZqsJB6 zT#PKNP&UlG49Sq^m)5E9>2Cd>y=^2(qco;ajWw^(>F~n7%eBY*Tr@ync?&&}qzRgY zO?lm(PdS6gtj?T3o=Js8OPlUi+k5E(($xs$ij1l|>Z~uDm%W&zC+U(x_i?&f@-y;z z!*MNp30_oTZRuD=*T`wZ7$$YUoTR7e8Dwn;@iU|^iNhI$x2`E?L@ywtTvh1)veSa? zqK(uO^42TVsn8>Baj9zUEQz>QsE#9Cp~hju6r;ubjIgKlnH;`s9k$YXZd$huZu=kW z+&KffpN?Cwnw_i{`{syn=d8@Q&NDH6nih>8;+KVBw1GQz0$RikJ+qkOExx zd$F>18ib93MjWPi+09ku|Z+*su_L zWlZv2LuA!doa#6O{dY|iu@UCQwBZ+ugF0P~Y2 z$zG_|9_{K@=z+h` zExz_}F<{vkW_h`>>Yi%z63AK9P|^h2dw^&v-r!O~HafqPth3R7JUXL-i-rXFhaF~WUl?d!9atM_oHt~75vY1J zz`7K`tH!Q|x-|ys@>Jk5?h&J+zQUq7E+bu3f{?dHhNfZCD%xo=D9POI!v`TNuA`3d zu|%PVt4=N`h+M8Qyn8Mnb2n4vZBy9?c4~UT&=$HiAFNK`DD_jQZJnaT)w7N^rE@h< zXX5aIEzh@6Eg1alu!Et3e!>_=*p2_uzd4>G(Q>)*$}5 zzoFv}DIJGyQ{pYOYAB80%~XruJyb_3)k9JSt!8gD37SO<^Q2T9;_jfO=`VE2XAO6o zsuj^%aA=XPJG4a4L%<7{sjg4<|{sJ z?}N5Fd^;g&JG@LE#xt-?=SMr=rr{f-%v@)gtWvu~7Nim`RGTF^+vxxx_eg1heD=95 zxG-v_^R$D8(EAu2q{jm?w!k}?j9T=1WYnUc-}3kdodOC@&{yee(3`(m^mVMtWZ(uI zw}FxjNkn}Oh7kso$oR%eGQP=ReQPxt-{$Y%`Ja*T-GGem1!O?^5*aNVh~LK;Ecyfb zA=C|XGeJKBE*w&C`XQzHAvJz+>lBl+UIa-}aGx$o*^HQplJX>+;}UwGq9(eG-lq{Y z&qz`_uwau_JC--^5HGR9rhp zKz$EG{C8Sg*F;9Wz10!lUXo^$~fF7Rix{}S-@SNa=z|L)K~SjB(-7ql5I A1^@s6 diff --git a/target/classes/com/example/streamflix/controller/WatchListController.class b/target/classes/com/example/streamflix/controller/WatchListController.class deleted file mode 100644 index 7f2b23cd8c2c29a2ffbc40a55ac08a019a98dbea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5231 zcmcIoX;&N98Gf$7R=_e~$FUPH$(Yz>F)-N1B&{$ti*RVf60z8h)4GFn5o4qoWo87X zuAA=rzOVlw^=X`*_MCp~hyI%Wf}TG2YP4Yql%CTMgXZ4*-uJ%ud7pO~{`H@~{R2P( zf9ga8ZBewxa0EvMdLCLkmYK7hthu!DP^JTcqZ77c2a^JA{R8VAcmW+zbjENDF@YOt zH*d;ER=$vvrXP6H%5UcEM`qe}0?*Cmq-Wl-g7j9>_Jf(q9FN&8&#p*s$4*OutH~z1 z{E|dfQMe`Ei7s?U(G$Z7oD{g$WPjex$ecNq$*j5+TT=4m|vZi0~Y$v8Fq{b0qO7U>9G})-z?8t5l-`~(HmShSk!j=@u*zjKG0Y+%5OKE!Zq{lH z{Wj3tarG=C1J2-f3~yncLEd&Y3EG#0AFsn*~6#Fe6(8+7)jE zx|6me7mN7~>8)BDInJC+x@jx7Zh5vEhm-BWmQ8-Y(d?*$QyZ=9S&+e&n_0G0%mvb; zvIk8KKOi~9TZfi})oxQb3ACOj1O#EYT;j#Sv83Q{2FU?oz-^amXxEJD6pMQ0oZR%B zgTN9v&eW9NUa$(GBRf{?tixpG@roP&Uk6MKPVxq?q>9X^dAV-;Hu*H=I4+YV^AH&@ zkYuLvcdcxeC2{Av=~}*NRRzqYg7mbkLc%ZR^Om>A$Yj+r@~Yq%fm;)qOa&R~r#)Mz zIe|eXXN8h>AoEJxG76=_*5}u49Y4erFDwqeIaRRBG3?W>BRf{ENHA~wFABInR@PjK z$F^&uOs&RBQK#kexAXr%WU>#i!()(FI(^W`Tgg&|F98;8^Ng`U9@)`)n?)fL%^rX_BFhpiY< z*T;rUOCv8e;+U=&FWr`bQDpP7FGUZfBzsc1BMmE;GgLzNnN*r)^o#o_RN9-V%lcz2 zyN5Pa$#bd^dDu;_xJ555=h%7+yj}g8NCf>W{gq(I-r+ zV4FKvP2Ci!R#u?@kQ5(SS>If>RBml|*tqeI`dUV*r|Pd1Xy4d$JtJ>9R+gz)d#?s) z9~b$B9O1V$+e`J0%(r&FAHnB1`!+{&{8j6ngC9c-Uik>oKXVj8l7G9<#=jT1<5j3{ zhXsyKm!z?XC62gHYZX}L`w=z*{1N;HeFsOi`hRn?Qj#3}3!+z^;W(ccpW*bv-*Nul z&{MqnF)n>r8a%_mQw-ORuRX=}+VPuD@o6>I8qe`_ppTl*VF;u8cLL*FQHJ;O(%^g# zhAB0|E0|hp7*{cYYq*K)`ZANWvWK9gu)=FyKsQ#g#KEy3Nr0lkj9P+o|P|{6fMM*}KWI;*3Um1F8=vRh;8Wyp`Q+N4vK%C~!BR+e4 zj`8bB55B|!lLj7>4T=dL(N*pIHMOgTo?+tTt-oUW!$sw);i`AaKqK**65G%;2w?d8h04_ZJlp4p@OuDFXIW#RC8b9TSQ~d z{}K^vjk(6VBSJAypp5wk8uJvDg_wU;Ve+*y=4}D(lv3O# z&is!<6=w#x!nwrjA8U;BRg8b4@c3yR#+ug}&Ec^yaN9(H2gh^!TaF{gTmg zNqVMw$7?m=n{Tg~9#8ncD6eA&?C85z%A`Eyek($+ach<(G` zHG(~1@FaP;IKfEE6-FmhJzDdeX5EGMp=PLW5Y@%tm)mq4j(F)kL3l%F#^AQ?3#EE2 zO*ED8@5JU-P+N>Hm2`>ias<_XUw6zm+}c;3+u@-35c^`y^?V(djLYkS(Rh;E>K<2n zjOHKUBWxo%?lC({Xx;fO}c=29CvECAdN$72BLDa2}xXu2D| zXrPPr#;qA#*O#9R4LXB4JAjtYVZ~fU>z~E$G!4=@e9vQlLC|NkK#Kq@DJsxKMa%e& z0Um?rvAcx7l?s`3xk6VG>{X4uh8s-r7<&qeuS2oMhtSUzeE~evy1&FBUaKA`BcQ%Y zw0^C%egh~Tqjczv1bS0LzXcSpQ#$lk0==C;@nWSz?LoZ;3;d{LB zC$YldN6P7CTrWrTa`bmx4$vCb6A&U=p$vYFAk>Et<|$gDS%hyLH#z`Ngh&4#zS_ur zNNOX$;HwApxqe0p7eCUm!tzI&YnwZZ3=@*lYwju%a}D{v9-1{G)v;L^iBqh9=>!hY z1LzsinI{}*W<`34RWmd3)vU9HKWP3p{D@rD(IMou?N+ws)*~v#Zr#Fn0JtGdf4YFWoNlLbdO}ZOLkl0^bgY}x8aS=@ z;S@DS%t^yYX&LFH$*uPcPFrjdyc;1x~N~aH@pO zA9JdNoTgGa9qHy&P380|ozszSP8K-XeK@`DQu&&MoA3CDg1*~I9!Fge2kZ~q5Mk8sWa diff --git a/target/classes/com/example/streamflix/entity/AgeRating.class b/target/classes/com/example/streamflix/entity/AgeRating.class deleted file mode 100644 index dcf38027a6bc1175aad22b25335cc61334237ecb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1893 zcma)6TT>KQ5bm?=g~b&V6)_MT<82p(kr2&AgAydxR;|VovQ_5AI_xyV(YdU1G4h}C zkW|G=RUYyK@}pAudS-wX$4VadobA)Mue-mVzyJC3FCyBchZ9txu?AIJRHM40Q!n)! zZF@R!?H_ht@*q)Ezh?rItSK66clu2lr)GmDT6BV1imner-{!a4A9`%ZNyOUU^~_tF zgTy5J_PWc@bYcQmQO(mk>?xY=9c9=`BIxc)%*hr_(J3VJO#m-Nsw>aaEjmMI6;+G_ zY$L9{5RQs2JWG+u=e~)}j>qdk5GF_%2C<@9n7`6dqU|9^v56B725fg7xLqk#J>q~P zov_o_Udk{VM@5j`{r~VlzU}>C=Fxq)`DDAh{an$_o(b*vXYINi*>6_uP{+1D!e{&J z7;SG2_So0Rb;99z5SgKXS9JNuz`Rb`GLCd|*KlNoyH>KtmUh_!sH~@rqVwq1ilX__ zqBcS=^#ds;NIlHJ<1q}L&}okYH`x;j4L;wK@suU0J$L*mz|iP*7r!w5J{d z_dnT%>So#_@d6|Zze_*SXLPwsR}O5iitROM z@F_TEdp)Lh?oh<-i0vZ4V4hURc?0QJaVy-l#Xm^>hCda$iBrCDM6*-{uqFHxATXk3 zvT;j*Td0j}X!G(nui=*R>H_!l5z@Tqt0?YT z7GWm0Iz(76#wWOH3W8v|Lto-Nj;V1RQ>PTuSM+rmrp9qhOIhosst+;cEGaNauM~Yl zcS|sx`~}LrzJ~TpdWxrC{2+i6RIY4RlcCfdSXx6oi)4T zw)s~@qN>CPen5W|;*58R?bH!MmiNx=oO|xMkJ)#B|M?ey1KgWM0aFzet08n zW3@f=_hVzZd@2*8O-Ep^t8Qkiao3M!>dT=%KT>Ti{iC3(BFR{_ zCr1C)SDtRWdYx*Oc=4%cyUGi)!g(+Mtz0E9vVhnV*qE5hejN5A<8mpuM>3I-vTUkAsiSOzv$0gGkND)%QA)GJqz=I*jY!gbL$N%Lza2!2E!Mra|Y`J zrqv9cGr35a^cs;ResX98>e%7a(fhQSzCUo~HD6@v)CNbR6f^K&TF4*S|EkM=h)@*qNd74P1_SS`RDZ-(=<}6OjC`T>X&GGuTBMg cL1hK(4TfZm#lFM6>*$v$zGB^3&JJ(_?V0I5J?Bh!@2}rKeiBiUp7c|MqDhLSDNYH426x0KVOm1h&GoGv zS#=FcJWyJ>O9n-A`AQ$9s4q$VX*xk^gJ!CB!<3&zW7m>q({-e1ytmY6Q)*YaZL^4T zyDge8DwIsq)cmnL}?PCzQuiW1C5T&QZwQ{Na)}RZacf1n1UAEUX2Tz7Z zys(vCv8`4^BP7Z1{Eo$^bjz{?G)7TqGU#k5_yCx&q^`T$91L)xrParl2Q^TH6}yHa z46G_G*IJD&>6AG!m&L9Mt0El5``|F&7cL2p5!a2 z`YY*f+qKuiLF>4vB>JN+-4bLC=kgtxpe5o>-#L=&_Hni(pnN@g9pL}yUO^Kf5) z9ca=dUEHKgyr#h*g6(CTc#f$OO=FF5@AA%U(iK0XA0GU~@XaCbBCG)$FMKECD;5#T z>CQ7O-ME2%=e93qC%`jZA@jUa?GOlJz7-X#wj@6afSm;gHFgbQNDFW5W7szESoI4tzB% zmhk>zO0LoMK*I+2&&24;s!R%X_0Q=+s!H2%_$$s z=_cI@cPox+JF<+ z&!GFEoQAqN-3{gRAe>W2-}$wB2u@2qIQgRzaN>T7&_nOL!RR@>Uv22mbsXrM^aXhFpx8N#-k-JIPG!gun8 zp3@%B@r57259RScvr7V-O+jCM~ ztLu(c5sV^wg;9V0H9zK>!Og0cx6Eo*I=hylH1#8jBe-LkjHdJN_gPSS%62eCLp01N z4qO9K2G0d3xVG+x7){Z2y1^)2Y1rJ=EwdoXmRWHav7AKsAVuTSP4ZZ;2e;299;{3u0{BR@D}cBjC{AV#C!9Ew4LnmeJTw1L>@Z zlJ4jShFCI9%T*LmzX9Mk+;+KE7q$a^VU~rKtHA6|sOXt61@<5+C2lkX)RHJ0*45KX zEU@e1XYeN&Yhgay57a<^fFb_uT0yUoHb zT+*ebi@lLu1YUT$k?5i&a}JGUjo0gZ^JbEnMZDVhr3pC|U~5M9dW(Y2Oc zKjP+&s9DF{kXdDn9$zi1VBu9!IiJ|riaN#Vylx8bAQWY_%}Np^5w7UXpWzGq{sfX|w5c(2eiOcebTSAhtHJ4nd7Fa>)@`C# zCMV5$VOH{o7FN(~t@S zHhpL121hUU7bckBW2=55o9h1^Mf&e4SPi=@)^*v;DL*&!@~C&3CTr?$L^j4l52M1% z;(|!xswGO`iHi~=?KV~eSlz+v%JI zM&ICh(fBPTeuFSVE#v7>;~Z^3OByfY9l^V6cJ3V|TA}5CIYhGvIHx>}dRl&XiRP8$ zIke(9Hh3}0(`E=$Uyx}&lxd4zAVwQgUy$iOm>zUs^7_DMN-L&q`Zk1VAjtG6lxc?w zVN3%-rl(-ac3_h2#>i(HQcOj98NxL3D}?83w;0M)qVL+6#^2FMkZBoAD;=1;*7upN zE2i&5^E4LB(|Rb=tMELH1(`O$^t=O;cj|nmamBP7nx~0ip0+}n_QLZt5oFo{Q=tQs zci?@dn~LcN`q58+>(IWX$zYyJAxz$U_z9)or=Kw!WUp_-S@Zl&O17vvz25oT1>0T+ rHaT+`aaeexp$k*!NvqSQAn*bnAV3+b2vyW*O5>Nn`Vh*Pc-emefmgiw diff --git a/target/classes/com/example/streamflix/entity/InvitationStatus.class b/target/classes/com/example/streamflix/entity/InvitationStatus.class deleted file mode 100644 index 09d4c2f51458f8e21e6c3194ea50e668d6c3cb72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1337 zcmb7DZBG+H5S~5yLTjN=KtVu3L0eFc#E%*hgJ7CeG}1JYFJ@`ivUsl_nY! zKllUuQO0L_2NWzZ=8~PA-I-^ed3NsC?;k&jXp7d15FLuh!8aMHU_k>S~}AEo_cOf zkQ4y+G{Qg1&`S46g^`Y|GHvD7J4jm^c=b{l6-ui*E$PJyVZ&$ve)ser8EB|^deqCP zzu4Mo)|>AH%?+%v8F;a87~902C&76L!V{E;js6t(JRvBv8FZAOazh)n6Z`urY;vri zN+W1XuO&mxetMX(2O15pH~vq4u$Zd$X`1R@YeDK)${qxrH!_sIvXD_GK(3YTaeLj%ylR>0K44s1qaqwaC@G#@>#^F_9C*V&inxdOHz{IE59CiyQ z(|a^SxA$n4ayY>cgmn&SPhbUXcI7*XFW5LVkCp#5LX8aY?_kGp2%@{R0LuW2h+u$i zSdJ|+z{=`3%3q+25>L+Yr9J-kAyr9I;@OCG4P-i=JrRWT$1l`En6p4nRil%X( zE6X#yLw!{h3@NBnmei*7467t)IQ#!w7V4(6?5Zb4XX%`vB;ZUXs5g7WZ_9Il=$rMX z=mPZ#I^!r)Nmr$%2jl+0*fT0h&)Ruu@@L}lhR>g*moH}x*K->HHP0}0{xmCSaI*#lDr(zsja^eMSeETU0NaB3 zCHVUw9Z%|2<+!j{R$l3u0=C_ZU0PO_azMSXEzO#OSqGHHymWZV4*1I*R{b5B^wL@` zlY1>_G;7$p`$?8c%F*|ybz8c+JmRNU_{{XJd|6c_e4QyMH}4o#CNJpf3(NReQ<_oW z6D}Ic(d?q;m6hgi6Vx6igCH>}=vr(%tI|5m+3OY)OBNM`JjwDYFNPr`mll?nHU(+1 zm@BqnJ+aMN#R6!nB`Y}pZlv7~ghH0=@IiN2HPe(hjs#MlpdE5e3)VI1x^~`Rk+n~T zS8fjJO02_%v)&x1W<-Y049+yvEEOi@P66#66H zA6^O*QOl&YqU{_p)P~e}X?#@6t)>^TI1EvzW~L_YqVBegk|n*G1Glq?0+roJ(z#RW z@MS&0XXl}4{6XRfb2gtQEIKN(T2+Nb8%8 zR;%nPC&vlEjf5M*wsZ`hhaX$LvVj7wS33cf^o)9^OB z$*S_oc40$0NMZyGLdlZyGDzUwq2VSVzgJ<=4Hhp9Mb{WDg{=)5g6;s}OZzW21G;JR z=$FUKt~=n`uMI(c$B2gta35<}^f$8)4H%>cAYs7-P5ojWDk2?Bfp*OFQivI`WY})N z)(eW2?Z)0?bF4Qi04LB;zEM9o{~zmA8Xv!|sMu+;+=jHlf1iqhHfB4Go|`Oq&_U4oLh~{s3c7tdPTloZZZosCEa-CV7GGvW z`*2~G|F?Nh^Ci-PyN%|ks-s+pD0dBLScdh(WpqBQQAPv6ioJ%e2jU~$vLGlSVQMu9Oo@&}3Ee3%tH z`BF;Ip%sz;gg4d#MwF#hj0~^_3kEPTwqopKfRQ_&seNShGj)%QeWvqAcmvo$kgpX2 zc0$4)nt%|KJ~n&7{t%nr0`2*YHDxA!zHA9v$9s1I281QTM<%mD&+%k3I>sQH->Lss zd}tw#hJz)XQCuXOv=v4&kI_#Rj-vmFd!WUAMDV~z7znHyeA~jrVs1%+33JNP3p}Ho z+M}H2VmWQo%Q#N$QBIG+Y2gG;frcR`)=SVUdL6^5JIW~?%jpfhjpNiE<@5}kGAD2f zDkS8@=O^eL{T##Te3Vl*meVh^6UV73?}D@20H^0Ca0<#X*_XID$&p5%h#Mw4g`$60*IDg}`AM4h+m%Xpc0E(*XT|(S-30MHRBh#v+WB0JP3*x;vLDv&nqsLx zs+AH&>O+4(e^k{oyK8K0r>Iq5c6VmZJ@?!@_g?$YzkdG%zz&{{VgRXO3}%tWkic@q zb1Zc(ow}{8AoP`VPHcT{DL2$%)7n#2Ed_?wwX4GofmFUwHVheKJ|b|KFa0yYW=$Q) zP`kCY(hGSeEnB)Z>oD}&JE3-zRq`s*-j<y+S1a_+v=W%8HhlW= z+(T9I+-e{&-Qn=D8>*V}*9`TwET%9mkU7&;<^AZ}0#hB(M4Lg*WHF0(1+LM<&lJ4x zdo^DLfg<~4Y|;p|ZIyHot_jQ?G?+9;m35$>+iKHwy)cqQ_&nh+r5{SGuKa*_8$Hm!9=2?xlP= zq_qvISnMnE2Ui_=d(v$Zk#S_buBs240%<2s{BT7E6b`-2+7qX`9G|z`aQEiDYj4bH}z`$jZyWb1iw1M{AdBDv>o+Jy0jgH%T)wCb{BE2+Oi znfJ>;p6?P!i0&o4{6VOk^2=55e}$GDJLxx{sOGMx z-HVZzam-K4v94l?qjrEZAjKyqK8M_UTut&l$DQ{BT*v$~e86wc?cURc)+uK-F^jYv zz>SDwfnQ7Hy%}H`w|;_9Siy(*=o#`T5SJkve+Bt0a%J%?_)Zt!K>QZbtnz7&LAu*~ zW@7;E;4W9j*rE$#{0jz&1aigI+jrhzxHvcX8l%Oz^lMDExu%E$<}u80#<9R~79-w` z*f!xM3qHmtk*^#+CCa2HhtKdigE3zwO@=8Kr6i3n_&t-R5eNr?AOt)#BEQ5vJ}*c3 z3ZuXBZ~*NHm->^J&lc!XaW%IWQfl}Nwve(03JnkzQkhq Wj_dCUX1^@sF+X+#JV7bm+4~!e6G?$hVL!FvS!SPJw)Wm@-_Fs$|NZ?B5f!MCrWnOjl*mw$Mg(1bW1Jeg zX;=;Y;N*?0dV)rtC`)rKXhbWI?}32L8-Q5*OiIc-O^=JKJYQyn#j;3T@^IZzED*@YeUJ}a3!6V+h>{K zzN+aA&Csl%V0 zWm&doc*?e1K?{KYYB-*ux25AM*JIc9Vhv);kw?4Il8)iYT7@|QHi{r(-8j2M0``2z zAW?i?*e@5$zX)0_DO-2n8jXf@^wUk9vM#-9a~N`m4?{l^ zrL9-+L5o!R!dBL{ZFX7~bS5mLh5V(%f_lw~8nrs6X&@hQlr;%jISi^zGhElMDh{#s zR(Z`29E!GAusW?EF|fB7Nr@pL^^xQ9QE`7)(9KBX1@XyVBwu@e70;HOrr23Th$@MuZY3GYT?(1qqscFM46b`TDs)%=L$`FD2>lj(?R;_HOufPT;T>DERLI ziswdThxpZqQ?zu~Nrb7VTMOP3&5;1Oys#Qt$Hf)Z6L9GZycaPvN&;V8;kX5^V|9z- z^clt*Sl^WNIW1xx3U-5$?J?xVDnz7$d8aO=2_&x&n zO^gC#GXEEeKQM{WxA^kg%?>6=qsN%B7XZ-{dWw-1JOhFim>3fnXIVl1-Z`c6tLHSH zUpuF%p1yfV5ZwlHiN+9z3AzKyE57c0ARp?6rZ8$e<7hCGS>M4peTVsY900-*VUx*x zPh0ph869Kb&7U;)J2nDu*jb`nq$fYncIXLv#I{D*i@6^6AQo%zVBOa+AD}Myj)Wz_ z+>*FSF$j8&KcEjer3N`|L~`1p-6&3}K~4|A>ERGgLFI*r&d%7pxJs^2M zzUE_oNV3*t-L>)|Kj8kTyY4>K)eW>gLGM~R-PN_vIr}`T_|Jd;{ELWY>6bz3q23Y| z%2cF2K`(8{O=-K*YuNYJHdHMV)OX$SoM=)|@7Vat0QJ*Ai3ZDbh{}TAtobclJ&~=p ztL!ielx(fL&J$aCkrQp(_XB_3ah0HwtkwLE2W@Xv|6XJ&22QY{7s~V^y(DNrHq?TQ z9Iqj0{2=)Rsx=%f(=i$nRPbaAG>%qx-CK+TDAcIqWqO%W{Z0#(mIIfKkCy2bdKJdk zoFHn>$O!Vov3%sTlwI{}(&f)u#cO3cNv8z$ICTVZkD01(jGQjh>-2`8Bb$!ef@Q>} z5vVX!VJgsx9M`ToVWd^QS*EirFt8<~T9ZEu8eKpTVSU93oi$fYd7d93Fun&RBe42d z29dPeDhL5rUQO9^^$9^|a<^_NPX%zGz9QX@0<0ey#JaJ4fCTIcs+NSULvis+8eU=IP|?e7VXq=y;7oA6f z-Y}(Z$uRV54kyCea-!ybw~Mn=w`Lav9Y$rYB0H!Yt?leoLs{|q?-iWLrI}LfbCuVK zn#}2KLC5CNCr^dG|F+-stQj92FrZsxwG+4~xaEav25aeUK_{9~)V^jXqv0a=yxzLF z(MA&w>YiGy!)FBi1~-_+i>x+(3l$3Z<*QdOUAS}^|G;3;X?QZ~1Q4BqZx~l;-&{Xn zIIQe%y@|=Lm4TXA^0_>K=x@n(Th(u%ElRuPSd(E*)3%)hcr62koZx z7F5gMlrD!{2)goYl?*PZfck=3R{=LRcuhQ~_YaaeqWvyR`^^5us^h8oPHRmCOI(~h zBzdx}$iU&X`B;dW4ra_j)7uPuko$G)fLtVl{vcR7&tq@((PUVFnvkUt0xjjsU^`zkHNxul1d|r~NEVMDb z!9~9l)YMIVGc22tagirE4i2>2)0dc8eVs(IUd`Rs(Zg=V=(6JLrxmC+ZdknV8kV#= zA+8l>fYWJQ@p>?U`4=}goX2lrb&7gv49{__D~it1dAb0=#gJ^e6w+n9{o-`nu4hwkXG zFoP<1-wkP=?mZ%*=sx|E79P?fEomHXs;SKKGszqU75vG8-xU)>foHMuZxVlKl@IXC zf1PlE3-m61f;SF5K=dg+#FG^~0)iF%hG!q17AvTn+o4kB{0<$iT-c!_mC?cu4Od2s zJ2bMZ`y`cse;>N9&;U~Q0$oMWuhA$Hc8acRdy}zFV-MO*`uUAh%IqwiDn89Eorr>d z<5(BzGrSKMAw+-^fCLC35lsGb`U1a9-o{hlS*-kvPW+CS9%An~IE2*adA>?t8qc{{ zm`5LbePY+YICj%uF{3pck5Q>FeWvc2V^LtioFuK`H_NG%<#a2TQ;q6*oJv_vcfhII zg;U&Kh7-q4kfQY*PKUFc?&Wf7P&1Fy;Vh>`a9ZlZDQ;fFiSsDPp^Y3)N3xtA5+S9{vjXwq_w+p9u<{3_L--Q$zwR`){%5w5^IK@M;1D7^w3j>iG zdN{X zDET&@^_eW|Z@~K7F06Uf?`M6MmUmg7O<8}3uzZhI5B;D|gb@VhN34GW80XV`az6h2 EKjRE9e*gdg diff --git a/target/classes/com/example/streamflix/entity/QualityType.class b/target/classes/com/example/streamflix/entity/QualityType.class deleted file mode 100644 index 3a97ff11d4eac571f5cfe490ed1538736b9d0376..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1122 zcma)5>uwT36#iyg*cPc=+G?@3wOSQu;y!?;iBU8ph?f!)f0+SBI(09&JDBoTnrKY? z;REi9lRpCQgrc{DX-Gv9lUI@{;R$FbwY7x|ihD#A+#a22blen_k&Dq_c{Hyky zdfL+u{n~T-qk8(x>?VY;NIx*k%)FQ9ecsFDpMU@S3&2Cv(&#~N3Vj*$V?bc^wcM4K zBi)*{y8T*}LxF+2wrhv?1bTC`r8EXHltMa#6UYdR?TbI~JV!}aAhj!37@Y;C%B!dgMD9~dww$YB* zik@3D5|=ZW=?F~4cr~N58RT(==?X#_HUoj{$HZFD+{n=O+$Tn3PT=%9*=1-YJFvGM zHSfA!Btdhhh-XLop|l#x59}aRZdqA{3e_$qUOiT>^2tu6r0aoVgG?^>wY_6Bh-hQ4 zVKfSh4_7t{8_xyKCHh#F?%syC>T2Qhi4sem?Jjsuv+h!)&y{s1aXc=IEg99tmgalV zZ90x*)V*vB2;4UGRV4%U-_{-LwJh%ho@>cADb^DAg_`U6YR&h^N@%NqUiwV~&|*Ud z!5h!lg<6%iqbfzOX1nuYsOpW7l%}>Fue_rRd2msG~-?`X^TmTdJ<--1^mC)uLys+DGaTlpKB1$s91U6Q`7_wiw0_{t{o z+%dJ)YFe_9KTfvQbF(ED{jv&Qd6hNkGf^a-t_D4Kfipoj-!Kccyt6DDF_`3xuQ44_ zh3Z_?R9Mh7w%X2&Hq%eM45G`4964-_J#(kAjA_-b`7ui~|8Ig&Ut90e-?!On-A=jv zvmyG9x>DfAu|AELgPlA?=Pd&_kITRzeI5RAJN#(!4Y$LI_NQXfy5^Wx9jEGI_pCm7 zAOrDh!3>Sl?jhmH&WyBVwdwQ|uj!Z7BX$!Zw#w|9PM<(>D?Bvs2tA!i>=aW=ydCu8 zhX&J}rahbi16(BDy>N!r~K#WntH{x?khjdcDGoOy>S?W@;BFv2gG$rQ6V0;>akHuh08vSP%fTl2Ea z1A!H+a@9U8ezlK&`k4NO|3utqQ7{|(eDt^ZSi|>}(#ksNrm8dt)B7CgA7H>OGI6^j z?tymP191(;abKXYz&f6A?Z!0GjpX*y zFuh<49nRfUH>UMOOh4kMBuu8xqiT$DS0bioM_`HuMU3gR!SoVa37CevF>NMd`WZ3_ nQ^)j(Fuf$Ets^i+Gb+Y3Moc}}Hfz~*qaJq^?iEpTs#X63)E=$z diff --git a/target/classes/com/example/streamflix/entity/Role.class b/target/classes/com/example/streamflix/entity/Role.class deleted file mode 100644 index 3892bcf11b4ab9f2e791a6cf34f40866cfff5b6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1275 zcmah|T~8B16uq4bAebf<4Kz*UZD_|bxI2rxvj+Z^CK?kT z`~m(bo*bYkXt5)Mv9cHP@W2cCQjrR>3Y)i-FHVP zs%r%mUTUN5mY|V(qf??$Dix_*p)slmTI~kDtIni<>M1v}p_2YbPoKHUSZ&YU{lHU# zaz^?HQ>{zx0~>17f5B{bDpaKjL5}XB{Q#yF03m38KL(kvIy%xvo_b|WV5QZ8i3H67 z{z-;bx~D3PbYzw3D!17~S#9Xm8)Z}|t?G587c0Od+lcX zgP_HsJ+=cc_6>8J*z+VbkHBhzk+IRA<0KPsWjpA>m1;{HwHNzGDr_^BeG0nL>&Q@Z zo*m}wvBraIt=kPp=h^yzkD1qv4(xra>~YY0FGKtz7M2Oh_myqJ`KfwipeFM^AElSm zgM7&yvlUUgm;ALeSBkTS0H8btln;V9?5bVO4=nLmZ1OPNWZ_OSK6uE&P=^ywaGMK3 zL@O8~2L?O%VUl3@CjgDMA>sh zftX+aN#Ywe4lQBjuZ7Sf2mS}xF&#kkkd_gdUFgV3PDe(j`g~x;&n~SLp3!n{JNrPz^hbK4Vu<&(Da{A X4y{9(LmSB;RIz)C{25e^MYQ=32{7>u diff --git a/target/classes/com/example/streamflix/entity/Season.class b/target/classes/com/example/streamflix/entity/Season.class deleted file mode 100644 index 01f8e16976b61b547eb0a1c5e74eb8deefb49830..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2434 zcmb7GZBrXn6h1dEBq6~Ru(Xw0s3Im%S#4{p4WdAy2A3C0CNtI#oy+DDF5TSC>~0+R zPkzvucAW8pKfoX5_}tyirj45+e#pH!_q;v#Ip^Hu?|=UMi->mU$4N?2I!BojWod%Z zwR8RpH*Buj#^K4iuzW@n>rzR7gHgItZ5Aj`g&a+ms7NJ7OP13y#0BqkZDDx6D|qMB zmKTOlzVt7RW5GQ~F-pl6LYsrwrlZ<59qYXkmFXIic*2#!W3+5u6;j&;zB477rS}6=;k!stqdmV+C8O1w- zQ6VhHOI^3T>e)62J85(bF?twO=#+yH_o8DP=iGXQDjPf|&)9>!h2!VqRJfWpM82Jv zJWf0n7V)Kus#rLdZN>eb3p8KD6Nr!WhCBw-9ZVpUUZ|X{4C|^*Ow7LU&z#l~chMCn zH_{dm?Kqm%3L=g*twBS@Zq>?oUK&9;NT+YoA8))ub({lt_`3AZGnKk_D8eDCKhVZ^ zj(Z1$)cQokhySOcipV@j(B~}nyiVH)i;zIq}D&A1R}DaLZ_b#t)lcAT1f3T&}C6Q zYz)D^O~5oYjA=O$(|5Fq9RI;IHH>Kmm{vz%3hNnRDhHUhXgdMZ^si99&C^;Urly=U(F&%b{A1HcZx%wqzn9MT13kQKPq z3_M32Nw4E7CyGNQy#rStIm(Z9+;etSOG|<5y7qOvA&^=rH*%O1n0+o^N~arZ*Vzn1 z*{kU&&LfA^6sA$g;X(m3xF~S@&G>Z{Y846OA|<21=i8;)5ZQoM4dKg%ZBd}UQhPJr zW?SvaSo`g20^>@*?bPFNxDsnmIkliE-EA4GYPn$uUnyV?^8(W?6*WWMiFM!$6vr_H zN_$1VFm^!*^FN6USY2VTf9mQF{75y>mRshq9A?RtJyAni*}F~X)@*j5K7 zsn%0^f9KnMfvLJ~`!eo^w0by_hxKzJuiFGxH|S^SOd8+#YCqVOevcNDp6ql~>rs!T zjZR9?lu=W*=z96g^IOUGey^j5klL)(1TF+&=TQ24$_rjfH${aBfln;c1Ih5iBhPi7 z%jOH#){%o$IZrqz71>s;J$0Z$lP8m)t-$!dSku1xy6Zht;l5;!O(jgh8Zy*o-d{|| zhnj-9b8cdc#E7o`ckv`8I<2vCgH>c$i3kPC=a3ufRUpF~XM&v$mCIr(qfddk#L0+k zsiIvK9|oW#|0=}|R9;ZmVXvA)AW!%g}k|$uIE20-q!0e^BS5shZ zZ1F$nG*2D__}M;(A-Z8O|9au6y?*hlqvrqGuMMmobi<~4tZlxNCUoEQ9f7m&@Q!VV z6wv}RHGah{Def9lzrRl3QG}g zVEG3Kg`2pA+u!3I+@W2TDEvzB`7URVXekVKq88-d%b%ct6oD!?9FZy?gJNutUg7Dg6qwL*OD!fvfz!6!}|VXp0v7 zK?2YRClv1CzI9lFLjoo-CA^3CEwPeKP#Pa_^>Uh~pb@wYZWwMK;sKwAo5Rt>nyLJX z*`N6`0TZA}1(>p7V3|Q+AK~MEU~3$W=WJ#5wJ4I)OZIfaEjwVI?HA8*%kb87U?;`H zKICaW`B6U0XYfhN{3$`!P-XNcsM={>wrr_WQQiCpUPd+o>pY*7GRrbw<4T|D3^x*I kpAmk8vk81|)wRr`eZl!Ajj6*M6p+AHgl+x}u`7@N25`d_FaQ7m diff --git a/target/classes/com/example/streamflix/entity/Subscription.class b/target/classes/com/example/streamflix/entity/Subscription.class deleted file mode 100644 index be3231441ba7e55639526c6f1f18c4affe0adc01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2840 zcmb7_TXWk)6vxjxzE>qp(%eX(DFkY}p-L}6OF~*VNn01^)(F$|C8O9&x$# z)3M!SbLZ#{Z-YHhD;@^^Ok#NlQT`rPjR=!kbF zcS8}Ln)|(@pzVv3PM z;>h83*Y!eMW-VYe12;bb$~I59ABZ62ZkwBp&I+SNt>`ItxeuyN%XWGkYDK&Z&c~-0 zSRkHtdZH{E8=Ko!!#ZGeO&ep&c26yD$Cb(pT8-zPaMwMj*L9(ib?q*$S&rZLP*y_G z?KzH(+m4_T35k=>rzG=QhiA^p>WvONxDveJ36h^8I1HoIm+;e$bDLfFI#3*K3YTy9 zx<}l%WK>4u>QP%ZOmL1rX2aJ4NniROMTGn1j(OO?oyIFwuqPd)>Kno!5>8aovT6$r zvBksJUT4?#k)jYUj5NVpT;PnVaQS|{07FDNGptqK(@s(p5K=Y+qdOPI8)zVyBXDGX z_oAo+)-t?-@cZYTH=U$0)q7AxpgzT!IHf5)=~5o@scK$ z9do8K)&FXVh76EPY7Op^J}#*Th-lZ|M988W>%adkSu2e6^}eZQD2i)N z+YYK;AvJ=&AxqaWi85%X0#;0DwAH6Ljp4M2{WF|D=X8TA_<)HUi?8BTg}8?OW9lu7S*poBp=D7Xs&=Hj5>9#D~P%47W! zm?n0HJy-jk*f02zp|7w?G2!}OW0e<&0^O$v*cp98D_H++{3{iHhOpmwQER+P-$F|o zuVI(j&DU=JMuqdx@=uhI)Ev?%!%sz8ad?Rqy~P@2IfjE|dPt8nOqbJ4OIoHnJw}WH zrpsxjWiZ_y!W0!i%rvT))@eh-G?r$%r)AotCpxCFG}C=BJs83im0Zj;p_rc1GY!*Z znrT(b)S%}&rpYwZBQVv6FhxZkGfgX|@3c&_X{HS=Q&Z10n`U|jrp6GasI_9IImNU^ z+nPMhrhUw2)@9v`qVYriC=qOE4V_VY2#} ZEHGv0d$qRIU>wEy2b^CbbC^vB{{T!PByRu! diff --git a/target/classes/com/example/streamflix/entity/SubscriptionTier.class b/target/classes/com/example/streamflix/entity/SubscriptionTier.class deleted file mode 100644 index 1cfc0717a87509f92268965dd6f6faaff2d77cb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1879 zcmb7^YflqF6o$`~mI4;a{URtRC@r@|!7E6jfTo6<(lqfCnYLpY+)Le^8u(Y5XiWUz z5Aa7B-`QQDY!(b(W_ISB^PXqVoS8p=fBhz+O?uHm32I4GGD9h9Wz>Ddzi`9kcFou= zABl=$)VeBdsn!^^{VP@SJJVIQn-u~vWg^I=UKKKySAcLyE2reZbnI)TY^zB+JwUfQ;!xc(WB(J(T9;;!OGUAUg~l&~wpC{z(P6>EAUY~gYxswHkVw0au~hII{C zS4QaK1w3tqjm@26q4&@jsdD?M) zFTm&p#=czCmM@`swuL%$s(ai;pD2``Q5tGHe%JJ0&1b2rqU;N<~0%uMV+)*C&ad!nrq-l;{$Xz2`19a>=4Ywj*OMP?$(fTjw ziZdFE?T=0W=HiG(W6l1_!`kCDUF`#fyG?g+i4%BKk{Izg5UvkaJy_kuJc{+0pj&hs z8@TA$O6aTX&$y?ddltLn*qgvQ>p`;}A9$HC-o>_onPE=N{3Q0xkDbJ*%|vOM!l)C& zf$q_L%#5Zfhw)au61Vi2c^$b>-SUtU5dthg~7lp zOs~;`TFk;L#jwy!b2J~r)EQ+Ok7Zgw6=Ik=qfC=vn!1EZ9{?W1fGO)UEz*M+rk*I% zbS%?DT8d-pi89TCY3>px-3(_;eLmANJ&Iu(h%ylILLdTiv=U?!7QDp3xayp{;u{97G$Ud4aJ^9T~l_^b7YmbOISrJm$LL8T^5uI4|X9u z*4|sa7T8e`B}hiP!&$mQBZ4|~1(|CKsOTXacwR#Ua8%ZT-mI!Q*Y(0E7lEM55PvQG zP};l74|EVJ*HLz%0=3b^qb21kAIK|ZS*(#2njdh|%2;6XG z4czE8oZR5+zF~N0$sq6?%@lF6bbCtd5MwZh60LW?eLBaCWbfKOYz4(6BV3dN0beyy-r zn4c>ZR#%Xhd9MODXN%fZEA`r@@=KgGL4#49m8GwFZ8lTkjt223+gDg}&p<%Gj3JYa zzGS+^JIplNLadlj{O$Em7W#M@wY{vu9k23C`XEAxdPR;w!UA|OI5ysd%@lq*h!4*< z20qIL!RNE(t~0K2pq`((j_%Xrz%{ z_H*4eHP$N)@Cn(4H2gG6kC;FbgxPq8lM>M{cUmwf?*?j|sYq?&lu%A;Md@4j>b|2E zHPiLdTkq68rkkMT0*-qRXn7^Ez9YKhbg;SQZJ5qD`Iajyek-jl&$nd!mYrypbOX;~ z2l`MNBc2=dqpMh5r%t*S&1h)~LhsWD5Eu>UCfy3?Hooyf8{u=9&FL7Qj^h;1tT`Qr zsT_7D9BkcDbeHatjrE5wX_BS_nuadkn1JqM6Yr~0yMWb4_=d!ThQ!C%!*gmRMq&69 ze1EFwAkofn2+~!LBBP~FJ4g88Zjd_?2{ug9n!^PtHW?hbPHB*QxA>N8D!=> zaxp{`k(c|iCF2D)O-}iX^T(Kmqm=MVF&u@S!e159KnkJ+A;K}?fI%+O5=I7TV~(Tz zgNA;?MjRz4fsp;A2*0F)iEtJ(d+y@shK{3&b2J6a(~*TsvD6XzE<+#2mj-(=33@_L zF}7mLv|@UYh^a`+NtiONm>vStOdCuwOARK@nV=O~O~BOOifJ|x(=+-i2~&S7rpLfE z-v(1$8wL~Kp`bNdPrx+LifJ(sQ;D7@VH#+~^aPlmw!st^p25VN5>%$I6EIzD#k7)$ x=^J{HgsG`IVm_?_(|Q|Baj6?j%<&F-8I9jYJ?b**|2wR|2TF8_9~xa^<3DXY(MD$(8d;J>Dy ztH081H&wD*jCw=jw2{qv#pX*k%F#g-fapSR-EI2Lvb3W5kZlv^~!+l z)eg3+tfnl_#A%GqBCf0%j#WHG+$B#@Sx?029Gw@`@3^|{W{oQB6B)&ji-nf43VLNx zcO{FI#d~qOKoet8uzU*9CPMP#S59n6b?AT#TkE z6{Q<-nxR=i=i20h{Wh%wK^LZ`+olXUHutgBRhcs!W4j_Bo2KQe#F6WH*zJKhU8_mk zF&tN#1*xTrbAqmfik?VQf;L&q>6Lv6wLX*-*5$(v7T_v-SW_11#ig}uI{RGErO+6w zx_Owj)=gGUhH9)>hPhx>_NyjTdQ6>nJ?7VKo*BKX%CCftlJ@IrMcdU2dyZvldLwyl z1?zcy-jpS2bK0DYs$S)x&)lz6bnw-UCKPb*m!}e53`NyI6jQ~RXVO7&z0UFnUZLfk zdZmqXCFN^=DII?wFr0j;Q9?Znsw;*whAG$ftJ~7fGA==9RB`8Y+u*hTvB%vpu(4W) z{tL7{a{p(lgGs&JJE(b`-9f3|!r&3JETmIC!HFJx_osSuP3O>0tJ2-EiW|C(%0LBx zk4Ra%X%xnpdaEecs@WiPJmJ_>Qw$|zZ!wtqxeJ8raA@lZ0gzhyK$QA>Ko@}*D&muUEPjD#*y9D@wkoychmBszX>5l!Nv&~ zFSeQ7<0m$^KD%Lq2<>bpn)keJ!ht3bDAPLWA%M3~!5}QsO|$~x8OttfI|hc73EWNd*3lMrWML0x12|{-&@fCF zjd`=mHvvr5WO^&Pww_I{JzhgGo$>(*UB*euo}fm!}Au_w2n#;1L*FM2;wt zO7tGlWGWFoqU3R$Yt#>04RLPa?KTa8^D%%*fago#`6}I`8z7o7YQ@o3ev6or8a?F9p^+O{JFpUN=<$&o~8%$o?`(J1`XLn4WDwJCC?-7|(_|2n2~1WSOx|eqG4VVQv`vK&o{~XKb||JI$uORp z=AkzWUIEiV8%*9*_c8H&>Y)+_kVpRto;-?%Q*UVeZ;GXUrzuQe&beDnsv$KSf5PPZ v8MF46CX~beyeTVPF{I8z9&9`K&_#y&-&J4vl-t9)0x3?XDw%k(9IE{fcEWoG diff --git a/target/classes/com/example/streamflix/entity/WatchList.class b/target/classes/com/example/streamflix/entity/WatchList.class deleted file mode 100644 index e249455270048b71280ddb43319e164115685662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2491 zcma)8TT>f17(Mb8I|c_Rfixr`x5joZ+q6mBgoKoPO-x`C#+~$q5w?IO>s`-!-N3(U zXTr48hrV{EKdRFs?P7?#sqMXG=_G6LrhM_ zq`PNr>>Q{@Adq=#yLK=qkSvw!ISe6}MZSP@C=3PGGWBjtwDj@l}_kv{c>h*gKAzcU>=#f$g~+fhqdDm3|jVOSuhY zRhzQ{H)D@hm8*P0Yu2UHRWutS`CRWE4e(&Tt)sT_s4gwP+^TN95x5$gV_mvOTi%AN zjb~yl);!x?^qg+XrAf+_Ei#yo?s;4DKIC>CN3z-^#Rh?g=K6Le3-k|Lj&&d#Z#$lA z$sQTj8rKWlex-JmuW8W5ncFuJr(=QiK;+F`Za2+krWU;>5fy8;t6p|nJIdeESOmt* z)z+nN>vgo53ifSYMR|b2dXgrtjnV+)6Dbcc3sqaLQ>g1I*!P;7(kGcDiGb33DyWjn zu@hyP<}@niVJAEsFOip%#x-X;TAr>E1QtgBPis6nGHlPW@~KtoltWBu%3+@mOc>U! z#oKc#4N7*(Re`DIPg}RZAZM0S5hEYA>8!!%xr=HNJicHcg5nA2K%2 zRjx+4n&Etn>+1?PaFZK+iKF8wByo$!UvT$!2PKq$LIroYlhE&Iyw%;aR(ug7t8<+1 z@xwUxnSxqFhNHz`Uvn0m)0N*Kel^j*Zmm(!f+EuHln ze9KX1wK#{c-(&O_ZX}>#>#9K7;vj#A#}VW?&bqvehC6zSG%UKz;wmP>h0v;pFcaOL z#+Bx82Pc6inB~}yDcg_fUM!}kcov5#+mGpipCBCWJUu3+Cm-P{6ljDgyt@TliNQ3{kLg(~rbR5pVVdaYX`YxCK7uK1hX~Uo zc}np=Qzi<###u|JSb2|$zmTu|j*G`I@smbbBSx(&$YPZWuAV_zjP$WW_awxKX>KjV XRc<8kgBh!4x5)Jx*DrX=Uab8Ku5buD diff --git a/target/classes/com/example/streamflix/enums/MediaQuality.class b/target/classes/com/example/streamflix/enums/MediaQuality.class deleted file mode 100644 index 922bb47ec541775141043668cf688908858f8f44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1243 zcmb7DYfsZ)7(H(<>sC7$6G0|;!6_@3RTR8rh!~jBWQbZ>Lj3fy6)5Ri({%*D`IB@c zLNppa`=gA{+s!zlCT>al^t?UiJeNNE{^RpE0A)PTATgM=pkX(hJf;B2M-t+ zM211jS07h4%QnO8n?5q=$l|<)jDcZXU{Gsb;5!V%1+%ix54h!Vf7jaB-gjzINyjKI zY8W$+!zG4+--I%?O(h&~ujOp)Ff0~&)TqP^T#jR$SkC z-fC<+;cLF_(aEE~RAcrrZiZ|nsBy2#LsvL1<9A}!Ut?oc20Bod=v>Z66_5@oi7 zR#~_$HxAisGlY zk^Y3CQ)E97Vwk3TB7P?mKZhAeF@J%gQ0Q4UeS(owFu|A3@Mr17=&&${A|Z<|m0|9^ zpwhNMj?LyN%JfQ`o*6&J_(yvDmy?EDqKnumC1IXAxlPxdShA4O#U@fp(UJv)?!ai` zE7+FYhHP&sZD{tE+NL>*de8Qk#3B~D;#Xa9@e66QSLI_|JAxd8NhJna46ds&&`*#* fLRwVb@yy101!gtTtfUn%D6yD*OIOy_|2cB zBN3v}@Yx?_Jhz*1MEtNN?ddu9InO!g*||S|ef@G2txib9ry8oA-Z6@cCg4W)J@gNT+dZwP&Arwlu$@L^TL#pW2ET?Muw&3g# z%dS-?{?xxhF(fy5aOCZ8Sw3L`?$dB`aAcFwGGZ4q?c7A$@%(+;wH$_c**DTzGEp!>UejD;)IhJPWe;KCxBrlDKivf_O z^$soVGLTG*{UwA#@hm0?pCfOatoUgyB);JG6;hwbW0<6OG(3|CpT-m-A%2dm$k0-3 z@)AQ=U;-bxhJQv8y^X>&^5hxxD0E3b3Mg$Iq>yZutVBoZ88U6eAgs=?x4=zk}^aElA}ZxdpYnqqIm#(eL&7VwlHVCwaS* zoc~6-sfW@99-Tu9opCvI)X;gXgpPKJ%sCRG^EPKH%$JcMozv9u1PvJJwD}UR2p7TY E-ztb0Yd2eF&40_i+Ph<(0E4VU!2m+SdSSwdI7;=^Vi1LCdt|K9s zlAA@#dR^UmxwBQWN;PqGLx+w*1~pqRR$i4X2IEb?)Eb76QZcAw7-pnmHYvesB_AHV!Lf#-DgH_>vGDT-6ZPt_1Q3_ z)_Hj7?`_%vVL~2IOLBPVkkPV2XxUEeV8`);J;$?MhJ@uDcy`zhh*!!PbrMdY;dW>F zq9>7S=l#HsEX{-t|^d0Ng^9;`k6T{+9wWJ4{ diff --git a/target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class b/target/classes/com/example/streamflix/exception/GlobalExceptionHandler.class deleted file mode 100644 index 890ef4c9fbb073373e456f580c5587bc5c70b36b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6839 zcmdT|`Bxj)75*NJCFF_i$i_>YIAemtV#72sjbm`AF$kj+L?U4v>?Y(PJz&geMwuD0 zv}v0*ZIbRwy2Ra^?rEFGX^S1ZNt-VIp?^dFgZ>LSJ-u&c5W|)mj-*-MyYIW- z{qB9sKmT*}9RLaZBZeweM^K}n7F!s0T;Q`@Gq^dW4NqJUS(jnU3EkA)ZiedSmeB~J z46Ru!uZek{FBn2|TwCz`q@mAic`GLjEn(Z1ofb~PG96Ke7`8^BD7XXL81A#|Da|R^ zx;Zsz^Sqd|>=|v^bqm@6ePvwk79EDV{^9hgWM5w*#c*hF9bLL&G|UbKcOuSEJIjrt z@Zct+*d@(b_Jp3x2{VS>*b~8C1^aLp!~SJwGa_5Gb$1~#pA`jHw@ilElyHZHfuV*VJ`zPE1G#%Ynj&ac(1O+#Vas^f7$UYP7(7eZ`-McbTTw7 z$2(~lVu~9*_Ea%1Ot&NwY4wDHN2Jxd)bPl7|L~bqUlbpbx0b8+TSYS$Lyxq0iUd+2 zZ^V-5H0tE_DUA?CAH)4glQga8R8QGz(;=rxowRJVz-11(!uB4u$Iy?{5ez6u;!%d7 zFqWkpsmrvL)3|9`lyIbVZOD7F9)v8#0)!E zb)max|1-Ad9PfYi{eR!J!+3(SaTwv+flhKI@Lpt zG&Qc^X>iIn8eDekr5fjQ)1{(S-`o=7L=0KvA`l8DF}0MZMOQbp9^2*%)Gz8n2gL zzZ~(3Le+9krrNB6IhktF-r-dLV6vC8cR}7+CQUzCH;Ruj9Iia7mox6P?x+RZn$!&u z#m5=;N?+8HGiu43C_YK4C*MwRN2mnTacu(XsG4E5J)Dl>Gx8O2W}DnlGs2#wy!GbF7+#QkzeoyRmS>5}2tG%FxM}4$ zZCDfBDD@iPX3h{c%^9))lJEz0Q>2Rd31N@$34@-*2dykOM!Bua``}@Xq=zze@D|7G zB0)}jLx)VL&z6UG)%);jzV`Il)lzv1SUHstuqrXna8G##+&Je`Bbqh6Ihvs()bvg8 zFx(-^mp=?gDp47-SfcTF_@(%Af43d76Im1H6+uW~j$6dkt$is9Y&pEDZ{mFg!;&+Fj@s`$#n;y>L|1n;w z`}w7vUc4-8gEbOWb=sO{XNGtom?!`Bf5q5_HFLMGnebH}!zB=}Bs*Lp@ zW4Qi4b!)@ntM|iq!;+A}EuU#h^XjI@q`oYpmPciH^)|rSFqg56RkX9BpSBCMLAim_ z0e_XwRPzqOmvp5FHh2k{b}Zpyz1 z=v)w6==<|@r_nE+p`o_*GT0@$sKOWMTfPRimSP}jYHUj^L zA@HSvAK}O0@Vg}ZZi0WZBK)op_$>s#mEa#C_-zDVs|f$qjllm&2z+Var}$X_|KB7Y z8Ga|;Tl+Su#;OT=A~RM)KOfFqK`KQT>Fa2fKZ_W>2(>N#k(#BJ^ysN8n2-&xVj3WU zNeTKfY+J=ZXN?r#z8Y!~a8T{(Tm}#5z+OB=r^mNI(&M7Thgnjh zLqhwx$AuFpaWBr}7x*P9agvynnykUE$ZizuJ@iNDgeO#?uZ3_V4gVUy2{?K$;K(Er zj8xlobhgE)YqI9^7ooC~pSflkydRpl$2RKax43IpMl$wu{ zM22t_!$eM+-el-Na}*p;cvN*$;W%2N05D6WnMB%eJ<`m8v|8u^X}r{YRoFsAL0cB;IVD-stzfDdK7 zQyL}0#x|MhxpVG!=JxaJ{R2Q7M+KyiwvgFD7CDCFXa2%%m-`d@G4i$aM0@UtnU>0D z$lXX^>J~%wto!Th>p=RG=K0W|whSlFP~YHK=^0D|^~y)Cpf(HJ3|EfwY%%BF%oTR1 z1Hru~SI()Fq}x5EJ1X+WEBDFVsmJw z|Bq@iSe^(&K8fLLvon+mQfe|6*B428am0!MVDxp8f6yi1dU zQ<%SMo)id)9PK-#TC|&buHIOHy~XFdv@YG**tti#wsfJ05-IXyA7vsP(07)RK#a{N v*gK@@1+04GVgc(fH&6Wq81Bi^_QSOjF14b9D&b5*J{2ca;*=V#DI9(S-)N1o diff --git a/target/classes/com/example/streamflix/model/AcceptInvitationRequest.class b/target/classes/com/example/streamflix/model/AcceptInvitationRequest.class deleted file mode 100644 index 93c66aff3aa21419b3b7fce289b4759f8dd006bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 826 zcmbVKO>Yx15FO`h={6y4X$yrGgi2agt|}uT?fmSvqf7W$#jbEwg;UsXR&&9{E>h zqP_AuHz#~GW`~lo4l#ZXl%Tclob%LMZH7+-ZJmsb*BSS;)BTi>jLC}{PEZF4m&$r2 zCo0v0X=$0{2K-7J@8ogrpP_|I)QK{7P7OhGcdCP12}oPowJgMF%L73>i(F?nqeZ?- zp|jc5hL6WtpY54~rl34eRN7Nk2Y(i<_;U@B2i^Y}4HP$qT#Rfsp7nO<_vcc64XEK3 zVH@o~QJo$Z`kMWDK6tCF%Gk4owD>e1+k{{0(8KmWmb4!@=@Hgl!b1_TSU7A0-ZK2# z_?nJZXcd75i90k8m2rpH1iOYyiBsUL?tUim0Y4=IczTT>t>eF50BD0Y;bB*F4|Xy6 zBSr6jOOF4VyhZmx5t0va2KBYwy-yU)sl%HOa;wywR@f|1Gx{Jc^eX59P*njZXxfG? G(asMZnY|nU diff --git a/target/classes/com/example/streamflix/model/AddToWatchListRequest.class b/target/classes/com/example/streamflix/model/AddToWatchListRequest.class deleted file mode 100644 index 6b8cb955b5824ad642c55792a31f355b24b73140..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 992 zcmb7DQE$^Q5I$#3vzBZf9Te6v#@^Z?VF~dzG-*O0A!TJ6Re5$3uk~=^k~q`&S0I6q zc;E-{qY$6VMrl=$P?S5LeRtpI?~?C7zJ4R3=d@QRk5&Szgyd6|(Z;#B5L}CF#9s~1 zWo#K$pQ=pRLq;o|?x04iR12sc(hUk3J&sMv<)ug`TJpl?Qlw{EUGmf6E~H8=SWP!pW)rn^R-TX&2U`2R{K=x4J9< diff --git a/target/classes/com/example/streamflix/model/CreatePreferenceRequest.class b/target/classes/com/example/streamflix/model/CreatePreferenceRequest.class deleted file mode 100644 index 16f0873e20a9eb2385e1c7027fce14cc9c32188b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1517 zcmb7^Sx-|z6vxjLN?FRj3+TnQWg8NWFBlRe=u52xNZ*IsX*+P2Ix_|QR+?x`eDDMK zp^X1&E0;oRG|ipq%$((S&YUwpe|`T!L&u!1uAX@=(i) z)H3qrk^In*1}4Yq*Lx=)>ypuCp@$(EHA2G^6Ha``L5yW-oF*8hFGSFQg{eY9er&ah zsymDpj~bCxp)4t*P6N3YMX?oD#gSpOQBW~Am!evgnqNHUu`paD0z8yuCHS#llc9jA zk+L+tR&}S%XwGqXamBMW=_SE1%CwUKu(t=rqXR}cfSn6%1qZ1rH{Ej|2v!RfS;LF5 zEjEGxJ~E*+MpO~aQWCLeohxNLjqE8c%Zye!!*VPgq0N_EPP*X@?D?$#^8Y~f8f9<} ztjU(S4ut-F+%Ti@f{J9Z5uQqYB9LKs53w(TlF-Vn+nfEirjYWNy>9uh<>Cmc(k2;k z@iOAMd@pldiw0KAT`SwU|1y#IR@NwMGzI>xx8kc+Y&X!X}@wQvZi)ep70`hd7~<675$Pz zyQlC&+hUx=Y7z4kzG*`F0hIvQ6rhLn2rEX9=?TV8GDd|#3!S-kgdx_1NiU$rys zbfzEr1Nx&nea_8|dEqkC&X8Sp_q;s!InOzN|MTZxMD#6fPf>v`6=|$Y<1`^?_C%d3 z8LGG`e>^(TbuVb*xrvQ`A?VWT+TkQ!rpY2rm8nE!LDjmAq<*iWcBp0IomSCtXx_`n zHgqUo;q3KmXOB&&_w`R5ogm&=tRgLFuGUX=;GKz^TO9mKnPzBKP)RlQzVf)X<4&%74r3#pq2ej%$J8I zF-M`^j$`YQ(#DCP2Q_15a;BP1?d0iWX;mUs-#|vXVU#?mw{)Zss?^X)-I+Ff3!2}< zCkFO7@GTv*yWcOUm=OiU-`iFk>{wu11Gaa{c_mL(Xc}pHSqB8~l!<*J_pE=3db~rD zVx*Gzj#gQ8wRZh~fNCf0kM=t#Ra3iV`-akUSXGknf2$9&76z>jPHTf1C>ruNLe zpoLW~U9VX(1;Zfe87wl5Bkya6sNE0Rw`}9Ja){%#1C}d@3L^V@pBoCEDjrq{J!CaM zm?1Ud_$36}%{l@nIQ36uyrtZB8{=9p&AHYn!)?HDImU7abafO{1(@Uon7SItc!KZ% z@EL98@U$?B=}|7G=k!$`PYa`%o&eL+3ovz+nPFN2rUHb(>({B6OF*}R9~$VJe*ti+ BpF02m diff --git a/target/classes/com/example/streamflix/model/CreateTrialRequest.class b/target/classes/com/example/streamflix/model/CreateTrialRequest.class deleted file mode 100644 index 87cb318981e97f1a913503ea6a524c9d4a1614f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 980 zcmb7CQE$^Q5I$#3yOeC*I_TKOfW5VwgvImFq-g>PDHJrSzPqW{c{uh+oN4?kkU&U0 z@B{c!h|i^?v=k&1<<4i{efQmGfBXLBD-k`Vy*dT7RHI5vAypZzU5G2ewJ;<8a&RG2 z$Ef;58Rd=`EwwxSh?Xg;Q9Y(x6f@$f&A7Z4*`=1eaJdxOnO4_4vqP!*31TOExf1$R zzAa?|v`D0>Esc9VM6+btEwN^F=+ReWYIx|XQ!@8iTc=1@ne3}V4YWKq#ya7YH3g$K z5Z;K~34SHC8jiQY$$*Mb#udD4UANTwkkRfeOz++0EL7zLoEbF|Wn{O^1~Tsnv}4pv zY$|kLU<|~Q&5Ao$=>ItRPXRzA9!YmR6`|Sg%*%kBVkT$9&v|n%W2HKM$oE3Jb31$` za*;_VbMT3P-J9qZ0pn&}e{IV=mCuwPX!Fl|dF0_~5Ap|i76Cq7FYFqkDluAzZ{lAj zwn5`4@VE5Oj1YBrQ?Mu@4$t7j{m;ZcU=h%5SbvO7x&s?e)-BpZ#ON+~}ECj~==UZbj!sxdb{}%u)S1?cP!@P_DJUg0-HU z!*4x3$INHMYk_Os5h}h z(V|9_ZELCZ1^y>mc!UF#UGc!p4~Gq-`3$#kdqjDIvXxeLbjM8XzXH4?aqr!P#bMVa z!}5rs{EFqSTamrT6O}#x zM}_I!LaBpFp%1q(l)ztWY|Il{LjUe!Y9XmYr zy^z-X#AO&!Sz)T9hkknK7B{yIOyCYf;ghHqDyX7}rwn8YDLE)xMyGvA2ES%E!R1;BFFbMW;z>=&DJ8vHA_wy z6^MV=Ir0c4U@Yat^`p}DGcmV+*;`?i##3udnZi)X@$7P~dSC~8)`3gOsIJkz6*zLP z3scdtLy${1@Jb(F=ZaU8bsfpVJ`=KON5}r*wiQ@aJF)}1H;(K`h*Phr5nejZ}QVVq|SJwvzKL9u%&lup458Z2^})^spt7DFI$S~Th>HiOe|$f76-^{BlC` zJAxEF4PXp;(Tw%6h$WuH)_vL#TR(G6bIsD$Sbq8(+pV*xqWvtt)7r~QY^m;rHg%MOG%J4#^dvipR1jum-qY4H_aiB)iU_<0F9f8i zVGBr;Coz-G|Bk_5x#@yLD)|6rTOv$1Bm4xP)+3Z`>FnnhFXc)8r-6IKJf|&8#9B@C zJ-SwG4{D_q4}$44Jm5EpY5W&nwqlxZi)jU)Q>#(I7?WiDBEvDJMPgd&fJu1M#3VUm z8{%ObOq0nxnQbwBiLb6;VxA_Gm>v+*N(W3*+a@NdA-lLOrkNzBhix$x+GCnYVk!{R zqYjvaWlc=NDMi(Gd74dPdfXP%H|;UCbVppfHDX%tfJyk?#1wOU6_4xbZ{_&iB&O$W zFvYXs2`xUwx9lk4)i#Yaxqesj)vY@v#`cofN*%Dp^Ja}N;#twdu9&|-JI9OI;W}G( sgQqS$!*i~KRQis;qKy}LN&BFJ!X~!38>QMN=fpPW#VhRM^^>Xp00_Q1DF6Tf diff --git a/target/classes/com/example/streamflix/model/ForgotPasswordRequest.class b/target/classes/com/example/streamflix/model/ForgotPasswordRequest.class deleted file mode 100644 index 07d40b64996f374ca6b9b9eab150889011db367f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1173 zcmb7DYflqF6undWfKn+?K%TmyBA{Us@tcPD2qY#I31Ghu-C-Hn-KjHEz<;HQ#>5Z) z0DqM6%yumzB*qUr&ok$qd+*sFKfiq^qD@+=Qh^34RIE{n%8W*j#i`)2NF)CC;8?aD zqw+JAD!0LCV0ooAM1wR`p=ynWsmADWTPIwei=-1vZk>@LIgHggPjo0_zNJm1-Hx#K zOq+05zVAv4!jepciW!YJFM{nkqtfUpqnX`q>Qo|I%Bq7{zDQH;gi|`Tj8^dAL>MRd zsfbmW9rHG>oDnK@mTznK653C|Q%R(?B9e^8Hhs82*}y=7GBRYe^e?9D4QM7_r&+8Z z(;OUXlL(~nsHrr!XCjKE;iv0d3(LiYRG!FC3BK1p!ZWZ8htjr<>i9gs`OZr7z~+=9 z3=O10ab8C>p$owCe7lAcGg`P@mi-T%%cUv4Ld6-?n<|yt-Q+-;eSz}%BI~w@Tf!*6 z&ku|4NFm~r=Kr@3*-S29ZQM!=vfoH|q{AIyL?WFuh%gvQmz5G(FZ=fWHdnvjgSL;T z?&+>+%Pr+~&t7YuHP3+Z=UxhQ=2734z?)RT5@{54a>t?JV5d9qYyoa(7~xusH?Uiz z0UE(s-O?zH^%mpQ!0m*kNt&{Blcs^2rb2`ING$u YUsL(2rUKo~g5S%!FpTgGe5m>TUugshqW}N^ diff --git a/target/classes/com/example/streamflix/model/InvitationResponse.class b/target/classes/com/example/streamflix/model/InvitationResponse.class deleted file mode 100644 index ecd64bb7348ab4fba13db001f698a3392e60090d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1095 zcma)5T~8B16g{*3qT80%3RDFJd}<8Hn1pbvK5)&W% z0sbiCncY=tOB2%c-kIsW=bSlr`s??Pp8#ItsS5{%3W{}Ljk}lWLL`%Tc7F4|=fk3-|BlplGIvVc@ zyu35+e0OgkP|UEKeI2Ry=iyK#pX4w|+2WB51~SohpC1;@iJk~NUv!s6PEcs;oiZo! z(MOrcP#KjFrZ!gQ@3jTIZf_CntVL{{Otfcll6*o%rs>_ZE@pZ`wwhl;Q#;t diff --git a/target/classes/com/example/streamflix/model/LoginRequest.class b/target/classes/com/example/streamflix/model/LoginRequest.class deleted file mode 100644 index 51200cd951ede3af3036cb7bd80030f74ad6fbd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1318 zcma)6TTc@~6h2cZ8lxbhjw6hr|wJv|CJ^h6CeBm z{wU)&%eED)(QNii&zW=i&UeoC$IoxyiD;Le4pWZ$3e;buJPk0KI1?9wha#@?{gX54 zTSfyfRIKbaqrP&bHbjFoRG{G^jZl$M#n%y+S0ZYLk{g>y5uJwWibpz-A+PGXiVx-c zmNdwdmyu8*qp51Q#*s}_T;Fn8$BH!WQVuqSF_$_C7|kEHVyhxqQ%0SH@>LvbE3DG7 zVYFUVnwv{eugipAY;r9O7hMM)$v_EyyI2gMUXkVs=Zed^hU z^jbzHj0#yh;KCVRX6gnyWwZk9=OVF!Ux-izDH!epkWGY&t>I~6)T{iyJew z8I87M-Zg}2z`TQrF&eL`Snjo=6PX+fQ0}IseG%3~qMV)?`)xyE25Z&-9zQA%)}>A9 zVKh^A{InM>TLO5Pj>l~IPlrkkGkGg*Lk9;UK^qngqJ89TGXxDey4vuOZY92aqg*SK zzwhIQtM9nKUQbV_+nzG)#_ok~cj+M(Glvf)hX)G}IRWi1_0c4LPZ^q~8AG!)2kV{m zhG>j()8s6W&X#j~okf6{FqkY<$||yIuZs}*S=7pTiX3~ z5L(F7LYiUj+AAyXpgGDIJ%Lvqlfl8L+!NF2ZA^0*dc9`nU1ipjuP2oWI^e7 z>Wg-!na=b@KR`cJrvE+3vV1Dha-<~ju#IrNv9p!q)iS@(&nGDWc`3oC= zXwfaYEoh?cISShSJq7>t*o*j9ELv3|8=R`juP2hdtXp)4vpd|9fh$`Poo&%w`UvT( zod)<4zgcXSWz}yYqoOFNe+MZSG+A<+^4V$QSOygbQ8+SZw4&xhF z8E9L~roF}n>~{iA4%5{z*LJ1V155$fWjY`G)GXPp1-B2|MS~@drk3N_-A#%|&^Wf! zYd(F@uA=C4YRSltI@J_`bt%SRu^-nJN)O&UtEp7%c6oKDFD@`aQz@*rX;&U&%k~GW zhUF@ZkW)(rD#z6r0d-b6>=k;764L@@nFB%ZUVpyGAxF#pX;77qU9Qs^ZCY&bl%Pel zLmwuUg(oHs-uie@D2Yj+BsyG4^t6)bC?(NDN}_F)L~|>Nc2*MYt0X!W@-m@qrl_weLxDCrYCt@!;@`V1>U_vv#yyAS_KL%(CQk2;6vjEBFVFJZ-p zw;}n92UpkL(NHHe4{D+X1iz|0T#BqH3eRYqA3>Y28o(7n2->2r3{2xX(+wljHa$R$ z9;R`fX$efr8BBav3Dcxv`kEdZn5J~5+eW5Gv}0nL(wWx4w4TAlb(1jV6jQ;Nr&*oJ zHZtv+^E9h7-BX3brPF(NT*C>|oML)xWSZBR?i-n&n3?8vrcE$yW#ozbDPg*zn7%PG zE$B=Sj7&u{(}K>l1ExX-6Zde!bWJgRYh=2vGd(dfJvB33*O{Jzsg%LQ^CV%qshCPe zrbV4;-^jFQW?Iyl%3!KwF!5|ln3fdNGunqrpHIs=(+eZh0exrcyJel}C751iF!7vC zm~JViLt~!uI@3=^rm{Itd7Y^WrdkFQ|0@!vRmD^>&ha&!scvL?Zl2?7I@4P)c^ORn z-%6P7D5fKNVYs`E->{kfxA;b;@9Br0yIXii8#+@9OkoBS|3ed|yNc;Y75wFYm%qf} diff --git a/target/classes/com/example/streamflix/model/RegisterRequest.class b/target/classes/com/example/streamflix/model/RegisterRequest.class deleted file mode 100644 index bdfd7f2c72ed914bac9e300d1f944ad150c03cb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1334 zcmb7ETTfF#5S}fR0xfb!5sODaYysIQj~Jp7e5y!*^wliqEbYR%)ZH!MztTiw;)6fH zA7%XZaM}i}F{bHkXLc^%%s1PgzrOzn+dS#BRR&it7CB9RlahG+tC5*XDbiio-upL_!$+|M?B$Th>II+U2 zBsM5sQwcYhqS26=Uu^M27%qAaJd%MD{K#*j>NcZdAdRn8%S9vco`d3vAkfknFFEyW zQ+jP9HKRh-5ys&RFEe!$%`#dA_H&`F;1?oPK?;WZ0A#gLu{AtRjC$37IqoO+H70!y z-}22ik3Y6OCgPnYx{K5kzMo(|=&ducXV147J?K_$No^bju`l^<61Jll#Rg*$ zNk(IxnDk@GI{e(JG6;LuPRi3tT+7MO<_j}&qdc8-(b%&&bT z_BqY_5Yk-}jgSXfx^@8SC1{M6X$A6T@E0n4!ex$n!E=3sAJZzlT<{J~H-w?J^{-Uu zm3Dt6gckC&kY<>>_R7jTXl`VTp1>;)j&Lxl^u;uG8`EN6Ox6CF#%^O;0;c7^U~*)1 WF^vOLj@DASo^o5nf0ZU06CeBm z{wU*_?Uquh52oqd*_nIKJ@?!@zkdJt3E(x>`;kLW3HdS#C<=_6%7*moGOGFSj!soz z1&Ysgr0teK&+^J)A9~SOLVpH@X97qN7nc|v3<#s&Nc-~p)y9+l)&hLqv2`eC1j=( zRTWs<*0FCcWv!+Xzp>%R()hBS$`4glOMgE&QK2N*KvkI_(dUkbz*37X0a>?~mdSmf zbVF6WhL#?~be$wJ#fTt}g7v4*HrB5`eiE4NEb`#vd1s;bcGRVJp6S`ie5T{-I8Ho! zqC6P{F?ACdx*;ah3FKd~LjuFwI#N3q;gL!XBz1LNi33?bl!Mw)x8?j^_XZC+$T>c+9DjT$ zTu1mdMLP;=Y%q=qeoPwN!CixUyaEOx#?f}jxp#0oa=I0H-$l;cj+{o3Iry5n&JuQm zR?rqI-yyzmlfwh9?%}&^d5mF>yK>VB9?66I0_N%A0N&H~&<;C*%IY_iDr?^`*oGP- z2uLfBN%XOzA>2(Ny=GVjNq|%f$7MLH?Gg8VP9k^YvOdNFSC`eNb*_cVSCl?;)1>XH z0;aoCUc^#Hd5hMSS4`=)nKp}lz~bgoggZ^IYlg*)q2pEH^P~}2?#gNKZ%*@FIjwZ( lbVI!+r$usF`UfXB2Lc%u5b}EeawaB4}Dnyci!bB*tK3Qjh>C&)e(LF5cbNyKRI2l_nY! zAN&LSqm19VVr!e@!H3RtX6MIeXZC*m{_&HD4(Mrxa+G(d;8KxFf@+s?B)vdJJ@0+z zQu$U;>6MPO-4~Q^G}~obpt3_1mu`_O=!I`WPhH7y5GXIPv6A6Mps&2pbXDM;S{d7O zY5l&AdME1RP$j5W9>nHC2kKoH?OM}*Efe+jxc+UIs$AR&%}67ReRo`{v#+cMIze$x zUOgE`R)?ys6Ws~a>nJi-T5Y04Py>ZOpu6-&GSJ=B?fE#evDA@Gydz_ehC#3=Xk)g_ z*{8v5slprd6I5;KNF5Esj*8DD+6k(+j4y+>j5XWW&Vuc0^ncm<@5ung?J0Xaold>c zoSzS3l9`w#zUkkUr6X*%G1o(7`=)y=V;L%|Vh~kO`*1u^7$`C^xa+}mtlZ!qvvO*N zv9I1@V;FhXN}n?TZDC6}ys11S-Uj3ntP%;j3w;m21rl(vmE#4c8cT(C5QXOlxeUvL z7SP4r?v6)d4nDzij()D`3|Ms zXWz(~w&st7aGxURx&87uipK33jc+Vy6K6#MgP9&>GA+(Ctz0~7Ci!c$crajlRt6%$zyrch3LJ{`~d*2LQZ)?L4GlC<|!~GBC`bc*fsz-Q;## ze|>x=nm&W!r-p6#I}C=ZwMGuEKrRb;4X%R5V58|+y13xhxhZtdcLldjOyfegoR%>4 zL%Gr5rqSZQ;n)Yl3rzHwJ9nKE!xXzM3|Q|(tUGpln<9*AP$2hNOSBBW+oHWO4N6eP z3$Az<2+tR-HvxL~KQdUY55N$1V0ro;If;y=`*95>;2OH;rs;eTNWqRM#FPfpFoPLm z9-d<}D0RVMGDd_qr@_2L@q%`{7bX21GiOk!8@6~ASjWPB%a2XmDb<}OHyhkFXdP{) z{ZqqZusMkTZ)MzOptXg+-zjCOTI=GK#Y7vg$EI9On$#Lt(>>vzI<0-~a!dHaMW#IZ zmW3mc^5`U9l=0-CPlGiK{_iR)SC6EXL=H8r z^i(NQ6&*>Q&}6*3NRb7(q0A5aB&SUJck_o%;5NleY;y)P>X)!itq)7sc`1C1!#Luj z!mku{znrn-<&5nrXKYV7WBcJe0*qcQjtjWbab`GYR=xrIBFz?Yq+{H~Fn~_O?`61- zD+V{QhYeRFphxupZTCr@KDI4iEi% z0e?jqRF~xGuMz1VGnFKh4tEtyl}~ust=W{4=^osVm^zxxTtcP86e>PFfQR}p=9!Q@ z8}LBEGnuT>tdeIF9`^BI4^1X{<_7T4dx&|aCC?+JX0u721trg8wPv#&o=~$T)NFYG Q6TQutXYIq1~||`P*_~GWSDI){{NNAp zM;YJQU7%g2i65Bh%z4k5XU~N{e}DZZqPMiuK`ClVQ?o%W)XJ#)f>*g|bN9^LFIVoB3>l1;i=JbOOYW3yVFuC{+&Q(aOVja6!ZwecQen@l zTH;UyWzP)+qm)&`BfB@R?0W9mnu^+GP?owGwMa|aQ1;e{j-+qJvR;Gwl&sCG3Ll|A zE8hWw2GwOp*$brb54b#6*TV*lsB5FZi=RrqSH3cg88l9J7-g!S6q^Xd&k`^0c$I=J zLa<4Lrc{)SFKodBvBM?ABZn1NT8_wDfd%EJ>w1z)3t!2|+*N5g<%;FfvCRuSA9$|G zuSMnuoDV=GeEDS!A6?~k1yM862b`4g1&lhcxZ|^-a8LLZuMq>CltQBBZ6uvhcGq&n z$BI)B{xJuc%A{B1cAoo|I>(pI^4tRIN{IUZ6;!NeLcT{8dvgmlB?{{ZV-Okpa!K1l z9);f%^4u#OaGyH@$pJtcq8&#S@2?9O*BlCjj1>d5QDcFMn?aEMxF2wW!_BD^wu9@o zKvH3?J!fIxs_7485Q_SnE@<+#tx}H~Hw&XKc-HCafMb~gRsj!d+evxWU346I6~8Fn zS*n7AwO%P3^$y1o)G*-+bex=k9g*&Ds+%Xu$pd=_5&EW5DK(i2HG;SN_V8CeH zHr#^&V{qFrj~7vQYQ>tvD<*b^y=Czyv2P*50#;?lB`sn#;@?YjA16i+Xc=q0^LI*r zg)v1p&cj+~lUATo&a2o}Hd+^#eo#93^hoGE7%3=vT4AZYF?tBU7PJmFMvv&RhNm;h zGp6NvLQi!(ok^bY20W^M*F4#f=b4tLC&@FR<$12>=}GcTHsDcxbj{Nj^1RUU^e1_y zwLCBNJpD^u75_8Jh=uu(e%bV;~~!*ZGI+_Jd0YMO?`eQk~~Wdc%pwAG0#-UvxPN9+y4MO C&t1*{ diff --git a/target/classes/com/example/streamflix/model/UpdateProfileRequest.class b/target/classes/com/example/streamflix/model/UpdateProfileRequest.class deleted file mode 100644 index 43e625de0aaad03137513ba7607f97cd3e2fc7bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1486 zcmb7ET~8B16urY&ffj-CC5W)1LJQatc@slIL=#L!QlzhDXs7Lv-JQBS1%8$$8WSJ< z0sbiCneEno^58@7?Cw4H%$a-7^!J~izli7!?M>1U4JT+MMWZyvXy!^>3+@WP%0FCQ z$&z6-_DcE695NctWQ!9tP7?{5Oi_|jjP^>}SbZ% zS)eP*l?C~^Aw%$u_`;Kn()spQ=O$2o^}v$fOVKpVFiMK5EC_?oCuPX$0OxfKO{Zwi ziWpZOz!!nbXt~hvjq+qsh3e9kN4~EONVFd^+RZD?!y8ep%79<*b1gzH+5qm!vJ(8f zRFj@KfPk_LOM$A}YDP<^^2V{k9bIuuO*-}FsTd{V<3q7Db@!d;D?4)D$|hpa1Xip)rZ7G^f!B=QG)kxx_;RAZ!5^GX42% z5#xL>O-+~2L;xNm18BJi)}zjmr@P3bp#uf#F4SD-4iU)mCqZ`d&lE`%wcjSsbt5Rr zx3DDqGRUGiYfMHwcC4eLix5q&VI2)mI>LA4tqP;(cv|!b_8r2l8b-wJAV%|eo58w( zFN+~9(fyE?@eGIeXIE_Y0kSZ4e|FVoA5sDtTvWt0aC58->*)3mVqftvMC*t)ZKH!| zsQ`eu4XlhF(-Xw*;%}7rf@EiLdSLNW+62lLA7Zs<9oyddPKox`_V0vf5gRT=42w-v zyuB6G?93KWqZkwx+!o3X&@|PnX=R|MZQ8*eeVV3vHLXF@`W>2@^N2M~N1ApA_Gz|P q)5bte&j$BtwpY_8G;Q6Xsp*1P(;PGnQ7(!+vQ-itTfh&tdj1!X%O!yT diff --git a/target/classes/com/example/streamflix/model/UpdateProgressRequest.class b/target/classes/com/example/streamflix/model/UpdateProgressRequest.class deleted file mode 100644 index 985e3ca8adace145ff85d834d3c0877f9b927c3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1076 zcmb7D+invv5FKxGX+zrdLKl)!pl?Ylu@d5~s1o8LA%zgNB|JO3L*lZ#4qk6n{t6@z z5)XU;AB8v$y^v~ygrdxN>^XCeXS_dueg8p3&uOPd9xYU=!oOs%_NeR z`$mrpXW1ct$T>s7jaX*3uQO$pPLDX$X_R5Ndqw>+wLD_8@7PxA)NoXOlpE;~Z>0^# z99;sNhk0sM!UL75Va!j`RNHAHindYoy);$^r!rPh1AG8NNR=wJSt8nNK zoCw;%I6XW&4?jE~*i}Ro60`=tj{hQw4Vp%toBBU9%D~~x!U948ynrw7ek1W23y*HX zx?^n8CT#tD?b9tpf^Jh2b{71Fs-JN9H+ZEG+@dXHIq(6zyH#a(?<-ZWsNI+4ZdFE> zqxb)0!8>{<&GkUg9jwa&fzz~IsA>7SCcjYA-C|A4*EO}EX={!q*Y!(Hjj5)4upZt2 E4e|=!%K!iX diff --git a/target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class b/target/classes/com/example/streamflix/model/UpgradeSubscriptionRequest.class deleted file mode 100644 index 639769781ba2c9b042f4d6d4b251cd3e01aea955..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 770 zcmbVK%We}f6ur)~Nds*YC~qNlNs;PhRe>rY1QOB^P#dy$W^Pj#j~zUos{N}Xfsk16 z0elqVI1LIBuwmhIec$K$`umTs--zf1-EUKcs!ghOs7?(*o2TkbNu%skzL}hI8U!_- zYOBM6plW|GZqXXGn$+&lI&}o?r7o9zuJV~-Sp?52KQ{VY<}PC+-_538Wqedliqz{_ z(9Rz6hmwm}HE8y)GIUIqEfZ&_PXygLEN##^k9DCZhM!sMf{NLNpgzLiD<70RQ$}ZV zvrOR%UTGT&IdoxI8iO(qR?CboW~-%Y&%sjAMxreb%Y4Fqq|i=KH*u*lW94u^c_Z0xMX(0Ahu}YtS0duS0I&tdZ!^T&u#0Ujf{NT|x@T`py>;A91PB z7BISDq%N?txcBG=G(k6M8@QVMiJF&i{+_)4*W_Dt8&NU&05WoJ>^%NV&1LKOcO&OI dqGJ2)t0=l^AF0I&@t@$U3n-D(U0{Xo{RA4}tP}tM diff --git a/target/classes/com/example/streamflix/repository/AccountRepository.class b/target/classes/com/example/streamflix/repository/AccountRepository.class deleted file mode 100644 index abb1e5f18645b279f38087ecffff02c951cda3e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 558 zcmbV}O-{ow5QWEu5K8&!iZhfAULaOMNFbF(6;z-n7*Z!VICkVrpxtsT794;>Atpp> zBO#=U#mJsnd~fEx&#(6n02sqCgr0!2#1&F4uqcg^6?uh4YV<;SRk})(^GkV^B(Apf zu$qLhEnt}A3rd4FlhemsB^1JrfOF?FS(RSfEcIBZSLdJQ90_G!Vi!6oaSaqONVT0` zFUJLHBVaU&*Yp`RwCJQA)ufG_mZY6U6Y(0iik{s34jVO6z7i0qmeQZ;%p%oZ3Ao>o zcoF~Mhq9z;X5)QvWLcKu&`#XTcpUKfT=g+YN diff --git a/target/classes/com/example/streamflix/repository/AgeRatingRepository.class b/target/classes/com/example/streamflix/repository/AgeRatingRepository.class deleted file mode 100644 index 685a76f3649c09c379b6478cb4209e93c4058ad0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 711 zcmbV~&q~8U5XNV#HP%|K|3KfMBDg0{5ut(}BvNU>gSTxvjaxUnVRxhL-KX;41Nczl z#6X)vh2kM>cG&sN%s1cH_s1sy+`wS~H3k<$lX9GjG*if(ww^e>@SFI%Y?84|B(V=Fef z9pbH2H0kk@#hw?MGdStzG)0P!(#e6suGYqr1&k)O&z3gsXccJuEJME7YDj)yutr~< uELS7X1h7fB2Hon^4HPzN!Zy?@A!rqEVTba{zg=kmM)qKz&JD^x0QdpzDBOeq diff --git a/target/classes/com/example/streamflix/repository/EmployeeRepository.class b/target/classes/com/example/streamflix/repository/EmployeeRepository.class deleted file mode 100644 index f643ba98af2d40c6902ec661044426b7bfeefe1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 zcmbVKO-lnY5S>)pwbqa7Mf?X9!CdsTA}G{@tW;X?K5eIKYBpK2S?!+vS04NU{wQ(Q z1-GC=@en4NB=5a>Gw&a-ZvfDT{Qw#QPA4{zw7?|QBs1>_lbKcv=_s|C^41ly&v&*U z8h)n&SQpTp;}c4aCYFQooF+bimVh(sVwt5*nRw2d5X)ZH!0SD0y;Bg zrdLHjL8S%sdQpu&@+De$RHbsSv>c>fS%W%c8Movg?DP&xHcu`S(4-2bJyNkjpF0vT zToLg+`oj=1UisRL@PfHtmQ+?Agwen1Uzv4@FtTh)sbX_vb2p)DrMb?7pWQkstrT!L z%(;Yw?p3D7nl6p89!ne9vEyH5XKW`~U!)}KR)yST0c*UPJVt^CA%G2NK$}lZ7Q>;# Xs{vd5sM>AA&R1p^x*S{lbr0YZ?Nsea08PC=U_MXd78+{W@C#C3tI*rbFH$EX?n~=D(shRJRtFa%qjzX5 zgGoDpWiZv0p%N}N$l&xxxIO{6*B`h+%n3p8v0eeoH#S_a0E@%yd(yrj183d*$k2ig-T?pI20Z`( diff --git a/target/classes/com/example/streamflix/repository/InvitationStatusRepository.class b/target/classes/com/example/streamflix/repository/InvitationStatusRepository.class deleted file mode 100644 index 1455c14211bc06f807372b254db08b1d9587a094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 682 zcmbVK!Ab)$5S>)pwOX}$^W?Q6n3Jb1f`WpwQt5&RZ)rPSQ?tpEWNY^u{38#3fFC8! zcEJ|3pdP{`ljO~t_vZcM^$h?nVLyNxgYM9z9A_fU6msV+igc{xj9bi%lit{ZU+by# z!b_uL3c2flT?Metpgj>&!IjVn?+qq6^Z_&&oEn>OmszQku@xyk8~emZ!V5mh#4`F# zCKiAUT4Sk47e!TxL8lWf;A3CHg-2B?_fqj*R`L=mBp$cuAI<1al;u2H$DrZngc&qr znP}o}k-@_+A-mBEX;6FVzsnMy5s8;2x|JGX^e-j;Tz#o}Rk>t4@Q)XhZ_ofEqOE cR3}mtwrJI$MUSf8HtftZyU?cCps#xXpIkuQ(*OVf diff --git a/target/classes/com/example/streamflix/repository/MediaRepository.class b/target/classes/com/example/streamflix/repository/MediaRepository.class deleted file mode 100644 index d530c238ca2d44aafbbaf4722615acd95edffcac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 951 zcmbVLO>fgc5Pj<=aoeQSlolvoy`h#$tPtwS2&qLNfgFmOst0bHc;jxcz1DgS$&H`H z6%L3qKMFCM8k(X;syKMZv-A08-v0dc?K^;%*z(~qTpNjxf~O)+c%<{BjRu-NS{?XU zWOy>r8JA;`6{+NS(E_l3duzNQUl}x_cJQnSso$vNitjdC0IL$EvjD!Q@!kl=5%P z=I>dDA>4V-(4;tL-=4LfVZMAm42>5$rij%@Z42J&+kIkc@;2c;KREy}KKpP>E6PU#r&1QFIg2!3XeBh|NVJ zDL@E{mF+2JW@r5V@%jb;=P>cX6L6f^O5$8qb&0ZZgsRF)J(q;FZM3trkTGF~geJke zFVKfo0h2;KDOoC$%iH?`6X(O4fFnz}Y--Xb&q!7HZ0S*^$|+gYYRG$4tDaE6IMXIQ zTf}5DUE*abU~fO_6(Vc$@Sp=`PM0#$jSJbtHM$3z#;VnFNR5D??m2>~&W&<2LIF3w z@|{FK#DT_XH;f`YW$l-Nzb=6=`q%b<&>cuFBWZnXOzn&kyx`mf?0(nRVH+fuBVdJR f#2-L#fj+E*2LZoEOv1+|j|W>k-EA9omdN-M4ZgG8 diff --git a/target/classes/com/example/streamflix/repository/ProfileRepository.class b/target/classes/com/example/streamflix/repository/ProfileRepository.class deleted file mode 100644 index 61d9013bf86ad74e1545fbd3dea795541d7fc469..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 640 zcmbVKO-sW-5S^{Iv3|9B5WIR)1ot4GB0>cPHDYPO`!<=xt;ufKOtiiFvpo0%{88ex zg{GjQco=4P*nRKKn|c3ueFK0C*!G|%;5f9I#F5JK6lFmURTiguBpv3q&}7|McAbrN zioJQogJl8jp?Xp>RVI;lgCU0GK|{cab%`u;r%e(&mEp5>k1|rEI+tP)2$-@z_^JPp&6aNJ4 z_SjLK;k_>OAjOU`mK14g*sQ~EMHlQOS{mmls~Pz`T>(oRbzUFA8{okT)S$_?I&0yy Y#!-WHel$C6z~;2lf;Q&{f87H31d#a46951J diff --git a/target/classes/com/example/streamflix/repository/QualityTypeRepository.class b/target/classes/com/example/streamflix/repository/QualityTypeRepository.class deleted file mode 100644 index 6021ab877608760f4014a350f3c2b8e9e1c93556..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 461 zcmbV}!Ab)$5QZmJyS7^Js;?lJlXzGWJm{e;h^=^^uG1xJHVMhBc3;4U^56sbP~zyp zE>bTZG7}Q`=KueB|9E`^fGe0}Fi>z-`bN_Vn_x*Nj)cw9nw5?ed@}4~r*B)dhP%5i z(Cp_mgKY%|H9n!X=qi1)sHx-(h6+x7taJ*|xM~@(p=Tc-^#d~MI$$3>4Y&p>7|l)P zklRQKUW(u7y!aRPy!gW)ITllWb9rxO;a!zq2Y+lB(cv)(Wzr zdy~w<(xr2rk&Smr!SP1%i7Aq2$yKj&$R$AqTXJ?}O(3^S>Hv0O^xem>Cw(aY`v9MU C#+Ud2 diff --git a/target/classes/com/example/streamflix/repository/ReferralRepository.class b/target/classes/com/example/streamflix/repository/ReferralRepository.class deleted file mode 100644 index a5eee1682a101888cf5b1eab1575d92b31d074e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1241 zcmbVMO>Yx15FLlMA?2%-eo)}V0kuNH7wBaxq*SF+s->+`i8Jo*Wb5FyE!$g~GyjSM zKY$-qb&{poRTH4i!SZ^>p7-`?-u%A&`3nF(!K)@T7`#tR&hbLzRw4Jnp~!nmUT}xj zco~cv@GkbyIib4aP!r}DEcL~?;7aI>AD#9w2~AjF@WHr@d+VgmdQRl{-MDW&6+!U6 z74@nIR#XKUZ1tp0cLyDPE(5xDl9)n=PTJOK8NzXh!S-fcVaG;i(arN#87P8O{Ky7r zv``UI{*2+wq{m{&j9@&4-WT-@rJ`&Hh>0&T= zJ%L^S(^&dIujBt-nw?BtlsEsxsmXqPtBZFcwOuN-C;g~V+SV$G>Ap}Nr?gDRADVDj zD8*oTbBba+6Tv%?`B7`X(h5;LlMQZ-S_2&Q|FCHmPfXzw+?R@)bbZpa?}naXu+b%B z$sE5(FHaSzrA-h+hmaZGPNnV1DQLpLB8`IUsSuIZ8Qh~aPt#B3v}Zp Fz!lPXh(iDX diff --git a/target/classes/com/example/streamflix/repository/RoleRepository.class b/target/classes/com/example/streamflix/repository/RoleRepository.class deleted file mode 100644 index fdeb0ae759e8b2d105472ccdd79a4685a29e25a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 630 zcmbVKO-}+b5S=2hf?ud7{{hCtdhxWzL=!!j6$~Wa3k++q+itqE2q*uS2Y-M+$~em= zf{7-?Lo=O8=e;*?`tkYp4gfcB5I{x1+0Z2tXDUez%8Wd!WNh?IdQ4rW$@#hLIfK3L zG=Qpr)a~r>UM2YA{IeV>QgV{2Rm8hgEdd2I$J&mr z=N)bl&~8Ud@IDn_;c;2YNgLTsNjs}d$SUrUp4{j`6=Z}y6Hv7akVap}R#EO5u(KxM zMf8UiXh~CPLwL!wuPXw}>BHz>ldr|NLKZppqEOK6yW9`)RvVu0@aKL`3-<&Z^>UV- z;G@p;z~Gg&j#S|sn{@K4+>9MWNAncraycQdRlo+H8h;wWs}EokD$wAp#zHu3@u|Qz UKPqoKu)C=2L5pLZTlWFJ0D*wVJpcdz diff --git a/target/classes/com/example/streamflix/repository/SeasonRepository.class b/target/classes/com/example/streamflix/repository/SeasonRepository.class deleted file mode 100644 index e8aad4b75fccd14db9ea7524cc0979844146ec62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 567 zcmbVK%TB{E5L_2ZTAsb|0dRr~mXJ6FaX_LHRiKD`!M521hs2K74zx#pivu6PMV^d0+U|AI;tz*J6E!0FZRmLi3cq(H;YxM0G z5WuQ{UXBka3)Gn$j&d5i0M-PY7|x`vSm`WfEa}nky-d)d%qv{x98|a<6tI^not#f& zVny~c8FE70tc~z>U5>or6&B*91Je&iyQ-zF_b)mO)MefcdH`u5fv7!W+I2p;qx3qlvXPuJ;^bvFsgthPsA%YzT#Ly4)N zEk!RLCKEF7&Hw*-|9E`^aE*fu1BJ87b(-hY`IdE*z|_rJGuMH=i$+{n=#qosIQe|&=cvFY_0F64Ofz3sBrE=t)mad*0VqzKfCa#A4o_yp4OSSo>oAGQE6&R(g#*} zDK^o0@h|K}@rO6IV&vObzP7G#w$3keg>mWnQ1Ojvlc1x`08WR71TqRWJtdG@|$^ke|ZIfE7x@)Wss21V9S<%k>1wUthrG4B*T z8yT59sa5+c?ZGmG#y|`OPlZZ&t2e;Nc~E0;s!hUeZlp^3Mr8P;%_EP66MT@1dF7j2 z%mf+i_oa$2$3Yb7Lb+xfsJLl^$jKpgO@Ry!4#Qc&P^-j0nkEV-Q{I|56sb?XZqYsH z_)e7N9U9A^E23oyCz&m1P^{6@ZGaB~@ZBu{ diff --git a/target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class b/target/classes/com/example/streamflix/repository/SubscriptionTierRepository.class deleted file mode 100644 index e1438c5b11febd1ff3f9de0036e46a4c0645037f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 592 zcmbV~zfQw25XR4;CA9qO8?crIPfV#8kYGT8BErOWO`PE1*pcHv`wYAh0}sGMAufqT zBcV#baI)_#|L*g5{`h=*2Y?Hh_~0>^#irn5iAAXdubdTFq)INi6{V@9Gj_$JdQrtz zmQEUdFNMAPKJsC}V3OlAa)mnM^F=OV=fgIGQ)4q;l~(F3wOEK3W1o0}4tZW;pMF*1 z8pvRrN}ZgqZs-bw=`>u!BiF!!s)rmR$-D_9(jDJ{+`_!!J!>zlQ(- diff --git a/target/classes/com/example/streamflix/repository/VerificationTokenRepository.class b/target/classes/com/example/streamflix/repository/VerificationTokenRepository.class deleted file mode 100644 index dda4ba173ab44d519c6614f848c076dcda6bf969..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 525 zcmbV}O-{o=423;~5=!|y0!!F1D^>v(NM(V7Km@Tji4!JGW}=A;?HxD~3l6}c5KfS2 z7Jvkc8OyTt-uLXc_m@`yn7~c|0|h6Et2E8As!P%hd%`L!&0Kq`U1Qk!g}x(iGLs-1 zXK&q&%1wNuK(dPQDi()QZU(%mg+ILYT-fj12b7R{0(m?jF-d@F1^MUg~>MxhTLXRFp5oXk!PM1 zJonr_vxL#VavcAevF98)NqH*N8?l@Dgw9PVY&iv_j&VTav^XvTs01B|@K#GA|RT0Owh>{Swu^L5G3uT*YOq7vU=?;IA z*phKj)+*3w%)?M|V1YrdE}DXeA`W=zxsFxqz#@Zlr2}pfP4{YAMA%aLmHWa9UQfh0 z{5}yQAcNzYjQxCvYSmqpF^Y1NXH5qhV#pJ!z~< zjTOx{9|HBnF2wQ+(Eq5=E9lnpTG1Mw(*nz!Ib&OJ5lx(Y8V>1*;^j!TOJsP4%RW Wp`0Umb5h}>Dh@mV4~3YN zEn}r(zve#Y<}?iu9s%! zv`H~$sGqzb@L!0(wXmcp=BbsgqTIyF&lQ{qxG8n^l#OceuU!1T^payQ7sYK1ADfO1 zX^#UXVBwP-uNPtB&4Vxj3Y<5$-vkJfB5BTDP zR(UHQ`~W}7vS+g-F`ANxol9?@)7__ge*XIY13(3LvPdCq!mu!eVTRl*{+in^_Zs$- zomaw948wP&C)GWMbfLJN!3Z)YvKEfPVpw(jrY-h)v*ijqRDs~lU03egp>U!=ssp#2ln96@OFAX}Whj!WM`{Og zjcQ;-bTnU55Yx zq8P?ynAEf7dzJl`40N~iy;jmS9)aUE#htxP-b%EcmSLH^_0)4W?1j_dR6N}UqRQ6% zC~(Av)IFN|>tU_vamO6J&lDIg(3jV5lwNs;&O=xr`z5+<(4L|_TlxxCTKWw0Bi&M1 zq;(YOcr`Fas2mD(n`(Qbh!Wih7qc=f(RqkOpp9V}muc1XHtkIRDt&_afevlv;n+AH zGg@tjgppx|K<(UC60q?N$NRA}Jy;{bUWxfv2}MtuR-M4rgkwY}@CD=jJZF1&MmjlM z9fUvGkDvc1{u*YJ_{&NBl<~f=`o$h(suTHo2U$swxpwAf-oiBA!ANVKbljh|ipxDb sxrFBi`3xa7dS9!5vqL#JnbjVWZZfwz$UYg*_ZM@s2j7+Pc3kq>Z@{!IlgeJnz1{FaFy91He3fFwuiv1AP|yF~BhT zly7s}$~bELvMPrIEWJ%G+ay@&ws$A)FFPk6~PgyvP zF~Vm<5M0;(LA6}-9j$B^YSpUeiXdy^Ele1A+rk;V!;pwPA96$C$za|K#p-4tpEZ3% zLhT0Yk+!gyHE@>UVi#nDuUxg;K-NH#JaiEag;NW(aIO|AS!p5G#5r8hRrDT1Cf$w5 z!>+QEMFSTZ?#N)n4yzjGa=niJbHut{%B+lfFX5eHAiX>dsIES)FeaDNN~vE;F3&q(ZS@VYv0` zu^&AO(_P^@w(cj341GCS5)31G*B48*%DM)twdoQ7Gu_xB zT#rAtS?UmrLT$>@1FnfuA|QJSPx6u`Wp1}Y4a0IL$nviZ7J2R$@!?FgpG^j%-cG0h zx@C8y_882A!l5M2c)YS+;;HsrQdb$C{72bzEL+OWEXZ2mhy_=3ceZQWyrO#p!^8?T zRktFFZs-!bv%W7C*G+};NO#7Vm=aR$R)t-NN!C=(NvWu7^J>g#IEs0OQ|S)~!p=v$Q3kxso%kLZqRMQ}Ubb>qSBew9%l(ok!c<<$ty(qPURNzEhZI){$X4-x zzZttcUbhwhy5$`Ayn?GlmLPd(qKHQZzSf;?jUm+rN!-EW=C{xQ<;VXYhI;3G+yYzt zifCM(QGY#}5`%Tm_eGHNco>S%z_)Z&wdK@_F$3SxX*ph$9xySzI8XK4LpQ_+^h@aX z3*Ftl^xY4e*00iRfyN#h&CDKHX6jcMKhvxS*XaK+$pWM3qgTct>3>MGu~-_{@e$2P zFS1JS82av~OPdCUPw*+JYV&0pwUK1z7Z_8&2@I69go~uv1H^#oBbjR?iOZg)N*q)=IECo$BSh5HlcVAtF&_;-_W{A LUytzw-(%u$GA1Dc diff --git a/target/classes/com/example/streamflix/service/AgeRatingService.class b/target/classes/com/example/streamflix/service/AgeRatingService.class deleted file mode 100644 index 174f0911d6fedbdcda67b03f4cd1a1e004ffb193..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2552 zcmcImZBrXn6n-ucwk%r`Q=p=?q%|!8v|FrIN@^7%STrq_AhlMlH_1)dvf0h--th8I z{24l&3&bIy6r%kE$Q9Q*-b8J3Pd^lKO}kisBC z<|*Idrp=wExmJBDYLa2_w&hsz4nu!odUFV8Fr+~@Fbspithp^y?D1CH7N##f!CMX6 z+B1FO?N~KoE;hwFmzLA4gaU?h{7|$m+OBU&*V||Ku-pU4J5q|V;)}&$j3BKcW8gJp z8E(E33d3;2a_UR_WnL9FL!nTPS*S>l+%HatqK>p|bFD2c*Wq?COB9Y87{fS2inrS~ zMKW1Psx~@KO(~lz(Wa>5bxde@!@!%kz>p0rb-@zRE#79h5xEgOJPDLElkkY)qJc@| z2&E|~dvh-d<_Ngbp#Uu51s+~D@HX;9jO#7izS!9E+}#+f+oPk4kw=N*DQdXNFcPy_ zcAch%0>kB4wrbm=$?Zk2*=Y$!F7MSuKya2SbH>0d-eEZ3;@hI+IyEjIT5^kazT}Sd z8Lsw@FK{HvpN=V9Q#EcfjGcC6lVPFoid8sGmj_C2T`;7}mLu+WT2YpNY=sh8~-1-CV@s^OnBbc+Qd{^TF&Yu=wqNY_A2q|TU z^`1>Uis)h(w|T2t=lL$^{B?%;mw@+5CmGZ$ZpW*M6-)6lc6>HmQzgMLvEFf{)e@VQ zZxLaOj^j!iA(V0I%(Ck>O~0*LvZ3PNb-itxJHivL+;0mr8Z(NfCD)ZSk$5}g%g>;6 ztXjeWci7s=vQ4M3tYZbAX}D`(6`wOq^%yNwv6kGAhZE6CeYaYt#2-$A06E}1gEB+0 zXPF(x>T)dtxX&=!1sp|@9>fEkhVz#OinyIYMFpGe$ewvQgRdBJN7={`-?@g{aq78U zYPFn0G%w^B;Xl;zH6CgB#=v8I+ttICv*T_HvmDe65zZUD7EbnID$@A$cd~e*OJRQQ za;I(!KVNp;?M}ORvLq9du8SL#_oVuNe;As4@!ceY5ps#!cEysSNR)i9;|YGyz}1|p zGUVg27HNkK6^^czLsEYH|7nOC`Ei1_Q|kSANDRiR;|Q;0bKe)f25QtRCt^A+V;Y+D z>Ups$eM^Mt#U-lLK6-mnm#7a3+V#^uh3h1pqtgnl>bXAi3~XlhS7<-esSoeb{|M=- z{{f^igbZ%bX*|@%`?yIbx)-1_d_emYy~b#vj||L{nu32qtAd}L`32hSbDZtN0nP!x z;r#3Y-cp-Of8xqdq!QZ9k~NS<8l&|79>Yc2$95-=>`vkq7RXd&_ZE=RRX>tQ*_{u< zB-dgzHc%rCopyCJu!V_#0sTSTaR2}S diff --git a/target/classes/com/example/streamflix/service/ContentAccessService.class b/target/classes/com/example/streamflix/service/ContentAccessService.class deleted file mode 100644 index c6ce7dfb5eacc64e8d8994b8fe26eebba9a1ecc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3330 zcmb7G+fx%)9R5z&WJBC2qEPXUS3n zJ~-3z(ixjh`_So=o#|u$9(DTdW)p)oq=nf%mz>|_`_Autd;WU)+wTA_Y$FK)`RqWHS zADs<>l6k9Wau>sNGoBS_CBj#GJYkuO=}kC@Ln;nyID(@BXX>QZa1R9bE*NI^(rOTN zJUeV=$DQGfYb?t-yGQ^>x?T?+WK(pFTx!}c2t7w?6X;fv)X;-o=IkfDZi#?uIG6JU zmsP{MyyID%+4lqO%xOk=Aey=LL@a_Y}$@<6af$D>sUIj`b_h+c1S->D+2TGWrEvR57gK5=I0P z8%M9PETf*%^W3}jgwDD)PYgXraaqF^Tou^D90S`4xh!)6ZMEASSR`1%<9J)eH4PIR zXH(2ODgW?PVJjw6iD3%UDz0m|;n~+nRyl;gCfB-IC`fxmcl_0}8s@xH7hTewr4cJD zqqrpynjQ6A-PZ6f?oj9C1G4M9mUU1#i91oePm{6HC_ZF~-5eVgINw!IEE=Abbb<*e zUMl2m!Z77jF+VTuIep$!rPc{6qvvkvw&9I~NyS|4Qv8YX0GA9Cz%wt1{UQg z0`VXQPl&IRVw^SL`=}^qeAd-7_b2s2KqjFFMKxT3ezOtuK-M;Z{(8Zd3D7GV+=zfm z0Pc*kHfQ`Z4^^X01D4)&yet z8dYkoQTO1uv%FuesK~OWQeydo@?*c#Gb-_7$0%=E=O0`dcViIe}U*u{tWdejzZA+-i#)` zx6q;m&Ai&obJSj5jSR9J(a!fOAlVPI@Yx6~V3Eu2vj3EAnC(FF8PuLX5pG&bmJol2 zSWgMts^b#chr9{4{mPy;IB z<#wtZs+!vXmS8X)hQ~eJXDSEKh8*(rnIsn`dkQT69!Df9;^GB%3URRvra44w(9Q!? zz=JX%af)q}bMh6S?x#=*YWJ@=5W-Y%30;ppoa`L?1AQf&8dN%zXV}!ClyJs}?Dt9- z8Vq-Lgi9Fhk8~6LnAcO=)V9d?HSiT5exJW}Cm6bq@J|zdKS>zi_70*KX}-_#{{px3 zBEKrbxQ!8?l9_U{RZ1jmI6fs3E{X)R1CvnEfiBB7uDW)1Mb3KmlFS+tE1oN$&E~E2oOA?s^z~0RSTh>TC*s(G zts1uJ*pB-Iw!ZyRn&?VzENxmDDi{j!`_7`H>^#U9mJVNHv$2n8wZ&%53KdVD>wP6a=8Xnbg4(}1zR% zz(fc#-;e2}on263ayp&@u}8U7;79`$Rqmo(*KOoLWt^uYk7WTpG;Ta&TIre)lntwd zpd8*+6f}H5$J5HBhiqrTaC54t%{yjBuGr2Jb3IJMl_Tl ziW+jE^^-b2g--`1rBZAJI!lS=7k|GHswx^)m(S_=Jib6gvvQsW3Tz4yRo3z{~bg5lw_%f-Y9AD4QI9|k8HGECSOL$peS1qAfUDpX? zD_WM0HD3ry)3E?(z1#*>zE%{;{35=g!ja%nR)-qAny1MzN_JTI=+t|2yA&< zv}|3zi&T-7bET{>sslZ?XgWz!KxR{odeX#-OLafOD;j>R<0tqjqf)Pq>`|$9&Mc&D zle}18K0_`M=wm$Ou!qiAt~HyMBiXFYw#*jA{f8zSF@9E>S=X3nsj!`bVU{--GiEw3 z6Li?JdLYmCJk6%Jj~T)edX42Vc$eiac=X`#1O6W;o5onA1==UAtenngW~Fo9Bp$MJ z(oUJ_8Pl=UdpOwSEvkk;IC<9{n7G(Vvy%N+`UxX2TqG!sz&STO1Oju%AkyN z)UGD|Q{jQCJ(w$>r%3blh7@uoV-BxhdaO!nJ*qHp$7xT;l=K$uIVJ&%isyH@)+JdO zEqIc?Y4*>LRdu~|o>i$?ucEpG`IZaZTh}E6wokW@?G2+K@f5q@8zC^0cM+)z0Kk2B)-rst^hEHC9FsOWR}E*g*aB zT>?EU#1VdfMEKA10t9}>w>a;w^7nHWzu*LqiuDt}L@NjRslw~8xX0jK@ZLOd6XMst zNOe`yKqs#Ass{`38@$Fhf#2c=rK`>V;#W%(?@!)>cIhVCriVIXdyCMcxVVPSp(5^$ zV6ccC5!}X>TiAVR4SQ~4popRBk%8B+-}Hx%_`?G?aP&Hk7jY_zBF+RjqfoF05aV?l zZP-q5JNW6clTW*NSnt76c8zKD;u7}Y3C=0#0|C4cI)UP*?=AHd#~RIOfD*Ac}n zu2-f8BfsOw?>W*I;TGjuO@#QP|%%Ik<)=)PjhQSA86U z=sM8v4sYObiWKU!z&W+^1n+8Bd|>bf=8E8Hw<0iz%TSfDc-go({@@#@ zU^ffCs9@d3A49n_v?$84E?@4{-p*KOrU*OAC7FtX0P%hj;DE1WSFoq7q$^a?f{pas zpD25sxKd9fnMoDaT)b~W^|m!UPif~M%JTogn%&^=7uN2;RN zQ`X%R>ZVY*c}WM{eDk6q~CX9Z)w zoBaLE4SZIi31SzxI#K27?lO|@dRIr(X*Sfb#JkeayN2f!n{VSwMSLYfhkc#Bqlj;- y#~-TTSQXE2r8O%0O*n~mjG&vJR9i7dR-dTC&|AjP+X%zoefakpYZqy)*0y%nw!XD}PuutXotZm#vP^>S^WGo#&Y78W ze&@G-f4_6iy!elyr-*1ZzaOO>%GD@Or+g}4D!$OzZdAvOL}&H-trwbY4pYHNR>E>l zVai>&sHKo1RH#u@ry|msD%$MsYICR2-4i#fQ%=$}x;x_5&gztz+-|j*)lFu*Wi-1t zOjEk$cB9#2r!2=#_Awpb7(yWFcXDZD2h|7*lXaRx#Y|Isl6Hp`H?yyqmK_4k4I_ss zswFy2qr;e{8J*@v!?6;bLAoa#0^Qot0~O^NI?bfR!LQk4rR?^P%9ZfN&BB#53t8cl=Fmjk#8G2K;w<0rFr+HMybXc36 zaLj~L+ty~LQa-&<8)fL#k1pV7X2eRLL|cWw!Mu zEvIj@W5rW&?J#P+cwgNd>|7J2#Z;})5}l5vwzM5&(E z=yV#bWr~W@T|L8EkYjubUROwTI-Q|WqfSj?<_YHYC&m>{2&q? z6BI}JaEN~~t<-2M)4Y&LJaeiEqSQ{NMjbkJQdf8wjE5$G5G$2|nRwjZVImyXFPy*? zJZGwzpqpZ{X3>@iT?nmtv$#far;2sh$zirwkct7(Vo8;DqDXOZow_LjT-w@e#oHCZ zRgTTHc_J3Qxx+Z@M*4}nNp+R!7{EG^g=-tsqtiu{#EkIQ)jJ(CiMSVxI4l$Bsx0t) zBT5eSYP4Oa9kg>;-Qad$(yY|#?j8qO9)-ge5$R%`K0%)Z6qt#m)z)>U<#b8jSYpG73;H-KW)<6x9xJF_*5^=^DD0DbF!e07fTNl8iqp6J?T6n4yNl*DzG} z!FEFu6_un72y;DsPNN%i+D$hy&CH_hFn%Ew>Lu8g>BKS7HcK%Wro(7J<~+Jdr<>^u zO!)@j9yV_LXhH{x`_ioOa)=V`x59b4jUJ{IV>0Ux2$CDlYm{!MFKhG_o$jDJ zGYA^AW4#@Zqt3OHfZhVf7SCXsJ(k7&o{GP(>hv|b8!p!75@1>q!Y-XawS&Jkk)-V-A@m|1}RA}s9Q%vZHsh%NT+W~U`G(xn=B`8%HX{^ zJuJW#9L67S681*9-&~u|1H;t5u=rrL^8HG4jw^`k2w;6GH0o>R_13EoUgBS!# z!v-Dh@iSt~BE{4`ot~g4nRHC$7iK8CFj0s`ynygKWAgp+}k} z3QyT?#Cy&49b%}{gYKZRVw7H^A8Pa?oqkL|VJZ(1CIcaXf*T7f=&;G&ZkZmUGu4fD zg)p_cjwbXJiH&Be$4;cgvDQzsi`+on~_q_B=@M==2-QFY^auJQR%Zc2yX(IZX7$F110SS!b$U+}Qv~I&#~e`q z4q@ysI{j4`D+0Cb^Mmv+I{mBk7h}~KZQI(Dc8{x>0(w&h{)bNgDFY>rG94h3Mt?`K zovr-V^>%MHlWXi`w*j~Gs=q?|Z~8!^4|Vzn{d0Ia?t=-sfum*;lTUY*Y1t@?t%L&= zyGcv~3!KBb8t3V(3c_h>#^D{|!#y%Ap3qo>8)upF8HsWM7wQ}pNu|tg!muZ(12S`H*_ZKq*)If8)3?fbD=QV!?ScgQYv~eCDJlh#SQHdo{bV8 zYL0npc9d%-D(miE@V3>A88L(+dd4i2_7RThJcsAP>n+4i((yQAIx{pcjtTHc^C5BO zp`S;%Cc?y?@_yP()K2GQ3c_~VhPzD9q4zsUu0u2r4ZzN^!_rbUs@!Lh6?QgbK-X zbv`cx%Pm&BX#-j#d;ws=b*vFa(-=$9vhicl3jsDnxrQj$;I$D(F)+gm(@{jN3AYWr zkx}jvs$I0Vh`aeXjj&S<|Uuul)-Y~ez^!0W>upO4&0S;v!#vp)!|vHcqA1&EI6{rlWMYS{eS?uS zx=qx%AfYO8$w+=Bz9EQKs?rbCv)+DKh>MS%q*3pvoF^;3;*qpuP2!$y$m)26ENkNB|c{Ks&|E;KHA}V)C_Fbl>`WR*bEliP?0zp{e|x#j%}5Riqee9 zCn*=%WY??75Nzt+)r?ox!!AbVkPLh8VY(Vsfr$S}DRT(^C*8z6z!KEwC;}ObhH z7@?W&G26sZ;h`+X@QA_8{J#mjWY4pjC^INRM!SQ&rg*h0Y6So!&FBErWh6w zWmvj|zdUxlm*8;uI6E%kITrjJp5;e5fENK}m@w=Wri(V)y~#Fn4Nh&5n$Xa-(J39n*ci72oIC#dw%$6lUbpV{}Y}-jW zNu$R_=~OZA8@UJItq7(R@RL%NKc(|y{75lBf(OsYgKz8nsJi=(+2$RyP7< zv0%$$5!R{hsot?{$r6t6k5G}0(+g)kofh`&(Gwd;M>Qm{-(aa~b9KwMtYPg;3pKTRsH>ZhsYrFs1{y}UHP zpGwP13;OA(@=~pz=9HI4`e}Z7X<+ zBZ_i7t&ndE@KHb&&QBNfF7)nj=jO}#3fzGUB_perNGX!9kk3$Xt|TpyG*7ie)_m0x zX$w?K<2Mh#1^89q$NX8o%2kf+6DWea z%O9gfX{uU>>!iv-IzESn=tQD4o%}S_ugojWOVjE!HB_Z(-EA~!A8l$aX&Ioi_M^X6 zF-T`i{|mhS#>#v=o>Q7vnxCe&Om|Clmq*)H7RbduK&9%MrqpdT8T$9``brD-g3nx~ z^yM@O?WGWQIV3s((yW9bYM`o9aJYXe_S@^|T%7fH;?Bl_{bh6pj>gt0<*#*R^Oe7w zujXsu97MbMTC{RN^(wv&tz6K(h_C0*LHX;cmT%zQP`*ub_(uLbK9sr%V`ftBhqOYY z^%~WGK#MgpKA`y;c1J|_g*kjRRC_I&%s2BFz`g5yxyqcKo_?D|ztES`@24{QT?e&_ zb}Pry?jU`dX^<|%89~($T}5bLmqX=4^m%E1F$ae+w++%=IrKE`%Brng`Q2s+cqSM> zi)Pan_{rH&;(1We`P2eyolO_QKhIODDtFoRRn@5!grgID2OtW{0fh#opj@SlBH|Be zxvLDNfP5C=`HN6M4u1*#ahmN#;I+6D?_Rc#_OuSrH<~Id(sWOn?v?9buS_y#H575*K)fl3mi|yW4 zcfU^4?^NSYY5KFgem~Ri{UQ1{>G$vV(7bhz(0^49iSMPE@1bdOUzO&_5Cc^3=wUEG zv>B%Qo4R&!ffoUF3JfoS47&i0m&2S_z+0~b$XpF`UV|984!(aq#@>w4yQ!6KBopl} zc!)(e(N3J(if$wtBGkmUD1R@ZdcKu!gV|g0W#8@mWoXW#DQJB~>9Eh&VV}~RaNfsv z@SRYY8x8p%Qk8OmgMX(5bsGI`y~YJoGSP4&oZ$|PVZMvM>M1!2%Hjxp70=P+eXO@m zXD^X`XQ_&BDzQ{_CUUbvYg#z1QtDPj%Ae(fp=+sLXx3DEqVmPd(bz zYA^t*a*&Ue_%O2Zah%FDBL3~jHg^DP?*vHR1!%b&U~@0k(tQAv`w=}4&;|4mZKJ(P ziA&)nr@=5^13=^ecrq{} zyDGvtqYD@<@IMX0EgR(JIdrRtbRyJMGr%V+eVmf!k2US%`qlwny{<|+rg=?OWtvZu zo72;LhFlvT=M8DzEX~$5pHr2U-^GVCTx$SHVhDNV3E;z%ko_s({(i{+G&RyQ_+sc; zCAW(YExwE`o{VKUN7=()hb}6qoWB9jcJU#Xc-C+kac1Djh%;@4HZ^0*c>V|(4anG* zk+EH5Gy{UTsPH@lc>#jF1VLT~E3ZJ1S0Tu20p@Fb=4*WBXZp-f;d}YMQ08&g<1r5b z74vmtGv6_S`7SVjVTSo_7&EY-3ECvLP|qd zlJf&|MEyCUDs74$qH4Ar=m-l-kLo-n8&Q$yXc~XgQxFHpo`nHemySK)(_;)c6$7xc z9eco7Gn_zq+^>X5^MET3KMw33MEe{n;0HrfV}@KqJ@E#eaq#rOODpy+?^%jWy7 zE*q#p5H%V?I%=T{G{@2-E#^LBB$G5-vbJR!BSXo=zLu88n{gN0&bUB$>9Vc@t-8{w;kH^V#u5#u>o@~T1y+^W$to~llCK1ULy1)Uf>F7} z(xtu~9OAaQn<{NLW$Z*cWhC3eWPPKKDP%agdQ(O9SnM^WaS_EYU zS~RTGaVB0$W0WODLIrBm*7{`D?B8Lf_X?cpDpN+F9}HJj4QB}~DFcFp)E;ed0jR}l zoTFiljmg zZ>lWOso?^Fg$18?CX?o{k!-hyb0aio{l1u~91_HZG@;wrml(;7L{qtuLDPz+hoWwl z$VQn^Vmr)ubSP~_?H!41)Nn#Jgm!F@DeEGE2EXckw#9<Q*ROPRX#BGZU1-)laU; z0UL%uqlRvQGs>*@{Nk&6>6)|uVV`aq(6;4^yle*c#A{JS0>1g)mRc{F=>>>ZPRfXUMUcg zX~EH-`Gk=+4VJE{A*{mXI=0I!R7DY;@em9QYKZBGgGxE%Q~OJ@vPhY`bB4@!MQrfY z-KK$aPA(AA%X>a1CRok3rgKrlGNY-q9W|KE(iSNlkJ@R;J2RVgIOwu7gduDQqY{ll zj7+&Wmx&YJ35$D>){&6}*0QGEV~m(#1RV6TI&9E z4QeT;bA;t=iJNpph2(!W>gcr9K>u$}K^z#a&rT5j=rf%D1(I+Bhj zhDI6bUM0*Hg4_jI!_~~}Q!}Pe4?{SBH^`WJqd;Ohq}({1BBpX9S#}+sa{Z$jvqW6U zG;~Hw3P>IPr}?gB~((48+F_yOM;u+T@yRwVRlN~ z;pQpb;d*>3cgQ^$;Wd(@2XL#7cj8@6gu7eKB*H!5B|R?>;&vw40xHpX+RUop8#I#) zYZ-Vd>t~#h!b9>iVj1>GH#829x(T);SrTB@7#f4P?n-27B{ zEpSqI`=#4A_iozI*|olJ`{wn%+uJ)jHf`zY7Z9E7u{zSJtZk(10VA0+E7?+1389tc zToD6QKU{EsSqAzRxe6ZVA}<#l-?9R(Ggzk@950n_k^Lr12n$C?I?fzDyDO10dsI#D zHwKe@6IP{Wz_1c>?7j?04-2%ED-ETO5^X%Hbeqi3@y&+C+-aK@h1A8^FxY0EkCwl7 zO|FHi&zWg$ET^Aar9EQP{1o+(#`?;Q)0Hq2GYLCq3fZ;R`=%&e*wZH5JS|W&n6T^} zvM@8zIx{EKYk_&PN=XeS_S_l^mOK)%eVImamt7K2a~~Tywok4t1k}|~Ia)P=R4Z=+ z<9#~Wp0$*&WtBENk#S{3dWjPo zp@Bp;L0Yz_m{6D~7~%r0U1@8$C7Y2=<&fmr-n6x=#kP!8)^N@VMt*urf8jMx8r+DT zOZR92%=|ehuQn&u^d*KOJYL9N0#{6$Rh}a=EB58rs{>kL zbGmg_rl@ydw^e?1iQU=u?vb^H^|c^~wX(dB$tpAy_)t)dobH(f%*3PK^sMvxqBa>& z^`3vcw*reM?ct+{Lq)nwFW4q|7D*Z-gK=Z=L~)DHl&28?vANuNlFF17vQpqwcY~6^ zEarrH?t~&L`9FB7sVeUOP!u9kVNX=?2kvp=Bqe0D`ehXrF~6+h50n+^OXsYZxq(+~ zysvTt#Jy}=ABkGwX*VYukdU=p5=?b$rpdu@k=%OryTuD$1l|AKXUYfj-RX1i*oc|9nZ^=5FCjL z9Y0f}fE-on_^BFcaun3@oEnAXs8+{M)TmC5cm?=lHL4GZIU=lyxw<$(G!)%FBvO0Q zyLir3?4oJL5OcS>daPyRP#8VwC#}24L1LqkiYLwN;;wXhS1wcHAmC$n(ZY%6hM9kK zw)Af`d96Pr+^-!*GTE2lS)S3=nM#>fN7Be<&8#MP@3^GUkrW9lHcu1t87fmxJZWz{dQQPnRmJ=HHSDET`d_u+otuDqYm75D%zTbb?f0G}V^ zn_Ku>&EII_A&AE12cb1K9YkGY^FhpMj06s%p)pc*5Rr$}>JRe&r}6{^Eac&N5f3Me zF$+s5^K?Xbw|E+s;zRtp&XI`^<0BkVy3$psC#kKpAS$nYd7PI|QsaPnk~&x6qkL|t z;v&#Ea3VMbym18t9>T}CjLM6RR3*fB`x_rdWE?MPt>Qynr0OshRp1dU1IE$(I99b* z^SvhDpIw1gEutL;9TvO`bw|*?HQaFs>mTEcIZcPLQ7*jLUD(wcJc6FBk?Kfrd2=Li z2%B4Ll%$%&=&Qiv=#h#BAYo}#nluxV7DBO-j#|Z=%(HlHcs4q)2EBaRhBoZtwVs4% z$k99x(SCd!57RnYgM-vtLbM-;_)bE!AD_Ts?o@-_)Nh;+Ex=YB!6ymy3UuL9c$8Rd zBb1L(tsr-qjZfnK#2R;lZg+!AqPo0L|K&OUiRtie~6Rzdnu>Q})P^A(nr3b|T{ zV_2<_>rt>(39gXvd<|cBdAXLqLB1PvwO729_$c*vdFr=TN2(nIB2_*MlG1{vqo|2g z)38+YQ7oXGIn6FPcjw8ek(k;ztVrM}LVOO4V~@lsQthXw14Aoa4)l`{1B|sxNR(|f z@G`XGaz^WRT*wQ*jTq#Y7)No^bBH7whJ~F9rd}Gfo#c`9lJP&52W-p*Oumy8mQEj2 z6qdvtQxulO-RBZF%AF)Nt6d@njxi)95qoMD)Otjei2BJ`OvG>Cn-0<{6G)!4yGR%D zWYhz?6i@a`lqFik^)(etDjs2+{FbJ@yGa;}?$41hdq|j3l6k+6H0^l|+ViAYn z{u(4Ju9ZpaKp{9BTVG4>Tt!xlkrh{y71xjz*D_qLQ?|+^OHJ~&4&?nkfT!>sCX%|(9t*2(Kpf2H`CF#Q1M&;uTFOI+Ygu_euy7A=7`z+ zt){uQ>);;0f8Xkv(;AR|mHuU-%e(h&oc6a^(hY#-<$8F2K`7CemcFS9X zEP(IvQ+96^xf8?M3oqsy!J&_9T3Q?JVs?I;vRfwtR|ByHK2QpBVb($Y#wdL<#)g5YF#k__4GEHksA z@m5gr07X$b#2djIl%_3}Lq$}?`>ywe_lM%n=Ohr8!1e&kZ#b9f5|8M!|443Cw6|?QcXJ zjS3PfnxG1tku^s%`hIP+nAbD5W9i!HVBXlDv2|X3nVSSrAIHC zw&9r8xWL)n5n`(a zVa#we$1n@Me3b04_)RAuzAJLRt7n0VlW?-Y0=qO|XDy>BHHCGwpMZ|unsZ(Gr>Hm; zrwPoo^=!#9obihtBX0|=iV|C5f2KD)xFdl@SgateVhNTCv_`mU>P&&uknVJroMFA- z7+KBHb3WQ#ITE3zwY$Q#ZnH4d(S+q#pZ8RMFBB z(%`YV!&mBByUT@J%XcIh3T-M@VKqrkn*|mJ*0}=*ONFfTOs2O~EanZ}A}D7dqhPIy zbvToYaaU3$RG`VUw&rcUZ`d-g7Fg#S6Cp4RhHt8ZvjkQ|fFL1t#xp(uO*k9RQgDun zXQN%fV37x3*-@Q^T$kO+)sN|YR!J9F83xsLTTMtMKpRz@3+4^Qu(ysD z9R@lx$UX(<3oNa;zAK;ChqQdBHB=hqsaJhn*vt^#YnIZZCEH04Yh!v^E2K5o zmUK8^(vF#S165C(B%5U?a2_s@32m#u{II>fj%AQ&t^IL4hj#7O3psu_p$pp;T%@8K zI|MdPk6v_G3#cA~UcQ(_!a%>r%PPK*gaT~}c1I^w-{+H+E9jwHB8>Lu1bH}tJ?N9j zTufyxQCXmxT!c$byF|rvu~(qc)}3H+>!%Yj*;ZH9rMOJN^Hf}pE10+|OxSFid0i{e zF=JZ3r0*JJVrd zFa*wu04)?J0dV1@9J9&ANs&;UUPAk)YwL)LyiA_;OfFryB)8mx3KKeT!LO zdc4GNhWTbYTEStCULIaTIM7_-K9dw!0-1 z8D1`##+}WLSUI{YOa$M81g^og3SOz=Rd}_)tO}PTV3bhQEL->9YbJv2Qo?H_6<;eb zw`F@+hl_a}uV)!R47I$G3$jd>z@`UHYgBU-+`!$UqIYL$bU?SZxqYEj_C`vtAw=?V zMG`a6CK-K`inrjcw3;jlBJ50VL=o~m;_bMd#hrdt zNs}c(6+8i(_r>u}frS+cr42h>Fw=RWt6OOo6lwXpRlEo9WiU&A%aTXe{T+k_!?Y{# zhjzqq2WcNtHmuocEzhDdH=Z8Qb(#F-=Ah@K&O24yRjt$V1tqW^Bhshw& zYK4O7_yW@7X30vMR~I~oy7gTWF|@_;F;);1<7!DVL|n&wT*W73#+<`cQ@uRtzym5K zaF992uRPrK2e!yMWNB0+yBk2aVLL2+bR1T3pWMBxlDbc1QiXnA2tKpfwacnVy)FT&%k9>F zf!G!%7=hW{MnU%yN}o26XG^l%%xd|5%`)V--xza-4F>SqC`2O8VjWD;J9KB*%9wLw(*`XDE(xiGA=*FntZ`cN@)G4#0n+M5(_HNS} z%GgC&D-TL)Ty0t-8OPEJwuGMNAUB!1!!v!AW}c+zYbFt9&oF_y!}-r&ZwLXG5!3B* z0g}{^2pl4*B^VV;Lc(h9ztSuZ|4KS7Q!r>^gJ-&)3)s@k?QF+*Whum_d~B`cZgQOy zW#W_uHTD`q1!i4~hHVUX-}2z;{8nD3^x0fjeb5vbwV+C9`3x&?e$9?&q%oD(Mh9}* z^2t72&NJ?^K)hQ@)ZBe&bEJfIvS1FES5fD_;|WN!!eMc$6}=p4H{7V8s_I}(#+xI0rrUKXt<(k?z3zQm6SIBg>#=Zn`jfmxU8ogu zdEH*#ZJHycVvQ3(DDbK#_7SY`@DuO!R33kBm_@$iEm}U`%d)eBker$jr-_A%Si~ci zSj;-5Ql0ywy&UA7y0!(N-2DHC!Owk{hF}XS?NHO?CGkq3t5DFbEqTqhbz2cUNUoT& zrdl3T#B$!~9DgW}0G@k!y%*=f6fAm^@^Y7NNqM*GK6xAJK6zW|K6$IiXM$H2_4qtz zeSu#$@!7yjt)Kng3dn3sTGWbYp#(I32 z@AK<930w>~5F7$tivj{)!B;)w#pRq7XWLC}_aZfch24D4Ty+@B>R6Pwf))Onhp>JJ zTj#TNLmhUmnLx)an0W-}?M-$b#FhuyHz&F6FfNo+cKTCxwa1QN&)(Gs(c501s!zox z(4VTGaR~b+@O;itQuT+CtHVRs)83G3n820yVH9Xrj^L`jsfLtt5cb`)E9L^$PKdyJ z$~abY6>A9KS{k>Gzh|Ny8?c@CfIT<|m+>Aj%a%Mg@-TiruES;*%pPxzKA1e);%oRi zaae>gd;{Mk7Ofb;xA1L(*~2;C!FQ?oGD`d&juOll5!~G2(D{BtpMhW+ue7N_t$CBG5$Zwv&+a_@9t^WLX z$O*FjU61%J?+aSqKY4y z_@D>-UAM*px8BY5g5+L_>$z4*`+m&l?{-rDB2s+^`}VMR7h15Je(Xgj`gkMRkG;5r zUff5IT#6E}D6Yj7xPg~mx3l#QdGN#_?&nR`!!YRG5!b_m49W%6^Go`51|e_(ckRA2Zwh+H1CmLKIw~@Wa~f{-ZnNPsIPv_zTC#Bo(FL&{PEu6_idu z!C#vbax;K<(O{Ri=&G+eljmjM6_ay+;4spCoPhe*E3sDPGIc!dd8CH?H6+o$xsE#gr|idL{QfWhZ|C0-=MsUZ lSeei8-(o@$r$C6f|D7oJ%q+h(b4;4^xub|BVwvC`@IP{Rb{qfz diff --git a/target/classes/com/example/streamflix/service/RegistrationService.class b/target/classes/com/example/streamflix/service/RegistrationService.class deleted file mode 100644 index 2c7c99b2aaf923a3e132b71150b593d7ecc9db0d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3405 zcmbtWX;%|h7=A8`6OzFt;=bX61PF`^wv`|jAt)M_LX2V;I!tcDz+@)QOb}}AV)wn; z)wX}4wt&=g+VA~QJ$>&aK!Aky9CJ=)x$~~iyZ-&pAAbUf;uk+EP${8GMm4rDY@bqR zRK-xuv@$d?#Wj~{q;xZRX|A9r6xyh! zpgHI2hBA~DK~@d&>==$qI3c3}CmDjCacf8zYIJAV<_7fP#J5<(2eR^8|~44zhv zZabaJ5K(ki2?el#L1a=4XYS1C&q5)_7pGC+_H5wt>v z`O$_-aXBO7tdQt)3UG_4os;pL2w8w)upHF@Uch+?9Wo;5WaxaB)S!yvUXoE(6noM$ zsoJ9=0ZffYO2k|k+$H0pFgQR4FWV;0yJcKL55sOc@1wNg$(WVa&2HD_nXKzDRE7GR z5`r=+qYsxE{6u-lu(WBO6kl)UzxK^Qy;Krslxf3A$haY*;d8Bg?n@YB zP&SYAhN}1DWxOKcRT-~goMDe=iK}P060E4?Z&l1BNjn<UWYQ^e$44O1lZRNT@|-yxs=L-dB&l9Jj{SJkEm)NBFseIvBbQG-nAj!u#2 zHciX*b}E9eqhtcmQfNAq%(OEo>I%6MUp=N(g0Sz@jKccqAEo}O@jjYf40}s#mTkge z(e+ABUy3ESTxym|VG)hsC^d-+YSmTPFFFN11;twH-D#)v?zH{Uy%Gd0-Y4o0=*^&e z72WHa7r~la<{>qQ=TY0-I*%Q{(s>0wr0Z_t01go4LDb+7w&F1Dl1K0noi^wB@i9K3 zH^6Y!w6)l^zsb zp5@2M_7em|BQ1s@w4w=J1Vj&7a0Ox7AKGXNox(UygQFcYv{K!tb?N~+J;>VgUamm) z5MSU+LZSf~e1)$GvT<709^fJAoFT}+!M6nRcb*OGkcVP5BOc&;`fnl5AL#DGkKVHg k>rpxrznY-$3Euh*ws3%X4hV8!7b$r}_X_;9BIIY_UwmNN+5i9m diff --git a/target/classes/com/example/streamflix/service/StreamingService.class b/target/classes/com/example/streamflix/service/StreamingService.class deleted file mode 100644 index 3e78ae1c40f6a7c58fe5f96772cf2c876549997d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5228 zcmcIo33n6M75>JSJ+>IYm;e!EQHTlNK%s=Bwa8+ODQ;~HHaNve+l-|rF?ggAGb6xs zOVgxH(xhp+CUoC-N@x%PnuKm?o25_3=i)x>6!Yn2Na7Y98x7ow z4h`EQBrO#n4J|pVnB6raMRa!7NC=RX4uvXhM@zot6iqwbPFUV%U^6-iIO?q7=Bjpz zFjp>SBx!2ATq@a?aOsq8^yug{umxLbjIyNePz}o*chL64!6UA7Lc_LDnTP{xoe`?4 z<8}?3BAg&y>d#aW8b=E6F|ZwXXoyRv2YzO>Xvfkggrv0SPTZyAy$0SV!+Z_7vjUnZ zZ73T@8as8|ZQvfoSxj8HLZ!GMPD!%jJA zx*}P0{G>@(Ij(g|WRt#=EPEp9AF;e-nCNlbhe26g_RJH>+Q7n)?wVMJy%^H*0g1H_ zGCC1Xs=?a4#MN+Htt|?`5#S=Jj(r+#jSv#XQVGzpKN|T3Co7nqs=iH_wv{y{D#wIZ zw*5Fp(I`L04IIQF4TdNDuz<;ASiMl&2}ypxfe%UYILVa;GMb~9H1Gf(BpbKK7(L-J zeGf2*M4g7MozbW&7rc}@D5R#d+Omalpnb>S>hKKsDA)DfATKoN z$4$E|MrnT?y6cOl>eOK-wX>Ca2v?qO7Ja6D-C@-ntSQdn zR-E1SV+KBsPp}uVu#A`|LyycN@8G+2%o25PfoEihK4swHD&WV<`Ft(lTkwd68>u8m zjU+uJ6OV<*E2}=8E8F%=D;~vTIzDUQb9g+uON!!r;p{kv_U>MyB`;)QYkw3^8u+}F z)UbEA{Q9(kFQ~8MyLEg~!^XL4I%L}-Z`%ECzFc6bsVS;o;t->-uh5)hYi900gTWSj zl|xflJm%hMcq7vLHHC8F)D#8M8YZR2*zD*PnXyaNh)5>YbE{3|kQ{+nlr+Q!SWoGK zVXG+il?zkCJ!nqZ+-V{i+RrfU|2 zFI*n8q6(vcT*D&^>T^wgsO}OacoBd^f`6$FLPcA{{wW(q+kE+~4h1>9s+aD9p0HN7 zyy=4cb@%C&|jC5dCmH8>P(sCcuhn&1*+{WUDt&FaW>EhVd!ms9n)Z)T=P z%utF4B7nP8-BjN}Sg7I;u>}JFsUSl}Z%m>+4neH68$2eVE z1u9kEj7<2 zvySjoVevRoJFcJ9lE;GKg8aXW&J&a2ns>H%Tuw(bP)nBK2D@e*i$ET1kCk_$Jq@Vk89>24926 zgKXczw*w`$x5?1Ty{p|XVoe2!z8DvJ&qdtafJ@i}RM2$=slKK}Qw7^9*pY};(03Zk zFXO($?Yk~u_p4-B-g^=6mkh%p!$@EAWsDxafCGJcLhq?yETQ*SFpBl+@)oW%E39h5)5T4?`qehgTE4N#_i}tA9^r^UL3$yO!5Z&5O;If2AeKF zg&hpyot(z*!jrt6K8v07+uisfZ>Z;RFJ9&i@d^g;8g}C?`I{8>s9-%9cyu;cSMeQu zR|V@TzQ>ge)>V8Tk1OB5g&%M)MvW>w`9}=&0X&8u<0pi~B_w2ss}7}YVNWAxwHp;KEK^yQ5d2?_(nNQ`kD4gyZ!H7W%D3jL)XU>}3sg%`}vN1HYuTp9? zY1~FzTvm1ZLng^asw4m6m+{Qu_Gd5P%Tl1GV%DsD)=PS}g+=MVvIu<#eJRgqzzd8L p=tT{`;qyiQ-#}?s_=56-C5Wb)eq**opa7T=Xai$m;Sf( z0)V4g3c}E$s@d>(x!&JuFb&(mGLxTGT}mQ&0~@ zV1L@mrnK{FHfLxl$F(&zJ8kIaQ;ufO>S--CmY;Icww`l!%N+Cf1e%=C?kO#2Il61v z^8(#N6~x&Eq7sQk-X5vfEebZEL10^0{8Y{T{_ZdLFq>=3wf zZ8%ryB2YW6o0X2p5l!sITv|quUI3Q4`JEOMdP-QO*Pe*Z=z|Nv;1`R`-QH?%(CZ8qI(er7|^Gq!c z3A6>OBvpg?nVCz?!)lJXwk{Q&sK-4r!dMM@Zw{i; zJc>gg8AYE!R|PSFd%^`oiiqL}ExK`@82V8ye;reBAMO{3Ihq>)LP0H+lB+a2Xts>>j$Hf6|fSx%zT z&rF_vj4OB$69NjA8FN+J^%=JYZd<|8I#I(EmH(nKjNr6_*W(S$^_-D+2Gn`hnXN6y zgA`i9Di3?3f`{c{61}5lMjjP|im52l3NoO=8;jA{Z&`+>n#5^VHS*fYX_ow!)8YIK zC_Fo66r6!hRV1jkt{Rd$RCbnWeHWZlU`XmZQu|3IGuKpLAtw-V)Y+98=C`rs91E9+ z7;HFExC-)^6=3LpYmj{TcTd8TC&5<#uInID) z>RKk0pN|L}n6&aq)z*@xraXEfGvzL*So_nW)cgf14O!>~5ke-r&yj#J0@LqugrSwr5 znFwjRI};?0Yzy%cv7XU!L}Wt0zw$_+Ef=NKh~*w*-wea>L4nQ@t6WJ+PD@h$wtW;- zzj?;+B)V(y5rOUD6id2HOAknotrj0+WCofSw*_9Qn73#;+j%}i0 zSYyOyMpawQYOZEe(v~#)jLUAvWDYw=U}ROYgc3byg%z@}Oh4f1bwN1DsBBm%i=(Ry z2l*`;&2~`eLgt~jkvS(6GPIl*M>47=RK9_QB)pTTg=fI^F$W!CRZH{bA@u0kn zTcfI;3EM$nO>ItEZ_KDDHrwMZx>HfH7Rw04XQi48yOwBT~qC7TxK{<2}^FugU)ozL-J=ilXMVgtL1 z%bTB?^9zB~fFB8LUUqJ{-)>lk7h}AqT~n}#=Nj-FH-0KNex_i-d;5ia`=x?Q-rKL` z+e-?b_ugKXZ@*RW1MltkG5i64jN(rU{*1qr48yuPYn|gT<$07!snbja?|xa&3RQf2 zIb4haCf|npRWoB~PV8=jy7$wySf7}H&i zj!*_o4kSkm)p0aVHGk*Lr#O<8C{k)(6#wL<<@%(oq&^n7oldCY6d6HW{sn-mxV&_G zPhN_>C$F)5#yH~F;Nzsez_-WvloRN-wnd1x_DhJibzH*ww$4k~*p`S~LgTaiT7^$Y z8m#kvN3b1rxD~hXHoFNskic#DB)=T+CE-)}G~Xn>)I@kIsf*Nao4lfPGLkE)PXrga z-WVY%s4(~$`~b2&h0ls{&1k}I?BV~tXyL)F zyyCQDlmqbuI`I&?pz%8E@atQ#4^QweDd9Wp+gO0_0zQk+dGKAp=eeq))JO3Je39qf zhhcmPUnVZ27{FKXRYLzY%BY3-5B5fRqJ*`siXuQ};OqQZM`7RKQ{bC~HsbmJ-xMbO zpQTle?F)GGQxdxX7tzpJ?L*cn2wjfgCyBLSQ22GwrN2$RB%%oFH&~ZlM>pX3ezWjnV9f3AG7+Vsts<815F6;DpaKd^`oNt=Zi`7*G8Q>150C`L-QO&kL(x6;{9D>aXOK N<@Rs*2mXb`{{hMLLBRk3 diff --git a/target/classes/com/example/streamflix/service/TmdbService.class b/target/classes/com/example/streamflix/service/TmdbService.class deleted file mode 100644 index 69a39c40c0f54bdf3f9f064d469f1166d3c51a73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2884 zcmb_eYf}?f7=BKGEQAFS@q&n=jTQp3)M_inOBJs*QA!YNZEH`G16i}#O?Nj@#_>b{ zMSq6QSUWh=PCxmfzp2ydb2b5D2~MkiNX})?`@YZnyq9zS{QK4K0Ir~%KnxuQI!$yT zE->&=)}`f0cg4D$f2azfK>V!j+TnSDj`4}59(1F}K*B^XOo3Fvt61u>tW+Ik1);BG zW!bSGTY>WHc0pN-m0~{Em<#kgQu#T@RxXvE&3OKb6;yrOU0L>JMLqKTvc&@ybreFo zuB_#n%ZcY&g-FJ_yDpvXML%{J7%;IDg92yXT@{^LQG~NNKJgxak_+m`nhL_}N*0wL z-0|&PRTa{jt05D+v4{=a^Ddov~eYt5(S?e^JsA-H*J2be5!V>veOp zGc4KkY-&4t+q8FjG>}zc$t%uF-OouYHN>@RH;Vk$66`2+0z20O+z3L{9g}TByqP7< zn2@`QE(@mWb^y02AcE}6g|bl2%4*ZqM9!=E1$EWdWDK<2SJT?AKzrtO)+jGrttzW| zy=nh1d0xm%P*xj61l_nNFei6{`Dr)q3yhV*uo|4PEW09C z)O3^%y0$~BYCSqNGb47y@m&JnFey>4^7ZKvM?a!xu<&JJ|}m^WNk`B8&WOqJbj zFHt7woBNHXY_7LNy)>{QaCD2Qc5WKrJ$3NyN13$^PH>p*D8|dV1B3dx!&yvUtPr@! zc`wIH{LKZp%n!Wgw|=-nS*9zl^1wC9=uvA-q<)1+P5uJod8G9^|ND6s*o96Ep@-^j zaJ8p#8eifjSCorz2xK_#5?p}l*)E&ve?mMGm`%Nav4%cA&|YBIE9~Xz-w?akF`2v9 z$*qyx8V+_~4N3BwTEkQfFLB}zq@Gb>3>wXTs^YVR91P^A0`$Z8z5d!wnzmpGlBdX1r}mpJFMwx4TknXh50V97j2I5{ zjdz5$labZ2hLR1dqfM)Ey7(0qXe39uZiv4yVzAe@py|XSKgDn-3SOL0m$<&mUq5%h k<@)RB{~P4FcZ2{A@k=4z!{^h$Hg6R6!{r7Z6Pf$FB_y$eu>1scLS z>Y)pqJ7!PC%|pgiI%&pT&oPau@uYPq?wZcDHD<>5TINA3HF2?HPdKLQ4rP}GqSM72 zgJ#-xEzfoi3vBJHK-9?t&8AyG@01KK)UgPQ1(u{8d)!Kzg>7;?59(yM7)?=)=vazV z1Qt!%EzQ)TdtibqB$QvduF34s?X4|3}?`k2Mup*vd?n8 zyxQ9bL0 z5!g7liA#(iP&aO+659^E%$0i-;VzWS)0(6ay*_jmOXr>$|r{IH9qVw@g4IMf< zv1QJ4^9R)MG^y)IPGL%dK07r*iWlHQ4HxO?!Zxx}fsay05YQ}l;9$yh1eOMEsK_?& z*RWk+l~Q@ivg0xg<79}rW4oEM%tX(jF;khg4m$+a@3S*8!!cti+lv_)Z_;+GtIR~q zvtx!rig}Zk8}rk^`Y^h&OD4Krfm4Hr4|xu=RA=*E4bK)>S@5jhWYU~4l3mV3W{PGB zoZc_c*<*Q=rV~^7pf?e-otPiUoJ?h0FE(n*v!|_unP`oTW<15`sO?RXm1kj>hKmK- zD(KGlf>{|fi~&^3KSMf(aS2_*H9g-`1X`Nr@CYB0lGfu=8IaGR_f}vakZxxsuLu}` z8ZM)yD&YFeD?G{VJ27{-Yf(oYyi*K z;ed%YO08|rIEKtqDnpwIW>-6v;9g_Uk8{Y9g#l!AOiT1eqW2lDcd;z7B;n(RaR`Ss zTqU`DK@peRY&&ThDM@^PkT-u_@%BO;FTypdD0sc@4z>vHq{(Da#7_=Ey4y?eQVlQD z@p8O^p;N*10iT=Y_`!M`a*b&kbWKq7Y`2oD0l{1=k6sl)HJYSSUajLbcx^V7=Hq7W zRGJH%I=o&WKJT=nx}NnBgwPaW-f5D&T(9FzxPh^gt!6&u-o${DFr96OhdjgahOH@c zyXlTG83l#RCdojVlhvIiu2#DE%>v5`o|Y@Wfs}O%lg+$2mzg(=o3KkZRBzSsHoTq2 z&c=1WkusDQuOcf4$gUAjn~qCf%oLlx9#y}5pw2qHL!f)8r?01bIMx!|F*wj4V__Ld zXPc}MzY`pBJ;QUGy1Rykn>zeGrqQmU*tXuCy}O5-o9l50-l^eTI_|`~1y;;?G<|xE z9jRyTX5>tpdoreMFXKUl<`REdITQlu^8GdA4UHM^tfhM^rCqChGA3j%)asj*sI(`h)T} zg-={ISm_KIIX@_<&qpZ(!NlSN)!|_#knFi;wztinT!&BQ_Y^ZD9%E!=(#|+;9X`#y zOv-wG#vEZ@q#JrUj_dFU_ZVs_Kjn8-52fL=0vjr%A~V-%#CO}?4z^su-s1#y=LfE; z>Em)gj7PDn4xgu2%t~Gbm^yrssD7ykE`OOORh6-@NIHY;^eE<=Rj;aF8 z>Qc$2j&gA!4!fly4tbqIJhNh3FrR$eS*fQ8Ap5!~Boai2YAB=;JQCFlNPNcNio=T$l+$JH$V@Goec zytMV3>|zu7v_WDki=809RpbqAeYP_Zchj*$7j9|>FG82f{vh%qvAG1W{nKdexuo0`IonY5Uc+JlN0&C~M@ZCb{Z#LT` zqmGpqnFT&UIoAA)J~wgSw@G}FU#qyH3OkF*9E_DJp@H>%>;SW+?=j(gfu^ZD%39HL zjFf9+Z>a%G4i}br&lOmhNhetB3d~KM%%-_kF`%#9($lwWn95Wdk(ynaEbTS9TIL`o z=0t_IcHxxa9Ow%4l||Ll?}I93?(F@!jP7OMm?$V{DQjr|VIOW6+ZXlzv|z}ZNEu$n zVUQ1%sL(H4DqG0zYs=I0fD34;MxIM9WQ*k=PN(o_{}m@aj!r}58WuU($1I~lOY?uW zQQ)H;pW9SdP{Tl%q!(rNpR3i9i{b=!IGsyk=N5x*30ThWmjWw(>6T*coQwN;Q#vbY zOpPXt)#dfHny)gOPxAOtxp0%oGYX_dV{6Ri+K<8_&RH(v(?32 zjC&W=iX~yxiHI)3BD7e9xNxdmXw=05H9K9-qPnP4vt@F2hAuQUJ5$ci(nYPBt&p>o zx~NgJ)pB;WE~?dRZCEskW=*u{Vx4F$dR(zm)Aj+A6-nBfoEhUxaw-Jt*)kVqj|ctA zN6HGa%Sa`Xrn|b&whv^|odLHjb<`rnVJavHKL5?^ktfFuwzfXyZX=l-vOJU4(0fyS z{_19ybWK+iaTdhFwXKMeV(~OV&pGqtpbY%1F0g{vwkp1TYKX|EdX6IUQCxlbjIF+W zd{$pR7ppH{dHJo9cRjxFe1+p$e!t3Z1HU!+8ZXgb=j?96X#B2eISSFT?g+G&)+1;!lNeG0p+-^$Dc2hL7*BrWNXijXBPmIZ8cA7d@g0t%wL}Mn2T?#^ z@O>OS5cn>Cm(=o4_DA4*Y9R1^{D2fGtGJD4NLf#`+=qsB$FQP`SD>}P4Awo2_Kpyj zmq$Z0*f@jpX0Y`wXgH4UeUYA{*!fWFqtMzOL4E5ZSi&!Np0zL1cNDuHI*x&TN3o}) zCR!tr2MMv7l$Nv}!(Ms7KL3FI9kqn0oxyYO!U_VM+I9?ANPsaPAdv$YkJeP*k1J=8 z&J8>@I5>l=iKsgt#8taVvUoJFRsOdhroHXdXwOvfm)>e27~6A$~*~+{)39 z@e_`y#k@^V;4b`B+4KZ{#*wt?3H+R+YVz_?`~ttE1@^+jukdTy^f%n+L&sCtsG(ic z7CePb8U~i*w_FdAPPejkLp8~Q1VK{ByYTNg7Wh5>kTohE;~~;b_DIdpsMkuBzS39e zK*6Z5DzWS9q+O$-)){!wP#d+abQ>zw?U~AJ_E5KjlxK+EJ&etKbKQdHP=ouGo;GEf z$m?Ve{^;vu51zoEluq{GNsgpWI+P|tN)vSuPhm3^P%8f)Dc2MHeUiVq5YEc~XZ*#N z{{a$|0n-}ADHFQ66UPXV+I@RvBf?H)pAED@1ppnkgf>An_NzupY zBnOq~E!p$)qWAftH&RK`;8ldI!e2S6E(k0wK1<*lLB!dD{tY+yg5E-OnIpDPq!fDL~)4s@?kK)#ci13!kZO3qX z6?V5p-a|jXyBe1=gM3gh4}7E}bR73F3(d$Z6ge6VRc|h(J`oMo6lR}_ zhH4A5&qPC7ekN5FQmLhb$sKqFRdp>@^-8MhRTTPl*oxO7W8>N0(7zmYDx~N(lmK_UGWQAL)tO3-JBaj1p_2m%`0*fEKWDRvA_(_@jw_QGmc-Cfx@ zX-V(A+jM4g<5#bVVxXb@wB=aASq&|{eN&z2K&OtRfi4&taz(eC6F1Cq z#TGd~@Pt{Ov8@|9UwHFYQRFV0L2<5N`N2fEp<&0u%GM=OaeXUry+sWt3QYugHKMw# zI%8g%v(>;hbZgjN@!T297WK~>p4^1oi>uj`)Rci8ctFFpvM5<*T;{P&$Q)~AQWAF> zcnI&&ke;{1g5}Io@2n?$KQ8;sCS*@FV^>;s8F(0vFpvy^B^rc3u?gXfQv;Wh`KWjfyc35!?BIyy(TUiI%X`VG`M)a zq@lOBusT)qec_hUK3)kd*D>vUih15^pbuGQ@T_1$@9B*b&QzVEB+X4!D;3)kUOtIK zIIQD{fgIkeA*J**2-VQ#dc(FaCg(hNK|_CJj3QbqFj5z*s=SV)8Xj)~f^Y(YoQnWR zq90EfcoN4oB;>@{DzSYp=s2fgpMu=6+?*`v91UWfoD+^E zO2apbLWQ;iBN~oOyVZ>8iHze08M7M9xt{fsC}jdSV=AUhFlYIhN?77Yk{H6M>~!Zf zJQ!C#5qPY~eBV?Toyn0(&Rq7h{#|4L8t)G`nVjb_Nw@s8pX`mEz zVFHsno;NUs%j|1SSYme7T5r6+q{9M=GV6S4z$q#5GRnu-*d42|=6=X(nKtkOKA@q~ z@-MQb(QtKIFqj|Iu(w{P=WScen)ZM3)7RwK7cvxAIF>RhArNxZpWYkfSv3YzX z@ghcbT-9)(iRTemHLsH}QIeq+8peA!sFMhf{03yoK$RpDm^Cm5OGB$~&QnE0(8H5+ zr5(k~KE>0vS0ZK`C`-gx48!G0u&Be;aI~3kD>pNg;g#ixCO_&CA$vxkX7T!%S&1T$thG!6n7+g@z$%L=%eNS$0f!C1l%ov4ohrEAY|gL9V{JxazTkS- zbAe|%z8Q`?W{o^IS>NWs#BkQ6**1p9(3J+*C?|RmW$twUubxsVf=~? zmu>C%Rg$-#UmN%l-sr{~-1wc`_`QK2s<%JNw?7&9fqHvWzP)AOb@lewB;Ll7j#~zn z@wbLE+H&UI>mpZB_|pt?h6$rCXe)$?K+i@{*{YyvC^~uU>q1fL#ZlAonNv_8UHR zKKEyrp=A#)LC+pq!j|meC2Y^8TbJ-)Hr=*_bT-|-gxznEGJ#L=e=p^L@>`KcCw5^g zFN-^IiqTLyK7-Hlje3-|np#PFr4Re%1(}0@TuGZ-)k<33rdHDK zc6^TObUWEWM?n^l7*q?Y0-gFizCczwrKR|6=ibfi+t_mOHXcjh4h{gfaPVF9U*PWU z!?$qk7Eav4nb)!9E{3L4!^?Q)9zShAbQ{k~hVh8u;;B|{>j$}isf9AGye2tboKAgc z86UZK7v}Wgbn7yTr`jZ0y6rY*5_lKpo3x-sft5v)>wUDl2RjMP0Xo{xi{#VD5}?DF z#t{^`$*bAnNC%f;YobeC%*+UmqK{LTslGXPtvic2+t{kaR!6D8Wwmhyv&u%rEF+V4VEi4 zSgzDy8H`|&5Ol6Uuxqp4e;+SL-oHxQWJaEjd%v*a{TO$5A8MLHI&$1o*sh#W7?I# tPikrTQ@;ObJGkA8M;Vb9I~aNou`=zvCUG{|56XWrZMywB`k|NQbBfH8a*M-!S2L`+1{BG9pD zFWXk$F3wwbXBTBw3A9{sijKM_(A?WM6GJOv2I3}mz!d1qx&=$F+J#bHTE6n6U6{)| zt5#V#d23o`%bug2Kjg$0h%c_FY3VIHSt&4(+2W*M*j1k+NElMdf5|<+w$GbK458{uE={j{^n{nmB~R0;fJ4Q@sI! zosM6#By$1wPYk34x~l}4$miv}ou5{=l4Gk`S<*4L;uC@7L&BJH{8Z6ZDZ84J%*D}- zW14H93he34REyB>%DP|qe-G+T7Bze` zqUd6_tR%U(73lwAxOzNo;Do^S?FObfpKlOFD=dM7Q*J8EAQiGU#SV1x_H16pF^JP9 z&fsi?vtc6a9@1jbUa~!9llg&pYeA`!HO`^lU%D0N1==*6uIIc6c(nr;amm1C6GKP~ z^la0VK#bKKmv+!Z#~Y!y8PHZ-6)<*ZY^iJ#+0oZQDX2E7|+hCk!+TJ)dZX5*au3IamjGqou8Yyk7?G0^_-%F z33LXCZi=oI4^7PA5nHNW9b~S?Z=&vS;3ba7V8ecC;wwBAXs>3jUsKfCEw)zWx+bhh ze!B08z{%eKq0oIZ0+CTS$3%8!oT8j67iOjRKyz3ik#Vzje#Z73Jr5TnYQdrREw6#i z7YK>kF)7L7+&x>%zLFjdwAZLhpqOX2A?7vO#xpb=w`;WFMnemYl%c-UY6ODFhD}XQ zZM#!-9Bk9|21{(Ft3WiMIvbj*n_Zf;OJP0tyibp@0!4&zIR4$0qQ3NY*OD{fh=#eV zrhavt>ZtJ^>=qCN^GMBdL;YtpV|KMA;?r)~%gS3$z=XOlf|Ht)B=&uFk5iB{j_;5k zBSqdBfl3kG{)SNagkHMpc}ZHMZlUBBd1Iz=gkL62{H7t{^tTD0B7BbGYp#C7*)9I- z_woK$5d8x`!}yW2CVb1W9nBm&cw!e~Ja2Q>RoRVMWI5xxz^Xuw&r!jNcE#4@Nb_PZ zO|)>W^crn%uv;KA@D}F48zcq3N87LHew;Y+3P*o`jpKh{aB}d^aPg#G99+YxsS}BF zZ*ZXrLy=_UE!vWi#FaH%TR(hL?@LBHj5XXEiuV5mBN<)8?H}l~IY{D6kn}z#u%E#n zKnjO>m3Cu*Pv>!zT7L{TaU9cJeag#qj&F3L*D7&`iN-Jw$#=XKFu~^qmSG4N;9!w{ zZt_N2LY{@1V?hdhilRsxNkreF%fSBFJ9HWt{2PZ1Tr@Boqal|rgCb#>CNu^;A1V^- zc2A(1TY??|i@BX*Zl{^i*&2{~Hex><#;#+Ep(%`s-b;8!&zc9z{MTr=_pjl> zzIZv={E~Yr7M&X$ X?ihq2 Date: Wed, 7 Jan 2026 21:20:01 +0100 Subject: [PATCH 03/22] working post movie and series method (still need some fixes) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73c4f5c..c736ac0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ backups/*.dump target/ .idea/ -**/application.properties \ No newline at end of file +src/main/resources \ No newline at end of file From 1674ce357332fdda9946f30ba5d62d88de82703b Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:25:44 +0100 Subject: [PATCH 04/22] working post movie and series method (still need some fixes) --- .gitignore | 10 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- .../StreamflixApplicationTests.java | 13 - 110 files changed, 7059 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c736ac0..0000000 --- a/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From 0a3b5e263a19e57ca8dcab8710b93ef85c181fd5 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:30:48 +0100 Subject: [PATCH 05/22] working post movie and series method (still need some fixes) --- .gitignore | 11 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7060 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2628335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From 3a284107c1667913cb00030cef179dd9f3536039 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:31:18 +0100 Subject: [PATCH 06/22] working post movie and series method (still need some fixes) --- .gitignore | 11 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- src/main/resources/application.properties | 17 - .../StreamflixApplicationTests.java | 13 - 111 files changed, 7077 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/main/resources/application.properties delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2628335..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources -**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index ee5a86e..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,17 +0,0 @@ -spring.application.name=streamflix - -spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/${DB_NAME:streamflix} -spring.datasource.username=${DB_USER:admin} -spring.datasource.password=${DB_PASSWORD:admin123} -spring.jpa.hibernate.ddl-auto=none -spring.jpa.show-sql=true - -logging.level.org.springframework.security=DEBUG - -# JWT Configuration -jwt.secret=VGFza1RyYWNrZXJTdXBlclNlY3JldEtleVRoYXRJc1ZlcnlMb25nMTIzNDU2Cg== -jwt.expiration=36000000 - -# TMDB API Configuration -tmdb.api.key=324eqjwjer2wrkewjrwk -tmdb.api.url=https://api.themoviedb.org/3 \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From ff9df4331caaae9dd88866bc0f55583a75dffb42 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:32:45 +0100 Subject: [PATCH 07/22] working post movie and series method (still need some fixes) --- .gitignore | 11 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7060 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2628335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From 9ee4dbd39e55f835935253a4cc747476d26e45a1 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:32:56 +0100 Subject: [PATCH 08/22] working post movie and series method (still need some fixes) --- .gitignore | 11 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- .../StreamflixApplicationTests.java | 13 - 110 files changed, 7060 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2628335..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources -**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From 388fb5162ac341f2be67f24ea950071237ae41f3 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:34:47 +0100 Subject: [PATCH 09/22] working post movie and series method (still need some fixes) --- .gitignore | 11 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7060 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2628335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From 5d86b3662fad6cb5a04ef433f2c6c344cd6f3dd4 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:37:00 +0100 Subject: [PATCH 10/22] working post movie and series method (still need some fixes) --- .gitignore | 11 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- .../StreamflixApplicationTests.java | 13 - 110 files changed, 7060 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2628335..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources -**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From 22c8082332202bd2353d4f93de884eb3fcc8e34a Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:38:49 +0100 Subject: [PATCH 11/22] working post movie and series method (still need some fixes) --- .gitignore | 11 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7060 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2628335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From e1412632ddd457c862dddff9fec045b477d18da1 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:39:56 +0100 Subject: [PATCH 12/22] working post movie and series method (still need some fixes) --- .gitignore | 11 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- .../StreamflixApplicationTests.java | 13 - 110 files changed, 7060 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 2628335..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources -**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From ebc04e78de65962bc1f99f87509dd18dff0a552d Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:44:00 +0100 Subject: [PATCH 13/22] working post movie and series method (still need some fixes) --- .gitignore | 11 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7060 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2628335 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From 3849b984e8dfec04b1b6072ba0575fbde0e813fe Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:46:24 +0100 Subject: [PATCH 14/22] working post movie and series method (still need some fixes) --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2628335..257eda7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ backups/*.dump target/ .idea/ src/main/resources -**/application.properties \ No newline at end of file +**/application.properties +application.properties \ No newline at end of file From f407baa601fd22cc19a15ad6e10e78c30048a283 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:46:48 +0100 Subject: [PATCH 15/22] working post movie and series method (still need some fixes) --- .gitignore | 12 - .mvn/wrapper/maven-wrapper.properties | 3 - Dockerfile | 17 - README.md | 25 -- backups/.gitkeep | 0 docker-compose.yml | 42 --- docs/BACKUP_RECOVERY.md | 258 --------------- init-db/03_schema.sql | 291 ----------------- init-db/04_referral_logic.sql | 80 ----- init-db/05_employee_views.sql | 59 ---- init-db/06_additional_triggers.sql | 127 -------- init-db/07_stored_procedures.sql | 157 ---------- mvnw | 295 ------------------ mvnw.cmd | 189 ----------- pom.xml | 100 ------ scripts/backup.ps1 | 56 ---- scripts/backup.sh | 45 --- scripts/restore.ps1 | 92 ------ scripts/restore.sh | 84 ----- .../streamflix/StreamflixApplication.java | 16 - .../config/JwtAuthenticationFilter.java | 61 ---- .../streamflix/config/ScheduledTasks.java | 37 --- .../streamflix/config/SecurityConfig.java | 53 ---- .../streamflix/config/WebClientConfig.java | 14 - .../controller/AnalyticsController.java | 105 ------- .../streamflix/controller/AuthController.java | 221 ------------- .../controller/MediaController.java | 88 ------ .../controller/ProfileController.java | 192 ------------ .../controller/ReferralController.java | 82 ----- .../controller/SubscriptionController.java | 99 ------ .../controller/ViewingProgressController.java | 134 -------- .../controller/WatchListController.java | 93 ------ .../example/streamflix/entity/Account.java | 109 ------- .../example/streamflix/entity/AgeRating.java | 55 ---- .../streamflix/entity/ContentWarning.java | 42 --- .../example/streamflix/entity/Employee.java | 63 ---- .../example/streamflix/entity/Episode.java | 92 ------ .../streamflix/entity/InvitationStatus.java | 38 --- .../com/example/streamflix/entity/Media.java | 98 ------ .../com/example/streamflix/entity/Movie.java | 46 --- .../example/streamflix/entity/Preference.java | 71 ----- .../example/streamflix/entity/Profile.java | 125 -------- .../streamflix/entity/QualityType.java | 31 -- .../example/streamflix/entity/Referral.java | 96 ------ .../com/example/streamflix/entity/Role.java | 38 --- .../com/example/streamflix/entity/Season.java | 60 ---- .../com/example/streamflix/entity/Series.java | 35 --- .../streamflix/entity/Subscription.java | 90 ------ .../streamflix/entity/SubscriptionTier.java | 53 ---- .../streamflix/entity/VerificationToken.java | 84 ----- .../streamflix/entity/ViewingProgress.java | 127 -------- .../example/streamflix/entity/WatchList.java | 74 ----- .../streamflix/enums/MediaQuality.java | 7 - .../streamflix/enums/PreferenceType.java | 7 - .../example/streamflix/enums/TokenType.java | 6 - .../exception/GlobalExceptionHandler.java | 101 ------ .../exception/NotFoundException.java | 11 - .../model/AcceptInvitationRequest.java | 16 - .../model/AddToWatchListRequest.java | 28 -- .../model/CreatePreferenceRequest.java | 34 -- .../model/CreateProfileRequest.java | 56 ---- .../streamflix/model/CreateTrialRequest.java | 28 -- .../streamflix/model/ErrorResponse.java | 103 ------ .../model/ForgotPasswordRequest.java | 29 -- .../streamflix/model/InvitationResponse.java | 29 -- .../streamflix/model/LoginRequest.java | 36 --- .../streamflix/model/MediaDetailsDto.java | 117 ------- .../streamflix/model/RegisterRequest.java | 36 --- .../model/ResetPasswordRequest.java | 40 --- .../model/StartWatchingRequest.java | 37 --- .../model/StreamValidationResult.java | 35 --- .../streamflix/model/TmdbMovieResponse.java | 44 --- .../model/UpdateProfileRequest.java | 40 --- .../model/UpdateProgressRequest.java | 28 -- .../model/UpgradeSubscriptionRequest.java | 17 - .../repository/AccountRepository.java | 9 - .../repository/AgeRatingRepository.java | 13 - .../repository/EmployeeRepository.java | 12 - .../repository/EpisodeRepository.java | 14 - .../InvitationStatusRepository.java | 12 - .../repository/MediaRepository.java | 13 - .../repository/MovieRepository.java | 8 - .../repository/PreferenceRepository.java | 9 - .../repository/ProfileRepository.java | 12 - .../repository/QualityTypeRepository.java | 9 - .../repository/ReferralRepository.java | 16 - .../streamflix/repository/RoleRepository.java | 12 - .../repository/SeasonRepository.java | 9 - .../repository/SeriesRepository.java | 11 - .../repository/SubscriptionRepository.java | 12 - .../SubscriptionTierRepository.java | 9 - .../VerificationTokenRepository.java | 8 - .../repository/ViewingProgressRepository.java | 12 - .../repository/WatchListRepository.java | 12 - .../security/CustomUserDetails.java | 56 ---- .../service/AccountUserDetailsService.java | 27 -- .../streamflix/service/AgeRatingService.java | 28 -- .../service/ContentAccessService.java | 82 ----- .../streamflix/service/JwtService.java | 69 ---- .../streamflix/service/MediaService.java | 241 -------------- .../streamflix/service/ProfileService.java | 176 ----------- .../streamflix/service/ReferralService.java | 119 ------- .../service/RegistrationService.java | 61 ---- .../streamflix/service/StreamingService.java | 83 ----- .../service/SubscriptionService.java | 90 ------ .../streamflix/service/TmdbService.java | 38 --- .../service/ViewingProgressService.java | 154 --------- .../streamflix/service/WatchListService.java | 111 ------- .../streamflix/util/SecurityUtils.java | 62 ---- .../StreamflixApplicationTests.java | 13 - 110 files changed, 7061 deletions(-) delete mode 100644 .gitignore delete mode 100644 .mvn/wrapper/maven-wrapper.properties delete mode 100644 Dockerfile delete mode 100644 README.md delete mode 100644 backups/.gitkeep delete mode 100644 docker-compose.yml delete mode 100644 docs/BACKUP_RECOVERY.md delete mode 100644 init-db/03_schema.sql delete mode 100644 init-db/04_referral_logic.sql delete mode 100644 init-db/05_employee_views.sql delete mode 100644 init-db/06_additional_triggers.sql delete mode 100644 init-db/07_stored_procedures.sql delete mode 100644 mvnw delete mode 100644 mvnw.cmd delete mode 100644 pom.xml delete mode 100644 scripts/backup.ps1 delete mode 100644 scripts/backup.sh delete mode 100644 scripts/restore.ps1 delete mode 100644 scripts/restore.sh delete mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java delete mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java delete mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java delete mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java delete mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java delete mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java delete mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java delete mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java delete mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java delete mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java delete mode 100644 src/main/java/com/example/streamflix/entity/Account.java delete mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java delete mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java delete mode 100644 src/main/java/com/example/streamflix/entity/Employee.java delete mode 100644 src/main/java/com/example/streamflix/entity/Episode.java delete mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java delete mode 100644 src/main/java/com/example/streamflix/entity/Media.java delete mode 100644 src/main/java/com/example/streamflix/entity/Movie.java delete mode 100644 src/main/java/com/example/streamflix/entity/Preference.java delete mode 100644 src/main/java/com/example/streamflix/entity/Profile.java delete mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java delete mode 100644 src/main/java/com/example/streamflix/entity/Referral.java delete mode 100644 src/main/java/com/example/streamflix/entity/Role.java delete mode 100644 src/main/java/com/example/streamflix/entity/Season.java delete mode 100644 src/main/java/com/example/streamflix/entity/Series.java delete mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java delete mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java delete mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java delete mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java delete mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java delete mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java delete mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java delete mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java delete mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java delete mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java delete mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java delete mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java delete mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java delete mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java delete mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java delete mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java delete mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java delete mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java delete mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java delete mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java delete mode 100644 src/main/java/com/example/streamflix/service/JwtService.java delete mode 100644 src/main/java/com/example/streamflix/service/MediaService.java delete mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java delete mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java delete mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java delete mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java delete mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java delete mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java delete mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java delete mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java delete mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java delete mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 257eda7..0000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Backups (sensitive data) -backups/*.sql -backups/*.sql.gz -backups/*.sql.zip -backups/*.dump -!backups/.gitkeep - -target/ -.idea/ -src/main/resources -**/application.properties -application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2b6cf00..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Build stage -FROM eclipse-temurin:25-jdk-alpine AS build -WORKDIR /app - -# Install Maven manually since an official 'maven:25' image is not yet available -RUN apk add --no-cache maven - -COPY pom.xml . -COPY src ./src -RUN mvn clean package -DskipTests - -# Runtime stage -FROM eclipse-temurin:25-jre-alpine -WORKDIR /app -COPY --from=build /app/target/*.jar app.jar -EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 54672b6..0000000 --- a/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## 🔄 Backup & Recovery - -### Create Backup -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -### Restore from Backup -**Windows (PowerShell):** -```powershell -.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip -``` - -**Linux/Mac (Bash):** -```bash -./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz -``` - -For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a2d1a89..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,42 +0,0 @@ -services: - db: - image: postgres - container_name: streamflix-db - environment: - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin123 - POSTGRES_DB: streamflix - ports: - - "5432:5432" - volumes: - - ./init-db:/docker-entrypoint-initdb.d - - healthcheck: - test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - streamflix-network - - api: - build: . - container_name: streamflix-api - - depends_on: - db: - condition: service_healthy - - restart: on-failure - ports: - - "8080:8080" - environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix - SPRING_DATASOURCE_USERNAME: admin - SPRING_DATASOURCE_PASSWORD: admin123 - networks: - - streamflix-network - -networks: - streamflix-network: - driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md deleted file mode 100644 index 6489d68..0000000 --- a/docs/BACKUP_RECOVERY.md +++ /dev/null @@ -1,258 +0,0 @@ -# StreamFlix - Backup and Recovery Protocol - -## Overview -This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. - ---- - -## 🎯 Backup Strategy - -### Automated Backups -- **Frequency**: Daily at 2:00 AM UTC -- **Retention Policy**: - - Daily backups: 7 days - - Weekly backups: 4 weeks - - Monthly backups: 12 months -- **Storage Location**: `./backups/` directory -- **Backup Method**: PostgreSQL `pg_dump` (logical backup) - -### Manual Backup - -**Windows (PowerShell):** -```powershell -.\scripts\backup.ps1 -``` - -**Linux/Mac (Bash):** -```bash -./scripts/backup.sh -``` - -**Output**: -- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` -- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` - -### What is Backed Up -- All database schemas -- All table data -- Stored procedures and functions -- Triggers -- Views -- Sequences and indexes -- User permissions (within database) - -### What is NOT Backed Up -- Docker container configurations (use version control) -- Application code (use Git) -- Environment variables (document separately) -- Application logs - ---- - -## 🔄 Recovery Procedures - -### Full Database Restore - -**Prerequisites**: -- Docker containers must be running (`docker-compose up -d`) -- Backup file must exist in `./backups/` directory - -**Steps**: - -1. **Identify the backup file**: - Check the `./backups/` directory for the latest file. - -2. **Execute restore script**: - - **Windows (PowerShell):** - ```powershell - .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip - ``` - - **Linux/Mac (Bash):** - ```bash - ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz - ``` - -3. **Confirm restoration**: - - Type `yes` when prompted - - Wait for completion message - -4. **Verify data integrity**: -```bash - # Connect to database - docker exec -it streamflix-db psql -U admin streamflix - - # Check table counts - SELECT COUNT(*) FROM accounts; - SELECT COUNT(*) FROM media; - SELECT COUNT(*) FROM viewing_progress; - - # Exit - \q -``` - -5. **Test application**: - - Open http://localhost:8080/swagger-ui.html - - Try login endpoint - - Verify API functionality - -### Partial Recovery (Specific Table) - -If only specific tables need recovery, you will need to extract the SQL file from the archive first. - -**Windows Example:** -```powershell -# Extract -Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore - -# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) -# It is often easier to restore to a temporary database and export the specific table. -``` - ---- - -## 🚨 Disaster Recovery Scenarios - -### Scenario 1: Accidental Data Deletion -**RTO**: < 30 minutes -**RPO**: < 24 hours (last daily backup) - -**Steps**: -1. Identify the last good backup before deletion -2. Run restore script -3. Verify data integrity -4. Resume operations - -### Scenario 2: Database Corruption -**RTO**: < 1 hour -**RPO**: < 24 hours - -**Steps**: -1. Stop all services: `docker-compose down` -2. Remove corrupted volume: `docker volume rm streamflix_db_data` -3. Restart services: `docker-compose up -d` -4. Wait for database to initialize -5. Run restore script with latest backup -6. Verify and resume operations - -### Scenario 3: Complete System Failure -**RTO**: < 2 hours -**RPO**: < 24 hours - -**Steps**: -1. Provision new infrastructure -2. Install Docker and Docker Compose -3. Clone repository: `git clone ` -4. Copy backup files to new system -5. Start containers: `docker-compose up -d` -6. Restore database using the restore script -7. Configure environment variables -8. Verify system health -9. Update DNS/routing if needed - ---- - -## 📊 Recovery Objectives - -| Metric | Target | Description | -|--------|--------|-------------| -| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | -| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | -| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | -| **Backup Success Rate** | > 99% | Target for successful backups | - ---- - -## 🔐 Security Considerations - -### Backup Security -- Backups contain sensitive user data (emails, passwords, personal info) -- **Never commit backup files to version control** -- Store backups in secure location with restricted access -- Consider encrypting backups for production. - -### Access Control -- Limit who can execute backup/restore scripts -- Audit all restore operations -- Maintain logs of backup/restore activities - ---- - -## 🧪 Testing Recovery - -**Monthly Recovery Test** (Recommended): - -1. Create test environment -2. Perform full restore -3. Verify all functionality -4. Document any issues -5. Update procedures if needed - ---- - -## 📝 Backup Monitoring - -### Verify Backup Success -Check that new files are appearing in the `backups/` folder daily and that their size is consistent. - -### Backup Health Indicators -- ✅ Backup file created daily -- ✅ File size is reasonable (similar to previous backups) -- ✅ No errors in Docker logs -- ⚠️ Missing backup = investigate immediately -- ⚠️ Drastically different file size = investigate - ---- - -## 🔧 Troubleshooting - -### Problem: Backup script fails -**Solution**: -```bash -# Check if container is running -docker ps | grep streamflix-db - -# Check Docker logs -docker logs streamflix-db - -# Verify disk space -df -h -``` - -### Problem: Restore fails with "database in use" -**Solution**: -The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: -```bash -docker-compose stop api -``` - -### Problem: Backup directory full -**Solution**: -The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. - ---- - -## 📞 Emergency Contacts - -- **Database Administrator**: [Your Name/Contact] -- **System Administrator**: [Contact] -- **On-Call Engineer**: [Contact] - ---- - -## 📅 Maintenance Schedule - -| Task | Frequency | Responsible | -|------|-----------|-------------| -| Verify automated backups | Daily | System | -| Test restore procedure | Monthly | DBA | -| Review backup logs | Weekly | DBA | -| Update recovery procedures | Quarterly | Team | -| Disaster recovery drill | Annually | Team | - ---- - -**Last Updated**: [Current Date] -**Document Version**: 1.1 -**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql deleted file mode 100644 index e5b33aa..0000000 --- a/init-db/03_schema.sql +++ /dev/null @@ -1,291 +0,0 @@ --- 03_schema.sql --- Purpose: Creates the database schema (tables) and inserts initial lookup data. --- Execution Order: 1 (after default postgres init) - --- Cleanup existing tables -DROP TABLE IF EXISTS employee, role CASCADE; -DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; -DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; -DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; -DROP TABLE IF EXISTS preference, profile CASCADE; -DROP TABLE IF EXISTS verification_token, accounts CASCADE; -DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; - --- Lookup table for video qualities (SD, HD, UHD) -CREATE TABLE quality_type ( - id SERIAL PRIMARY KEY, - name VARCHAR(10) NOT NULL UNIQUE -); - --- Lookup table for age classifications (e.g., '12+', '18+') -CREATE TABLE age_rating ( - id SERIAL PRIMARY KEY, - label VARCHAR(20) NOT NULL, - min_age INT NOT NULL -); - --- Lookup table for viewing guidelines (Violence, Fear, etc.) -CREATE TABLE content_warning ( - id SERIAL PRIMARY KEY, - description VARCHAR(50) NOT NULL -); - --- Lookup table for invitation statuses -CREATE TABLE invitation_status ( - id SERIAL PRIMARY KEY, - status VARCHAR(20) NOT NULL UNIQUE -); - --- Lookup table for internal employee roles -CREATE TABLE role ( - id SERIAL PRIMARY KEY, - name VARCHAR(20) NOT NULL UNIQUE -); - --- Main user accounts -CREATE TABLE accounts ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - is_verified BOOLEAN DEFAULT FALSE, - failed_login_attempts INT DEFAULT 0, - is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE -); - --- Verification and password recovery tokens -CREATE TABLE verification_token ( - id SERIAL PRIMARY KEY, - account_id INT REFERENCES accounts(id) ON DELETE CASCADE, - token VARCHAR(255) NOT NULL, - expiry_date TIMESTAMP NOT NULL, - token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' -); - --- User profiles within an account -CREATE TABLE profile ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - name VARCHAR(50) NOT NULL, - image_url VARCHAR(255), - birth_date DATE, - CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE -); - --- User-specific preferences -CREATE TABLE preference ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' - value VARCHAR(100) NOT NULL -); - --- Subscription tiers (SD, HD, UHD) and their monthly prices -CREATE TABLE subscription_tier ( - id SERIAL PRIMARY KEY, - name VARCHAR(50) NOT NULL, - price DECIMAL(10, 2) NOT NULL, - max_quality_id INT REFERENCES quality_type(id) -); - --- Active subscriptions for accounts -CREATE TABLE subscription ( - id SERIAL PRIMARY KEY, - account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, - tier_id INT NOT NULL REFERENCES subscription_tier(id), - start_date DATE NOT NULL DEFAULT CURRENT_DATE, - end_date DATE, - is_trial BOOLEAN DEFAULT TRUE, - is_active BOOLEAN DEFAULT TRUE -); - --- Referral system between users -CREATE TABLE referral ( - id SERIAL PRIMARY KEY, - inviter_account_id INT NOT NULL REFERENCES accounts(id), - invitee_account_id INT REFERENCES accounts(id), - status_id INT REFERENCES invitation_status(id), - invite_date DATE DEFAULT CURRENT_DATE, - discount_applied BOOLEAN DEFAULT FALSE -); - --- Base table for all content (with dtype for JPA inheritance) -CREATE TABLE media ( - id SERIAL PRIMARY KEY, - age_rating_id INT NOT NULL REFERENCES age_rating(id), - title VARCHAR(255) NOT NULL, - release_date DATE, - external_id VARCHAR(255), - dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) -); - --- Junction table for available qualities per title -CREATE TABLE media_available_quality ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - quality_type_id INT REFERENCES quality_type(id), - PRIMARY KEY (media_id, quality_type_id) -); - --- Junction table for content warnings -CREATE TABLE media_content_warning ( - media_id INT REFERENCES media(id) ON DELETE CASCADE, - content_warning_id INT REFERENCES content_warning(id), - PRIMARY KEY (media_id, content_warning_id) -); - --- Movie-specific data -CREATE TABLE movie ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL -); - --- Series-specific data -CREATE TABLE series ( - media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, - description TEXT -); - --- Seasons belonging to a series -CREATE TABLE season ( - id SERIAL PRIMARY KEY, - series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, - season_number INT NOT NULL -); - --- Episodes belonging to a season -CREATE TABLE episode ( - id SERIAL PRIMARY KEY, - season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - duration_seconds INT NOT NULL, - video_url VARCHAR(255) NOT NULL, - episode_number INT NOT NULL -); - --- Tracking viewing history and "continue watching" -CREATE TABLE viewing_progress ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - movie_id INT REFERENCES movie(media_id), - episode_id INT REFERENCES episode(id), - start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - duration_watched_seconds INT DEFAULT 0, - last_position_seconds INT DEFAULT 0, - is_finished BOOLEAN DEFAULT FALSE, - CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) -); - --- Personal watch lists for profiles -CREATE TABLE watch_list ( - id SERIAL PRIMARY KEY, - profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, - media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, - added_date DATE DEFAULT CURRENT_DATE -); - --- Internal staff management -CREATE TABLE employee ( - id SERIAL PRIMARY KEY, - role_id INT NOT NULL REFERENCES role(id), - name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL -); - --- Insert initial lookup data --- Age Ratings -INSERT INTO age_rating (label, min_age) VALUES - ('AL', 0), - ('6+', 6), - ('9+', 9), - ('12+', 12), - ('14+', 14), - ('16+', 16), - ('18+', 18); - --- Content Warnings -INSERT INTO content_warning (description) VALUES - ('Violence'), - ('Fear'), - ('Sex'), - ('Discrimination'), - ('Drug Abuse'), - ('Coarse Language'); - --- Playback Qualities -INSERT INTO quality_type (name) VALUES - ('SD'), - ('HD'), - ('UHD'); - --- Subscription Tiers -INSERT INTO subscription_tier (name, price, max_quality_id) VALUES - ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), - ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), - ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); - --- Internal Employee Roles -INSERT INTO role (name) VALUES - ('Junior'), - ('Mid-level'), - ('Senior'); - --- Invitation Statuses -INSERT INTO invitation_status (status) VALUES - ('Pending'), - ('Accepted'), - ('Expired'); - --- Sample test data for movies (with TMDB external IDs) -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), - (1, 'Inception', '2010-07-16', '27205', 'Movie'), - (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), - (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), - (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), - (3, 'The Matrix', '1999-03-31', '603', 'Movie'); - --- Insert corresponding movie records -INSERT INTO movie (media_id, duration_seconds, video_url) VALUES - (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), - (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), - (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), - (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), - (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), - (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); - --- Sample test data for a series -INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES - (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); - -INSERT INTO series (media_id, description) VALUES - (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); - --- Add seasons for Breaking Bad -INSERT INTO season (series_id, season_number) VALUES - (7, 1), - (7, 2); - --- Add sample episodes -INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES - (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), - (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), - (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); - --- Add available qualities for media -INSERT INTO media_available_quality (media_id, quality_type_id) VALUES - (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD - (2, 2), (2, 3), -- Inception: HD, UHD - (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD - (4, 2), (4, 3), -- Pulp Fiction: HD, UHD - (5, 1), (5, 2), -- Forrest Gump: SD, HD - (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD - (7, 2), (7, 3); -- Breaking Bad: HD, UHD - --- Add content warnings for media -INSERT INTO media_content_warning (media_id, content_warning_id) VALUES - (3, 1), (3, 2), -- Dark Knight: Violence, Fear - (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language - (6, 1), (6, 2), -- Matrix: Violence, Fear - (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql deleted file mode 100644 index 6233786..0000000 --- a/init-db/04_referral_logic.sql +++ /dev/null @@ -1,80 +0,0 @@ --- 04_referral_logic.sql --- Purpose: Creates stored procedures and triggers for the referral system logic. --- Execution Order: 2 (after schema creation) - --- Stored Procedure to apply referral discounts --- This procedure checks if both the inviter and invitee are eligible for a discount --- and updates their account status and the referral record accordingly. -CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) -LANGUAGE plpgsql -AS $$ -DECLARE - v_inviter_id INT; - v_invitee_id INT; - v_inviter_discount_used BOOLEAN; - v_invitee_discount_used BOOLEAN; -BEGIN - -- Retrieve inviter and invitee IDs from the referral record - SELECT inviter_account_id, invitee_account_id - INTO v_inviter_id, v_invitee_id - FROM referral - WHERE id = p_referral_id; - - -- Check current discount status for both accounts - SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; - SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; - - -- Apply discount only if neither account has used a discount yet - IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN - -- Update inviter account - UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; - - -- Update invitee account - UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; - - -- Mark the referral as having the discount applied - UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; - - RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; - ELSE - RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; - END IF; -END; -$$; - --- Trigger Function to handle subscription changes --- This function is called by the trigger to check if a subscription update warrants a referral discount. -CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() -RETURNS TRIGGER -LANGUAGE plpgsql -AS $$ -DECLARE - v_referral_id INT; -BEGIN - -- Check if the subscription is now Active and NOT a Trial - -- This handles both new subscriptions (INSERT) and updates (UPDATE) - IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN - - -- Find if the account owner was an invitee in a pending referral - SELECT id INTO v_referral_id - FROM referral - WHERE invitee_account_id = NEW.account_id - AND discount_applied = FALSE - LIMIT 1; - - -- If a qualifying referral exists, apply the discount - IF v_referral_id IS NOT NULL THEN - CALL apply_referral_discount(v_referral_id); - END IF; - END IF; - - RETURN NEW; -END; -$$; - --- Trigger on the subscription table --- Fires after a row is inserted or updated to check for referral completion -CREATE TRIGGER trg_apply_discount_on_paid_subscription -AFTER INSERT OR UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql deleted file mode 100644 index cd77cd2..0000000 --- a/init-db/05_employee_views.sql +++ /dev/null @@ -1,59 +0,0 @@ --- 05_employee_views.sql --- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). --- Execution Order: 3 (after schema and logic creation) - --- View for Junior Employees --- Junior employees can only see basic account information for support purposes. --- They do NOT have access to passwords, login attempts, or any financial/subscription data. -CREATE OR REPLACE VIEW view_junior_accounts AS -SELECT - id AS account_id, - email, - is_verified, - is_blocked -FROM - accounts; - --- View for Mid-level Employees --- Mid-level employees can see profile details and account status to help with content issues. --- They can see which profiles belong to which account and their age ratings. --- They still do NOT have access to financial data (subscriptions, prices) or passwords. -CREATE OR REPLACE VIEW view_midlevel_profiles AS -SELECT - p.id AS profile_id, - p.name AS profile_name, - a.id AS account_id, - a.email AS account_email, - ar.label AS age_rating_label, - a.is_verified, - a.is_blocked -FROM - profile p - JOIN - accounts a ON p.account_id = a.id - JOIN - age_rating ar ON p.age_rating_id = ar.id; - --- View for Senior Employees --- Senior employees have full visibility into accounts and their subscription status. --- This includes financial data like subscription tiers, prices, and discount usage. --- This view joins accounts with subscription details to show the complete customer picture. -CREATE OR REPLACE VIEW view_senior_full_access AS -SELECT - a.id AS account_id, - a.email, - a.is_verified, - a.is_blocked, - a.discount_used, - st.name AS subscription_tier_name, - st.price AS subscription_price, - s.is_active, - s.start_date, - s.end_date, - s.is_trial -FROM - accounts a - LEFT JOIN - subscription s ON a.id = s.account_id - LEFT JOIN - subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql deleted file mode 100644 index b791d43..0000000 --- a/init-db/06_additional_triggers.sql +++ /dev/null @@ -1,127 +0,0 @@ --- 06_additional_triggers.sql --- Purpose: Adds triggers for automatic watchlist management --- Execution Order: After 05_employee_views.sql - --- Function to automatically remove media from watchlist when finished -CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() -RETURNS TRIGGER AS $$ -DECLARE - v_series_id INT; - v_has_unfinished_episodes BOOLEAN; -BEGIN - -- Only proceed if the viewing progress is marked as finished - IF NEW.is_finished = TRUE THEN - - -- CASE 1: It's a Movie - IF NEW.movie_id IS NOT NULL THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.movie_id; - - -- CASE 2: It's an Episode - ELSIF NEW.episode_id IS NOT NULL THEN - -- Get the series_id for this episode - -- episode -> season -> series - SELECT s.series_id INTO v_series_id - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE e.id = NEW.episode_id; - - -- Check if there are any episodes in this series that are NOT finished for this profile - -- An episode is unfinished if: - -- 1. It exists in the series - -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) - - SELECT EXISTS ( - SELECT 1 - FROM episode e - JOIN season s ON e.season_id = s.id - WHERE s.series_id = v_series_id - AND NOT EXISTS ( - SELECT 1 - FROM viewing_progress vp - WHERE vp.episode_id = e.id - AND vp.profile_id = NEW.profile_id - AND vp.is_finished = TRUE - ) - ) INTO v_has_unfinished_episodes; - - -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist - IF v_has_unfinished_episodes = FALSE THEN - DELETE FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = v_series_id; - END IF; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; - -CREATE TRIGGER trg_auto_remove_from_watchlist -AFTER INSERT OR UPDATE ON viewing_progress -FOR EACH ROW -EXECUTE FUNCTION auto_remove_finished_from_watchlist(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Auto-update Subscription End Date --- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION update_subscription_end_date() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) - IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN - -- Only update end_date if it's not already set to today - -- This handles cases where end_date might be NULL or set to a future date - IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN - NEW.end_date := CURRENT_DATE; - END IF; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; - -CREATE TRIGGER trg_update_subscription_end_date -BEFORE UPDATE ON subscription -FOR EACH ROW -EXECUTE FUNCTION update_subscription_end_date(); - --- -------------------------------------------------------------------------------------- --- TRIGGER: Prevent Duplicate Watchlist Entries --- Purpose: Prevents duplicate entries in the watch_list table at the database level --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() -RETURNS TRIGGER AS $$ -BEGIN - -- Check if a record already EXISTS with the same profile_id AND media_id - IF EXISTS ( - SELECT 1 - FROM watch_list - WHERE profile_id = NEW.profile_id - AND media_id = NEW.media_id - ) THEN - RAISE EXCEPTION 'Media already in watchlist for this profile'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Create the trigger -DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; - -CREATE TRIGGER trg_prevent_duplicate_watchlist -BEFORE INSERT ON watch_list -FOR EACH ROW -EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql deleted file mode 100644 index a46e04e..0000000 --- a/init-db/07_stored_procedures.sql +++ /dev/null @@ -1,157 +0,0 @@ --- 07_stored_procedures.sql --- Purpose: Adds stored procedures and functions for analytics and reporting --- Execution Order: After 06_additional_triggers.sql - --- MAINTENANCE PROCEDURES --- These procedures should be run periodically for database maintenance --- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) - --- Function to get viewing statistics for a specific profile --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) -RETURNS TABLE ( - total_movies_watched BIGINT, - total_episodes_watched BIGINT, - total_watch_time_hours NUMERIC, - unique_content_watched BIGINT, - finished_content_count BIGINT -) AS $$ -BEGIN - RETURN QUERY - WITH stats AS ( - SELECT - -- Count distinct movies watched - COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, - - -- Count distinct episodes watched - COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, - - -- Sum duration watched in seconds and convert to hours - COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, - - -- Count finished content - COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id - ), - unique_media AS ( - -- Get unique media IDs (movies directly, series via episodes) - SELECT COUNT(DISTINCT media_id) AS unique_count - FROM ( - -- Movies - SELECT vp.movie_id AS media_id - FROM viewing_progress vp - WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL - - UNION - - -- Series (via episodes) - SELECT s.series_id AS media_id - FROM viewing_progress vp - JOIN episode e ON vp.episode_id = e.id - JOIN season s ON e.season_id = s.id - WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL - ) distinct_content - ) - SELECT - COALESCE(s.movies_count, 0)::BIGINT, - COALESCE(s.episodes_count, 0)::BIGINT, - ROUND(COALESCE(s.total_hours, 0), 2), - COALESCE(u.unique_count, 0)::BIGINT, - COALESCE(s.finished_count, 0)::BIGINT - FROM stats s, unique_media u; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- FUNCTION: Get Popular Content --- Purpose: Returns the most popular content based on viewing statistics --- -------------------------------------------------------------------------------------- - -DROP FUNCTION IF EXISTS get_popular_content(INT); -DROP FUNCTION IF EXISTS get_popular_content(BIGINT); - --- Updated to accept BIGINT to match Java Long type -CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) -RETURNS TABLE ( - media_id INT, - title VARCHAR, - media_type VARCHAR, - view_count BIGINT, - unique_viewers BIGINT, - completion_rate NUMERIC, - avg_watch_time_minutes NUMERIC -) AS $$ -BEGIN - RETURN QUERY - WITH content_stats AS ( - -- Movies - SELECT - m.media_id, - med.title, - 'Movie'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM movie m - JOIN media med ON m.media_id = med.id - JOIN viewing_progress vp ON m.media_id = vp.movie_id - GROUP BY m.media_id, med.title - - UNION ALL - - -- Series (aggregated by series) - SELECT - s.media_id, - med.title, - 'Series'::VARCHAR AS media_type, - COUNT(vp.id) AS total_views, - COUNT(DISTINCT vp.profile_id) AS distinct_viewers, - COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, - AVG(vp.duration_watched_seconds) AS avg_duration - FROM series s - JOIN media med ON s.media_id = med.id - JOIN season sea ON s.media_id = sea.series_id - JOIN episode e ON sea.id = e.season_id - JOIN viewing_progress vp ON e.id = vp.episode_id - GROUP BY s.media_id, med.title - ) - SELECT - cs.media_id, - cs.title, - cs.media_type, - cs.total_views::BIGINT, - cs.distinct_viewers::BIGINT, - ROUND( - CASE - WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 - ELSE 0 - END, 2 - ) AS completion_rate, - ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes - FROM content_stats cs - ORDER BY cs.total_views DESC - LIMIT p_limit; -END; -$$ LANGUAGE plpgsql; - --- -------------------------------------------------------------------------------------- --- PROCEDURE: Clean Expired Tokens --- Purpose: Removes expired verification tokens from the database --- -------------------------------------------------------------------------------------- - -CREATE OR REPLACE PROCEDURE clean_expired_tokens() -LANGUAGE plpgsql -AS $$ -DECLARE - deleted_count INT; -BEGIN - DELETE FROM verification_token - WHERE expiry_date < NOW(); - - GET DIAGNOSTICS deleted_count = ROW_COUNT; - - RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; -END; -$$; diff --git a/mvnw b/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 1d87747..0000000 --- a/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 4.0.1 - - - com.example - streamflix - 0.0.1-SNAPSHOT - streamflix - streamflix - - - - - - - - - - - - - - - 25 - - - - org.springframework.boot - spring-boot-starter-webmvc - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-starter-validation - - - - org.postgresql - postgresql - runtime - - - org.springframework.boot - spring-boot-starter-webmvc-test - test - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.security - spring-security-test - test - - - io.jsonwebtoken - jjwt-api - 0.13.0 - - - io.jsonwebtoken - jjwt-impl - 0.13.0 - - - io.jsonwebtoken - jjwt-jackson - 0.13.0 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.5 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 deleted file mode 100644 index a05412a..0000000 --- a/scripts/backup.ps1 +++ /dev/null @@ -1,56 +0,0 @@ -# StreamFlix Database Backup Script (Windows) -# This script creates a compressed backup of the PostgreSQL database using PowerShell - -$ErrorActionPreference = "Stop" - -# Configuration -$BackupDir = ".\backups" -$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Create backup directory if it doesn't exist -if (-not (Test-Path -Path $BackupDir)) { - New-Item -ItemType Directory -Path $BackupDir | Out-Null -} - -Write-Host "Starting database backup..." -Write-Host "Timestamp: $Timestamp" - -try { - # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping - Write-Host "Generating dump inside container..." - docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql - - if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } - - Write-Host "Copying backup to host..." - docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile - - # Clean up inside container - docker exec $ContainerName rm /tmp/temp_backup.sql - - # Compress the backup (Zip) - $ZipFile = "$BackupFile.zip" - Compress-Archive -Path $BackupFile -DestinationPath $ZipFile - Remove-Item $BackupFile - - $Size = (Get-Item $ZipFile).Length / 1MB - Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green - Write-Host "Backup file: $ZipFile" - Write-Host ("Backup size: {0:N2} MB" -f $Size) - - # Keep only the last 7 backups - Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | - Sort-Object CreationTime -Descending | - Select-Object -Skip 7 | - Remove-Item - - Write-Host "Cleaned up old backups (keeping last 7)" - -} catch { - Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red - exit 1 -} diff --git a/scripts/backup.sh b/scripts/backup.sh deleted file mode 100644 index 3420cd8..0000000 --- a/scripts/backup.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# StreamFlix Database Backup Script -# This script creates a compressed backup of the PostgreSQL database - -set -e # Exit on error - -# Configuration -BACKUP_DIR="./backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Create backup directory if it doesn't exist -mkdir -p "$BACKUP_DIR" - -echo "Starting database backup..." -echo "Timestamp: $TIMESTAMP" - -# Execute pg_dump inside the Docker container -if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then - # Compress the backup - gzip "$BACKUP_FILE" - - BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) - echo -e "${GREEN}✓ Backup completed successfully${NC}" - echo "Backup file: $BACKUP_FILE.gz" - echo "Backup size: $BACKUP_SIZE" - - # Keep only the last 7 backups (optional cleanup) - cd "$BACKUP_DIR" - ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm - echo "Cleaned up old backups (keeping last 7)" -else - echo -e "${RED}✗ Backup failed${NC}" - exit 1 -fi - -echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 deleted file mode 100644 index 49e2eb9..0000000 --- a/scripts/restore.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# StreamFlix Database Restore Script (Windows) -# This script restores the PostgreSQL database from a backup file - -$ErrorActionPreference = "Stop" - -# Configuration -$ContainerName = "streamflix-db" -$DbName = "streamflix" -$DbUser = "admin" - -# Check if backup file is provided -if ($args.Count -eq 0) { - Write-Host "[ERROR] No backup file specified" -ForegroundColor Red - Write-Host "Usage: .\scripts\restore.ps1 " - Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" - exit 1 -} - -$BackupFile = $args[0] - -# Check if file exists -if (-not (Test-Path -Path $BackupFile)) { - Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red - exit 1 -} - -Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow -Write-Host "Backup file: $BackupFile" -$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" -if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { - Write-Host "Restore cancelled" - exit 0 -} - -Write-Host "Starting database restore..." - -try { - $RestoreFile = $BackupFile - $TempDir = $null - - # Decompress if zipped - if ($BackupFile.EndsWith(".zip")) { - Write-Host "Decompressing backup file..." - $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" - New-Item -ItemType Directory -Path $TempDir | Out-Null - - Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force - - # Find the .sql file inside - $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 - if (-not $SqlFile) { - throw "No .sql file found in the archive" - } - $RestoreFile = $SqlFile.FullName - } - - # Stop API container - Write-Host "Stopping API container..." - docker-compose stop api - - # Copy file to container - Write-Host "Copying dump to container..." - docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" - - # Restore - Write-Host "Restoring database..." - # Using bash -c to handle redirection inside the container - docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" - - if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } - - # Cleanup container file - docker exec $ContainerName rm /tmp/restore.sql - - Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green - - # Restart API - Write-Host "Restarting API container..." - docker-compose start api - -} catch { - Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red - - # Try to restart API anyway - docker-compose start api - exit 1 -} finally { - # Cleanup temp dir if it exists - if ($TempDir -and (Test-Path $TempDir)) { - Remove-Item -Path $TempDir -Recurse -Force - } -} diff --git a/scripts/restore.sh b/scripts/restore.sh deleted file mode 100644 index 85f9c5b..0000000 --- a/scripts/restore.sh +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# StreamFlix Database Restore Script -# This script restores the PostgreSQL database from a backup file - -set -e # Exit on error - -# Configuration -CONTAINER_NAME="streamflix-db" -DB_NAME="streamflix" -DB_USER="admin" - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Check if backup file is provided -if [ -z "$1" ]; then - echo -e "${RED}Error: No backup file specified${NC}" - echo "Usage: ./restore.sh " - echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" - exit 1 -fi - -BACKUP_FILE="$1" - -# Check if file exists -if [ ! -f "$BACKUP_FILE" ]; then - echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" - exit 1 -fi - -echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" -echo "Backup file: $BACKUP_FILE" -read -p "Are you sure you want to continue? (yes/no): " -r -if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then - echo "Restore cancelled" - exit 0 -fi - -echo "Starting database restore..." - -# Decompress if gzipped -if [[ "$BACKUP_FILE" == *.gz ]]; then - echo "Decompressing backup file..." - TEMP_FILE="${BACKUP_FILE%.gz}" - gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" - RESTORE_FILE="$TEMP_FILE" -else - RESTORE_FILE="$BACKUP_FILE" -fi - -# Stop API container to prevent connections during restore -echo "Stopping API container..." -docker-compose stop api || true - -# Drop existing connections and restore -echo "Restoring database..." -if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then - echo -e "${GREEN}✓ Database restored successfully${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]]; then - rm "$RESTORE_FILE" - fi - - # Restart API container - echo "Restarting API container..." - docker-compose start api - - echo -e "${GREEN}Restore completed successfully at $(date)${NC}" -else - echo -e "${RED}✗ Restore failed${NC}" - - # Clean up temp file if created - if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then - rm "$RESTORE_FILE" - fi - - # Try to restart API anyway - docker-compose start api - exit 1 -fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java deleted file mode 100644 index 736387f..0000000 --- a/src/main/java/com/example/streamflix/StreamflixApplication.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix; - -import io.swagger.v3.oas.annotations.OpenAPIDefinition; -import io.swagger.v3.oas.annotations.info.Info; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) -public class StreamflixApplication { - - public static void main(String[] args) { - SpringApplication.run(StreamflixApplication.class, args); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java deleted file mode 100644 index 4bf5612..0000000 --- a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.config; - -import com.example.streamflix.service.AccountUserDetailsService; -import com.example.streamflix.service.JwtService; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private final JwtService jwtService; - private final AccountUserDetailsService userDetailsService; - - public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - final String authHeader = request.getHeader("Authorization"); - final String jwt; - final String email; - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - jwt = authHeader.substring(7); - email = jwtService.extractEmail(jwt); - - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); - - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); - } - } - filterChain.doFilter(request, response); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java deleted file mode 100644 index c364739..0000000 --- a/src/main/java/com/example/streamflix/config/ScheduledTasks.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.config; - -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.scheduling.annotation.Scheduled; - -@Configuration -@EnableScheduling -public class ScheduledTasks { - - private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); - private final EntityManager entityManager; - - public ScheduledTasks(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * Runs daily at 2 AM to clean up expired verification tokens - */ - @Scheduled(cron = "0 0 2 * * *") - @Transactional - public void cleanupExpiredTokens() { - try { - logger.info("Starting scheduled cleanup of expired verification tokens"); - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - logger.info("Completed scheduled cleanup of expired verification tokens"); - } catch (Exception e) { - logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); - } - } -} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java deleted file mode 100644 index 065bc3b..0000000 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.config; - -import io.netty.handler.codec.http.HttpMethod; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final JwtAuthenticationFilter jwtAuthenticationFilter; - - public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { - this.jwtAuthenticationFilter = jwtAuthenticationFilter; - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/analytics/admin/**").authenticated() - .requestMatchers("/api/v1/analytics/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() - .anyRequest().authenticated() - ); - return http.build(); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { - return authConfig.getAuthenticationManager(); - } -} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java deleted file mode 100644 index 0949ab4..0000000 --- a/src/main/java/com/example/streamflix/config/WebClientConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - return WebClient.builder().build(); - } -} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java deleted file mode 100644 index eb3f9bc..0000000 --- a/src/main/java/com/example/streamflix/controller/AnalyticsController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.example.streamflix.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import jakarta.persistence.Tuple; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@RestController -@RequestMapping("/api/v1/analytics") -@Tag(name = "Analytics", description = "Analytics and statistics endpoints") -public class AnalyticsController { - - private final EntityManager entityManager; - - public AnalyticsController(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Operation(summary = "Get popular content", - description = "Returns the most popular content based on view count and engagement") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "400", description = "Invalid limit parameter") - }) - @GetMapping("/popular") - public ResponseEntity getPopularContent( - @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit - ) { - if (limit < 1 || limit > 100) { - return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); - } - - try { - // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature - Query query = entityManager.createNativeQuery( - "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); - query.setParameter("limit", limit); - - List results = query.getResultList(); - - List> popularContent = results.stream() - .map(tuple -> { - Map item = new HashMap<>(); - item.put("mediaId", tuple.get("media_id")); - item.put("title", tuple.get("title")); - item.put("mediaType", tuple.get("media_type")); - item.put("viewCount", tuple.get("view_count")); - item.put("uniqueViewers", tuple.get("unique_viewers")); - item.put("completionRate", tuple.get("completion_rate")); - item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); - return item; - }) - .collect(Collectors.toList()); - - return ResponseEntity.ok(popularContent); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Error retrieving popular content: " + e.getMessage()); - } - } - - @Operation(summary = "Clean expired verification tokens", - description = "Admin endpoint to remove expired verification tokens from database") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "500", description = "Error during cleanup") - }) - @PostMapping("/admin/cleanup-tokens") - public ResponseEntity cleanupExpiredTokens() { - try { - entityManager.createNativeQuery("CALL clean_expired_tokens()") - .executeUpdate(); - - Map response = new HashMap<>(); - response.put("message", "Expired tokens cleaned successfully"); - response.put("timestamp", LocalDateTime.now().toString()); - - return ResponseEntity.ok(response); - } catch (Exception e) { - Map errorResponse = new HashMap<>(); - errorResponse.put("error", "Failed to clean expired tokens"); - errorResponse.put("details", e.getMessage()); - - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorResponse); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java deleted file mode 100644 index beeb19f..0000000 --- a/src/main/java/com/example/streamflix/controller/AuthController.java +++ /dev/null @@ -1,221 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.model.ForgotPasswordRequest; -import com.example.streamflix.model.LoginRequest; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.model.ResetPasswordRequest; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import com.example.streamflix.service.JwtService; -import com.example.streamflix.service.RegistrationService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -@RestController -@RequestMapping("/api/v1/auth") -@Tag(name = "Authentication", description = "Operations related to user authentication and registration") -public class AuthController { - - private final RegistrationService registrationService; - private final AuthenticationManager authenticationManager; - private final JwtService jwtService; - private final AccountRepository accountRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final PasswordEncoder passwordEncoder; - - public AuthController(RegistrationService registrationService, - AuthenticationManager authenticationManager, - JwtService jwtService, - AccountRepository accountRepository, - VerificationTokenRepository verificationTokenRepository, - PasswordEncoder passwordEncoder) { - this.registrationService = registrationService; - this.authenticationManager = authenticationManager; - this.jwtService = jwtService; - this.accountRepository = accountRepository; - this.verificationTokenRepository = verificationTokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Operation(summary = "Register a new user", description = "Register a new user with email and password") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "User registered successfully"), - @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") - }) - @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - try { - registrationService.register(request); - return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Login user", description = "Authenticate user and return JWT token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully authenticated", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), - @ApiResponse(responseCode = "403", description = "Account is blocked") - }) - @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - if (account.isBlocked()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); - } - } - - try { - Authentication authentication = authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - - if (authentication.isAuthenticated()) { - Account account = accountRepository.findByEmail(request.getEmail()) - .orElseThrow(() -> new RuntimeException("Account not found")); - - // Reset failed login attempts on successful login - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - String token = jwtService.generateToken(account.getEmail(), account.getId()); - - Map response = new HashMap<>(); - response.put("token", token); - response.put("email", account.getEmail()); - response.put("accountId", account.getId().toString()); - - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } - } catch (DisabledException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); - } catch (AuthenticationException e) { - if (accountOptional.isPresent()) { - Account account = accountOptional.get(); - int attempts = account.getFailedLoginAttempts() + 1; - account.setFailedLoginAttempts(attempts); - if (attempts >= 3) { - account.setBlocked(true); - } - accountRepository.save(account); - } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); - } - } - - @Operation(summary = "Verify email", description = "Verify user email using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token"), - @ApiResponse(responseCode = "404", description = "Token not found") - }) - @GetMapping("/verify") - public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(token); - - if (verificationToken == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setVerified(true); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Email verified successfully"); - } - - @Operation(summary = "Forgot password", description = "Initiate password reset process") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), - @ApiResponse(responseCode = "404", description = "Account not found") - }) - @PostMapping("/forgot-password") - public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { - Optional accountOptional = accountRepository.findByEmail(request.getEmail()); - if (accountOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); - } - - Account account = accountOptional.get(); - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(1), - TokenType.PASSWORD_RESET - ); - verificationTokenRepository.save(verificationToken); - - // In a real application, you would send an email with the token here - // For now, we just return a success message - - return ResponseEntity.ok("Password reset link has been sent"); - } - - @Operation(summary = "Reset password", description = "Reset user password using a token") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid or expired token") - }) - @PostMapping("/reset-password") - public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { - VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); - - if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); - } - - if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); - } - - Account account = verificationToken.getAccount(); - account.setPassword(passwordEncoder.encode(request.getNewPassword())); - account.setBlocked(false); - account.setFailedLoginAttempts(0); - accountRepository.save(account); - - verificationTokenRepository.delete(verificationToken); - - return ResponseEntity.ok("Password has been reset successfully"); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java deleted file mode 100644 index 59babe7..0000000 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.service.MediaService; -import com.example.streamflix.service.StreamingService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/media") -@Tag(name = "Media", description = "Operations related to media content and streaming") -public class MediaController { - - private final MediaService mediaService; - private final StreamingService streamingService; - - public MediaController(MediaService mediaService, StreamingService streamingService) { - this.mediaService = mediaService; - this.streamingService = streamingService; - } - - @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity> getAvailableMedia( - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); - } - - @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/profile/{profileId}") - public ResponseEntity getMediaDetails( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @PathVariable Long profileId) { - return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); - } - - @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Stream validation result", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), - @ApiResponse(responseCode = "404", description = "Media or profile not found") - }) - @GetMapping("/{mediaId}/validate-stream") - public ResponseEntity validateStream( - @Parameter(description = "ID of the media") @PathVariable Long mediaId, - @Parameter(description = "ID of the profile") @RequestParam Long profileId, - @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { - return ResponseEntity.ok( - streamingService.validateStream(profileId, mediaId, quality) - ); - } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ - - Object created = mediaService.createMedia(mediaDetailsRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java deleted file mode 100644 index a591357..0000000 --- a/src/main/java/com/example/streamflix/controller/ProfileController.java +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.model.CreatePreferenceRequest; -import com.example.streamflix.model.CreateProfileRequest; -import com.example.streamflix.model.UpdateProfileRequest; -import com.example.streamflix.service.ProfileService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/profiles") -@Tag(name = "Profiles", description = "Operations related to user profiles") -public class ProfileController { - - private final ProfileService profileService; - - public ProfileController(ProfileService profileService) { - this.profileService = profileService; - } - - @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) - }) - @GetMapping - public ResponseEntity> getMyProfiles() { - List profiles = profileService.getMyProfiles(); - return ResponseEntity.ok(profiles); - } - - @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}") - public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Profile profile = profileService.getProfileById(profileId); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Profile created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping - public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { - try { - Profile profile = profileService.createProfile( - request.getName(), - request.getAgeRatingId(), - request.getImageUrl(), - request.getBirthDate() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(profile); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Update a profile", description = "Update an existing profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile updated successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PutMapping("/{profileId}") - public ResponseEntity updateProfile( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody UpdateProfileRequest request) { - try { - Profile profile = profileService.updateProfile( - profileId, - request.getName(), - request.getAgeRatingId(), - request.getImageUrl() - ); - return ResponseEntity.ok(profile); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @DeleteMapping("/{profileId}") - public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - profileService.deleteProfile(profileId); - return ResponseEntity.ok("Profile deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Add a preference", description = "Add a preference to a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Preference added successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @PostMapping("/{profileId}/preferences") - public ResponseEntity addPreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Valid @RequestBody CreatePreferenceRequest request) { - try { - Preference preference = profileService.addPreference( - profileId, - request.getPreferenceType(), - request.getValue() - ); - return ResponseEntity.status(HttpStatus.CREATED).body(preference); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } - - @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/{profileId}/preferences") - public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List preferences = profileService.getProfilePreferences(profileId); - return ResponseEntity.ok(preferences); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); - } - } - - @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), - @ApiResponse(responseCode = "403", description = "Access denied to the profile"), - @ApiResponse(responseCode = "400", description = "Invalid input data") - }) - @DeleteMapping("/{profileId}/preferences/{preferenceId}") - public ResponseEntity deletePreference( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { - try { - profileService.deletePreference(profileId, preferenceId); - return ResponseEntity.ok("Preference deleted successfully"); - } catch (SecurityException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java deleted file mode 100644 index 8013175..0000000 --- a/src/main/java/com/example/streamflix/controller/ReferralController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Referral; -import com.example.streamflix.model.AcceptInvitationRequest; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.service.ReferralService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/referrals") -@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") -public class ReferralController { - - private final ReferralService referralService; - private final SecurityUtils securityUtils; - - public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { - this.referralService = referralService; - this.securityUtils = securityUtils; - } - - @PostMapping("/create-invitation") - @Operation(summary = "Create an invitation link to invite others") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Invitation created successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), - @ApiResponse(responseCode = "400", description = "User does not have an active subscription") - }) - public ResponseEntity createInvitation() { - InvitationResponse response = referralService.createInvitation(); - return new ResponseEntity<>(response, HttpStatus.CREATED); - } - - @PostMapping("/accept-invitation") - @Operation(summary = "Accept an invitation using invite code") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), - @ApiResponse(responseCode = "404", description = "Referral not found") - }) - public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); - return ResponseEntity.ok(referral); - } - - @GetMapping("/my-invitations") - @Operation(summary = "Get all invitations I have sent") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) - }) - public ResponseEntity> getMyInvitations() { - return ResponseEntity.ok(referralService.getMyInvitations()); - } - - @GetMapping("/my-referral-status") - @Operation(summary = "Check if I was invited by someone") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), - @ApiResponse(responseCode = "404", description = "Referral status not found") - }) - public ResponseEntity getMyReferralStatus() { - return referralService.getMyReferralStatus() - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java deleted file mode 100644 index 6ba5a35..0000000 --- a/src/main/java/com/example/streamflix/controller/SubscriptionController.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.CreateTrialRequest; -import com.example.streamflix.model.UpgradeSubscriptionRequest; -import com.example.streamflix.service.SubscriptionService; -import com.example.streamflix.util.SecurityUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.Optional; - -@RestController -@RequestMapping("/api/v1/subscriptions") -@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") -public class SubscriptionController { - - private final SubscriptionService subscriptionService; - private final SecurityUtils securityUtils; - - public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { - this.subscriptionService = subscriptionService; - this.securityUtils = securityUtils; - } - - @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "400", description = "Invalid input") - }) - @PostMapping("/trial") - public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { - try { - Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); - return new ResponseEntity<>(subscription, HttpStatus.CREATED); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/upgrade") - public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { - try { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); - return ResponseEntity.ok(subscription); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/my-subscription") - public ResponseEntity getMyActiveSubscription() { - Optional subscription = subscriptionService.getMyActiveSubscription(); - return subscription.map(ResponseEntity::ok) - .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); - } - - @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/cancel") - public ResponseEntity cancelSubscription() { - try { - subscriptionService.cancelSubscription(); - return ResponseEntity.ok("Subscription cancelled successfully"); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java deleted file mode 100644 index 2f70dc6..0000000 --- a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.StartWatchingRequest; -import com.example.streamflix.model.UpdateProgressRequest; -import com.example.streamflix.service.ViewingProgressService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/v1/viewing-progress") -@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") -public class ViewingProgressController { - - private final ViewingProgressService viewingProgressService; - - public ViewingProgressController(ViewingProgressService viewingProgressService) { - this.viewingProgressService = viewingProgressService; - } - - @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully started watching", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping("/start") - public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); - return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully updated progress", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}") - public ResponseEntity updateProgress( - @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, - @Valid @RequestBody UpdateProgressRequest request) { - try { - ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); - return ResponseEntity.ok(viewingProgress); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PutMapping("/{progressId}/finish") - public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { - try { - viewingProgressService.markAsFinished(progressId); - return ResponseEntity.ok("Marked as finished"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); - return ResponseEntity.ok(viewingHistory); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Profile not found") - }) - @GetMapping("/profile/{profileId}/stats") - public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - Map stats = viewingProgressService.getProfileViewingStats(profileId); - return ResponseEntity.ok(stats); - } catch (SecurityException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java deleted file mode 100644 index 642b8b9..0000000 --- a/src/main/java/com/example/streamflix/controller/WatchListController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.example.streamflix.controller; - -import com.example.streamflix.entity.WatchList; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.model.AddToWatchListRequest; -import com.example.streamflix.service.WatchListService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@RestController -@RequestMapping("/api/v1/watchlist") -@Tag(name = "Watch List", description = "Operations for managing user watch lists") -public class WatchListController { - - private final WatchListService watchListService; - - public WatchListController(WatchListService watchListService) { - this.watchListService = watchListService; - } - - @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Successfully added to watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "400", description = "Invalid input"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @PostMapping - public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { - try { - WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); - return new ResponseEntity<>(watchList, HttpStatus.CREATED); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } catch (IllegalArgumentException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); - } - } - - @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @DeleteMapping("/profile/{profileId}/media/{mediaId}") - public ResponseEntity removeFromWatchList( - @Parameter(description = "ID of the profile") @PathVariable Long profileId, - @Parameter(description = "ID of the media") @PathVariable Long mediaId) { - try { - watchListService.removeFromWatchList(profileId, mediaId); - return ResponseEntity.ok("Removed from watch list"); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } - - @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", - content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), - @ApiResponse(responseCode = "403", description = "Forbidden"), - @ApiResponse(responseCode = "404", description = "Not Found") - }) - @GetMapping("/profile/{profileId}") - public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { - try { - List watchList = watchListService.getMyWatchList(profileId); - return ResponseEntity.ok(watchList); - } catch (AccessDeniedException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); - } catch (NotFoundException e) { - return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java deleted file mode 100644 index 800f360..0000000 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "accounts") -@Schema(description = "Account entity representing a user account") -public class Account { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the account", example = "1") - private Long id; - - @Column(unique = true, nullable = false) - @Schema(description = "Email address of the account", example = "user@example.com") - private String email; - - @JsonIgnore - @Column(nullable = false, name = "password_hash") - @Schema(description = "Hashed password of the account") - private String password; - - @JsonIgnore - @Column(name = "failed_login_attempts") - @Schema(description = "Number of failed login attempts", example = "0") - private int failedLoginAttempts = 0; - - @JsonIgnore - @Column(name = "is_blocked") - @Schema(description = "Indicates if the account is blocked", example = "false") - private boolean isBlocked = false; - - @JsonIgnore - @Column(name = "is_verified") - @Schema(description = "Indicates if the account is verified", example = "false") - private boolean isVerified = false; - - @JsonIgnore - @Column(name = "discount_used") - @Schema(description = "Indicates if the discount has been used", example = "false") - private boolean discountUsed = false; - - public Account() { - } - - public Account(String email, String password) { - this.email = email; - this.password = password; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public int getFailedLoginAttempts() { - return failedLoginAttempts; - } - - public void setFailedLoginAttempts(int failedLoginAttempts) { - this.failedLoginAttempts = failedLoginAttempts; - } - - public boolean isBlocked() { - return isBlocked; - } - - public void setBlocked(boolean blocked) { - isBlocked = blocked; - } - - public boolean isVerified() { - return isVerified; - } - - public void setVerified(boolean verified) { - isVerified = verified; - } - - public boolean isDiscountUsed() { - return discountUsed; - } - - public void setDiscountUsed(boolean discountUsed) { - this.discountUsed = discountUsed; - } -} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java deleted file mode 100644 index 5e35a42..0000000 --- a/src/main/java/com/example/streamflix/entity/AgeRating.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "age_rating") -@Schema(description = "AgeRating entity representing age restrictions") -public class AgeRating { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the age rating", example = "1") - private Long id; - - @Column(nullable = false, length = 20) - @Schema(description = "Label of the age rating", example = "PG-13") - private String label; - - @Column(name = "min_age", nullable = false) - @Schema(description = "Minimum age required for this rating", example = "13") - private int minAge; - - public AgeRating() { - } - - public AgeRating(String label, int minAge) { - this.label = label; - this.minAge = minAge; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public int getMinAge() { - return minAge; - } - - public void setMinAge(int minAge) { - this.minAge = minAge; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java deleted file mode 100644 index 34af7f6..0000000 --- a/src/main/java/com/example/streamflix/entity/ContentWarning.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.streamflix.entity; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "content_warning") -@Schema(description = "ContentWarning entity representing content warnings for media") -public class ContentWarning { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the content warning", example = "1") - private Long id; - - @Column(nullable = false, length = 50) - @Schema(description = "Description of the content warning", example = "Violence") - private String description; - - public ContentWarning() { - } - - public ContentWarning(String description) { - this.description = description; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java deleted file mode 100644 index 095d9ec..0000000 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "employee") -public class Employee { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "role_id", nullable = false) - private Role role; - - @Column(nullable = false, length = 100) - private String name; - - @Column(unique = true, nullable = false) - private String email; - - public Employee() { - } - - public Employee(Role role, String name, String email) { - this.role = role; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Role getRole() { - return role; - } - - public void setRole(Role role) { - this.role = role; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java deleted file mode 100644 index 4ec6fa6..0000000 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "episode") -public class Episode extends Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "season_id", nullable = false) - @JsonBackReference - private Season season; - - private String title; - - @Column(name = "duration_seconds") - private int durationSeconds; - - @Column(name = "video_url") - private String videoUrl; - - @Column(name = "episode_number") - private int episodeNumber; - - @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Season getSeason() { - return season; - } - - public void setSeason(Season season) { - this.season = season; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getDurationSeconds() { - return durationSeconds; - } - - public void setDurationSeconds(int durationSeconds) { - this.durationSeconds = durationSeconds; - } - - public String getVideoUrl() { - return videoUrl; - } - - public void setVideoUrl(String videoUrl) { - this.videoUrl = videoUrl; - } - - public int getEpisodeNumber() { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) { - this.episodeNumber = episodeNumber; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java deleted file mode 100644 index e4ff5ae..0000000 --- a/src/main/java/com/example/streamflix/entity/InvitationStatus.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "invitation_status") -public class InvitationStatus { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String status; - - public InvitationStatus() { - } - - public InvitationStatus(String status) { - this.status = status; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getStatus() { - return status; - } - - public void setStatus(String status) { - this.status = status; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java deleted file mode 100644 index b7c9085..0000000 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Inheritance(strategy = InheritanceType.JOINED) -@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) -@Table(name = "media") -@Schema(description = "Abstract Media entity representing common media properties") -public abstract class Media { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the media", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the media") - private AgeRating ageRating; - - @Column(nullable = false) - @Schema(description = "Title of the media", example = "Inception") - private String title; - - @Column(name = "release_date") - @Schema(description = "Release date of the media", example = "2010-07-16") - private LocalDate releaseDate; - - @Column(name = "external_id") - @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") - private String externalId; - - @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchLists; - - public Media() { - } - - public Media(AgeRating ageRating, String title, LocalDate releaseDate) { - this.ageRating = ageRating; - this.title = title; - this.releaseDate = releaseDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public List getWatchLists() { - return watchLists; - } - - public void setWatchLists(List watchLists) { - this.watchLists = watchLists; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java deleted file mode 100644 index 1f32d6f..0000000 --- a/src/main/java/com/example/streamflix/entity/Movie.java +++ /dev/null @@ -1,46 +0,0 @@ -// Movie.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "movie") -@DiscriminatorValue("Movie") -@PrimaryKeyJoinColumn(name = "media_id") -public class Movie extends Media { - - @Column(name = "duration_seconds", nullable = false) - private Integer durationSeconds; - - @Column(name = "video_url", nullable = false) - private String videoUrl; - - @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - public Movie() {} - - public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - Integer durationSeconds, String videoUrl) { - super(ageRating, title, releaseDate); - this.durationSeconds = durationSeconds; - this.videoUrl = videoUrl; - } - - public Integer getDurationSeconds() { return durationSeconds; } - public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } - - public String getVideoUrl() { return videoUrl; } - public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java deleted file mode 100644 index cd114a4..0000000 --- a/src/main/java/com/example/streamflix/entity/Preference.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; - -@Entity -@Table(name = "preference") -@Schema(description = "Preference entity representing user preferences") -public class Preference { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the preference", example = "1") - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "profile_id", nullable = false) - @Schema(description = "Profile associated with the preference") - private Profile profile; - - @Enumerated(EnumType.STRING) - @Column(name = "preference_type", nullable = false) - @Schema(description = "Type of the preference", example = "GENRE") - private PreferenceType preferenceType; - - @Column(nullable = false, length = 100) - @Schema(description = "Value of the preference", example = "Action") - private String value; - - public Preference() { - } - - public Preference(Profile profile, PreferenceType preferenceType, String value) { - this.profile = profile; - this.preferenceType = preferenceType; - this.value = value; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java deleted file mode 100644 index 0014d08..0000000 --- a/src/main/java/com/example/streamflix/entity/Profile.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDate; -import java.util.List; - -@Entity -@Table(name = "profile") -@Schema(description = "Profile entity representing a user profile") -public class Profile { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the profile", example = "1") - private Long id; - - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "account_id", nullable = false) - @Schema(description = "Account associated with the profile") - private Account account; - - @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "age_rating_id", nullable = false) - @Schema(description = "Age rating associated with the profile") - private AgeRating ageRating; - - @Column(nullable = false, length = 50) - @Schema(description = "Name of the profile", example = "John Doe") - private String name; - - @Column(name = "image_url") - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Column(name = "birth_date") - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List viewingProgresses; - - @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List watchList; - - public Profile() { - } - - public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { - this.account = account; - this.ageRating = ageRating; - this.name = name; - this.imageUrl = imageUrl; - this.birthDate = birthDate; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public AgeRating getAgeRating() { - return ageRating; - } - - public void setAgeRating(AgeRating ageRating) { - this.ageRating = ageRating; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } - - public List getViewingProgresses() { - return viewingProgresses; - } - - public void setViewingProgresses(List viewingProgresses) { - this.viewingProgresses = viewingProgresses; - } - - public List getWatchList() { - return watchList; - } - - public void setWatchList(List watchList) { - this.watchList = watchList; - } -} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java deleted file mode 100644 index f40d20f..0000000 --- a/src/main/java/com/example/streamflix/entity/QualityType.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "quality_type") -public class QualityType { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java deleted file mode 100644 index e7ae865..0000000 --- a/src/main/java/com/example/streamflix/entity/Referral.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "referral") -public class Referral { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "inviter_account_id", nullable = false) - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviterAccount; - - @ManyToOne - @JoinColumn(name = "invitee_account_id") - @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) - private Account inviteeAccount; - - @ManyToOne - @JoinColumn(name = "status_id") - private InvitationStatus status; - - @Column(name = "invite_date") - private LocalDate inviteDate; - - @Column(name = "discount_applied") - private Boolean discountApplied = false; - - public Referral() { - } - - public Referral(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - @PrePersist - protected void onCreate() { - if (inviteDate == null) { - inviteDate = LocalDate.now(); - } - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getInviterAccount() { - return inviterAccount; - } - - public void setInviterAccount(Account inviterAccount) { - this.inviterAccount = inviterAccount; - } - - public Account getInviteeAccount() { - return inviteeAccount; - } - - public void setInviteeAccount(Account inviteeAccount) { - this.inviteeAccount = inviteeAccount; - } - - public InvitationStatus getStatus() { - return status; - } - - public void setStatus(InvitationStatus status) { - this.status = status; - } - - public LocalDate getInviteDate() { - return inviteDate; - } - - public void setInviteDate(LocalDate inviteDate) { - this.inviteDate = inviteDate; - } - - public Boolean getDiscountApplied() { - return discountApplied; - } - - public void setDiscountApplied(Boolean discountApplied) { - this.discountApplied = discountApplied; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java deleted file mode 100644 index 501b996..0000000 --- a/src/main/java/com/example/streamflix/entity/Role.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "role") -public class Role { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, unique = true) - private String name; - - public Role() { - } - - public Role(String name) { - this.name = name; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java deleted file mode 100644 index a726051..0000000 --- a/src/main/java/com/example/streamflix/entity/Season.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.List; - -@Entity -@Table(name = "season") -public class Season { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "series_id", nullable = false) - @JsonBackReference - private Series series; - - @Column(name = "season_number") - private int seasonNumber; - - @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) - @JsonManagedReference - private List episodes; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Series getSeries() { - return series; - } - - public void setSeries(Series series) { - this.series = series; - } - - public int getSeasonNumber() { - return seasonNumber; - } - - public void setSeasonNumber(int seasonNumber) { - this.seasonNumber = seasonNumber; - } - - public List getEpisodes() { - return episodes; - } - - public void setEpisodes(List episodes) { - this.episodes = episodes; - } -} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java deleted file mode 100644 index a2a05c6..0000000 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ /dev/null @@ -1,35 +0,0 @@ -// Series.java -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonManagedReference; -import jakarta.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "series") -@DiscriminatorValue("Series") -@PrimaryKeyJoinColumn(name = "media_id") -public class Series extends Media { - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonManagedReference - private List seasons = new ArrayList<>(); - - public Series() {} - - public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, - String description) { - super(ageRating, title, releaseDate); - this.description = description; - } - - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java deleted file mode 100644 index ad20509..0000000 --- a/src/main/java/com/example/streamflix/entity/Subscription.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "subscription") -public class Subscription { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "account_id", nullable = false) - private Account account; - - @ManyToOne - @JoinColumn(name = "tier_id", nullable = false) - private SubscriptionTier tier; - - @Column(name = "start_date", nullable = false) - private LocalDate startDate; - - @Column(name = "end_date") - private LocalDate endDate; - - @Column(name = "is_trial") - private Boolean isTrial; - - @Column(name = "is_active") - private Boolean isActive; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public SubscriptionTier getTier() { - return tier; - } - - public void setTier(SubscriptionTier tier) { - this.tier = tier; - } - - public LocalDate getStartDate() { - return startDate; - } - - public void setStartDate(LocalDate startDate) { - this.startDate = startDate; - } - - public LocalDate getEndDate() { - return endDate; - } - - public void setEndDate(LocalDate endDate) { - this.endDate = endDate; - } - - public Boolean getTrial() { - return isTrial; - } - - public void setTrial(Boolean trial) { - isTrial = trial; - } - - public Boolean getActive() { - return isActive; - } - - public void setActive(Boolean active) { - isActive = active; - } -} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java deleted file mode 100644 index e1e89b6..0000000 --- a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.example.streamflix.entity; - -import jakarta.persistence.*; - -@Entity -@Table(name = "subscription_tier") -public class SubscriptionTier { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private java.math.BigDecimal price; - - @ManyToOne - @JoinColumn(name = "max_quality_id") - private QualityType maxQuality; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public java.math.BigDecimal getPrice() { - return price; - } - - public void setPrice(java.math.BigDecimal price) { - this.price = price; - } - - public QualityType getMaxQuality() { - return maxQuality; - } - - public void setMaxQuality(QualityType maxQuality) { - this.maxQuality = maxQuality; - } -} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java deleted file mode 100644 index a2dcb57..0000000 --- a/src/main/java/com/example/streamflix/entity/VerificationToken.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.streamflix.entity; - -import com.example.streamflix.enums.TokenType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@Entity -@Table(name = "verification_token") -@Schema(description = "VerificationToken entity for account verification") -public class VerificationToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Schema(description = "Unique identifier of the token", example = "1") - private Long id; - - @Schema(description = "The verification token string", example = "abc123xyz") - private String token; - - @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) - @JoinColumn(nullable = false, name = "account_id") - @Schema(description = "Account associated with the token") - private Account account; - - @Column(name = "expiry_date") - @Schema(description = "Expiration date and time of the token") - private LocalDateTime expiryDate; - - @Enumerated(EnumType.STRING) - @Column(name = "token_type") - @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") - private TokenType type; - - public VerificationToken() { - } - - public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { - this.token = token; - this.account = account; - this.expiryDate = expiryDate; - this.type = type; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public Account getAccount() { - return account; - } - - public void setAccount(Account account) { - this.account = account; - } - - public LocalDateTime getExpiryDate() { - return expiryDate; - } - - public void setExpiryDate(LocalDateTime expiryDate) { - this.expiryDate = expiryDate; - } - - public TokenType getType() { - return type; - } - - public void setType(TokenType type) { - this.type = type; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java deleted file mode 100644 index 242ad1e..0000000 --- a/src/main/java/com/example/streamflix/entity/ViewingProgress.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import org.hibernate.annotations.Check; -import java.time.LocalDateTime; - -@Entity -@Table(name = "viewing_progress") -@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") -public class ViewingProgress { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id") - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "movie_id", nullable = true) - @JsonBackReference - private Movie movie; - - @ManyToOne - @JoinColumn(name = "episode_id", nullable = true) - @JsonBackReference - private Episode episode; - - @Column(name = "start_time") - private LocalDateTime startTime; - - @Column(name = "duration_watched_seconds") - private Integer durationWatchedSeconds; - - @Column(name = "last_position_seconds") - private Integer lastPositionSeconds; - - @Column(name = "is_finished") - private Boolean isFinished; - - public ViewingProgress() { - } - - public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { - this.profile = profile; - this.movie = movie; - this.episode = episode; - this.startTime = startTime; - this.durationWatchedSeconds = durationWatchedSeconds; - this.lastPositionSeconds = lastPositionSeconds; - this.isFinished = isFinished; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Movie getMovie() { - return movie; - } - - public void setMovie(Movie movie) { - this.movie = movie; - } - - public Episode getEpisode() { - return episode; - } - - public void setEpisode(Episode episode) { - this.episode = episode; - } - - public LocalDateTime getStartTime() { - return startTime; - } - - public void setStartTime(LocalDateTime startTime) { - this.startTime = startTime; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Boolean getIsFinished() { - return isFinished; - } - - public void setIsFinished(Boolean isFinished) { - this.isFinished = isFinished; - } - - @PrePersist - protected void onCreate() { - if (startTime == null) { - startTime = LocalDateTime.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java deleted file mode 100644 index f8c3f00..0000000 --- a/src/main/java/com/example/streamflix/entity/WatchList.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.streamflix.entity; - -import com.fasterxml.jackson.annotation.JsonBackReference; -import jakarta.persistence.*; -import java.time.LocalDate; - -@Entity -@Table(name = "watch_list") -public class WatchList { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "profile_id", nullable = false) - @JsonBackReference - private Profile profile; - - @ManyToOne - @JoinColumn(name = "media_id", nullable = false) - @JsonBackReference - private Media media; - - @Column(name = "added_date") - private LocalDate addedDate; - - public WatchList() { - } - - public WatchList(Profile profile, Media media) { - this.profile = profile; - this.media = media; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Profile getProfile() { - return profile; - } - - public void setProfile(Profile profile) { - this.profile = profile; - } - - public Media getMedia() { - return media; - } - - public void setMedia(Media media) { - this.media = media; - } - - public LocalDate getAddedDate() { - return addedDate; - } - - public void setAddedDate(LocalDate addedDate) { - this.addedDate = addedDate; - } - - @PrePersist - protected void onCreate() { - if (addedDate == null) { - addedDate = LocalDate.now(); - } - } -} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java deleted file mode 100644 index ba21818..0000000 --- a/src/main/java/com/example/streamflix/enums/MediaQuality.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum MediaQuality { - SD, - HD, - UHD -} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java deleted file mode 100644 index 99b3c5e..0000000 --- a/src/main/java/com/example/streamflix/enums/PreferenceType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.streamflix.enums; - -public enum PreferenceType { - GENRE, - CONTENT_FILTER, - MIN_AGE -} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java deleted file mode 100644 index 0e50073..0000000 --- a/src/main/java/com/example/streamflix/enums/TokenType.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.streamflix.enums; - -public enum TokenType { - EMAIL_VERIFICATION, - PASSWORD_RESET -} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java deleted file mode 100644 index a651cb0..0000000 --- a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example.streamflix.exception; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import com.example.streamflix.model.ErrorResponse; -import java.util.Arrays; - - -@ControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.FORBIDDEN.value(), - "Forbidden", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.NOT_FOUND.value(), - "Not Found", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); - } - - @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { - String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); - - if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { - message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Bad Request", - message, - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolation( - DataIntegrityViolationException ex, WebRequest request) { - - String message = ex.getMessage(); - if (message != null && message.contains("Media already in watchlist")) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.CONFLICT.value(), - "Conflict", - "Media already in watchlist for this profile", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); - } - - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.BAD_REQUEST.value(), - "Data Integrity Violation", - "Database constraint violation occurred", - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { - ErrorResponse errorResponse = new ErrorResponse( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "Internal Server Error", - ex.getMessage(), - request.getDescription(false).replace("uri=", "") - ); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java deleted file mode 100644 index 53cbcec..0000000 --- a/src/main/java/com/example/streamflix/exception/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.exception; - -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } - - public NotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java deleted file mode 100644 index c6efd38..0000000 --- a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotBlank; - -public class AcceptInvitationRequest { - @NotBlank(message = "Invite code is required") - private String inviteCode; - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java deleted file mode 100644 index c2816b9..0000000 --- a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class AddToWatchListRequest { - - @NotNull - private Long profileId; - - @NotNull - private Long mediaId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMediaId() { - return mediaId; - } - - public void setMediaId(Long mediaId) { - this.mediaId = mediaId; - } -} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java deleted file mode 100644 index b18fe7e..0000000 --- a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.PreferenceType; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -@Schema(description = "Request object for creating a new preference") -public class CreatePreferenceRequest { - - @Schema(description = "Type of the preference", example = "GENRE") - @NotNull(message = "Preference type is required") - private PreferenceType preferenceType; - - @Schema(description = "Value of the preference", example = "Action") - @NotBlank(message = "Value is required") - private String value; - - public PreferenceType getPreferenceType() { - return preferenceType; - } - - public void setPreferenceType(PreferenceType preferenceType) { - this.preferenceType = preferenceType; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java deleted file mode 100644 index 56eac15..0000000 --- a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - -@Schema(description = "Request object for creating a new profile") -public class CreateProfileRequest { - - @Schema(description = "Name of the profile", example = "John Doe") - @NotBlank(message = "Name is required") - private String name; - - @Schema(description = "ID of the age rating for the profile", example = "1") - @NotNull(message = "Age rating ID is required") - private Long ageRatingId; - - @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") - private String imageUrl; - - @Schema(description = "Birth date of the profile owner", example = "1990-01-01") - private LocalDate birthDate; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } - - public LocalDate getBirthDate() { - return birthDate; - } - - public void setBirthDate(LocalDate birthDate) { - this.birthDate = birthDate; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java deleted file mode 100644 index 1106234..0000000 --- a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class CreateTrialRequest { - - @NotNull - private Long accountId; - - @NotNull - private Long tierId; - - public Long getAccountId() { - return accountId; - } - - public void setAccountId(Long accountId) { - this.accountId = accountId; - } - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java deleted file mode 100644 index 2670e3f..0000000 --- a/src/main/java/com/example/streamflix/model/ErrorResponse.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonInclude; -import java.time.LocalDateTime; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ErrorResponse { - - private LocalDateTime timestamp; - private int status; - private String error; - private String message; - private String path; - private List validationErrors; - - public ErrorResponse() { - this.timestamp = LocalDateTime.now(); - } - - public ErrorResponse(int status, String error, String message, String path) { - this(); - this.status = status; - this.error = error; - this.message = message; - this.path = path; - } - - // Getters and Setters - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public String getError() { - return error; - } - - public void setError(String error) { - this.error = error; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public List getValidationErrors() { - return validationErrors; - } - - public void setValidationErrors(List validationErrors) { - this.validationErrors = validationErrors; - } - - public static class ValidationError { - private String field; - private String message; - - public ValidationError(String field, String message) { - this.field = field; - this.message = message; - } - - public String getField() { - return field; - } - - public void setField(String field) { - this.field = field; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java deleted file mode 100644 index 8d87484..0000000 --- a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for initiating password reset") -public class ForgotPasswordRequest { - - @NotBlank(message = "Email is required") - @Email(message = "Invalid email format") - @Schema(description = "User's email address", example = "user@example.com") - private String email; - - public ForgotPasswordRequest() { - } - - public ForgotPasswordRequest(String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java deleted file mode 100644 index bdd3d0a..0000000 --- a/src/main/java/com/example/streamflix/model/InvitationResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.entity.Referral; - -public class InvitationResponse { - private Referral referral; - private String inviteCode; - - public InvitationResponse(Referral referral, String inviteCode) { - this.referral = referral; - this.inviteCode = inviteCode; - } - - public Referral getReferral() { - return referral; - } - - public void setReferral(Referral referral) { - this.referral = referral; - } - - public String getInviteCode() { - return inviteCode; - } - - public void setInviteCode(String inviteCode) { - this.inviteCode = inviteCode; - } -} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java deleted file mode 100644 index 9103cc4..0000000 --- a/src/main/java/com/example/streamflix/model/LoginRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user login") -public class LoginRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java deleted file mode 100644 index 4496e93..0000000 --- a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.example.streamflix.model; - -import java.time.LocalDate; - -public class MediaDetailsDto { - private Long id; - private String title; - private LocalDate releaseDate; - private String ageRating; - private String externalId; - private String mediaType; - private Long seriesId; - private int durationInSecond; - - // TMDB enriched data - private String posterUrl; - private String backdropUrl; - private String overview; - private Double externalRating; - - // Getters and setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getReleaseDate() { - return releaseDate; - } - - public void setReleaseDate(LocalDate releaseDate) { - this.releaseDate = releaseDate; - } - - public String getAgeRating() { - return ageRating; - } - - public void setAgeRating(String ageRating) { - this.ageRating = ageRating; - } - - public String getExternalId() { - return externalId; - } - - public void setExternalId(String externalId) { - this.externalId = externalId; - } - - public String getPosterUrl() { - return posterUrl; - } - - public void setPosterUrl(String posterUrl) { - this.posterUrl = posterUrl; - } - - public String getBackdropUrl() { - return backdropUrl; - } - - public void setBackdropUrl(String backdropUrl) { - this.backdropUrl = backdropUrl; - } - - public String getOverview() { - return overview; - } - - public void setOverview(String overview) { - this.overview = overview; - } - - public Double getExternalRating() { - return externalRating; - } - - public void setExternalRating(Double externalRating) { - this.externalRating = externalRating; - } - - public String getMediaType() { - return this.mediaType; - } - - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - public Long getSeriesId() { - return this.seriesId; - } - - public void setSeriesId(Long seriesId) { - this.seriesId = seriesId; - } - - public int getDurationInSecond() { - return this.durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) { - this.durationInSecond = durationInSecond; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java deleted file mode 100644 index 65f4593..0000000 --- a/src/main/java/com/example/streamflix/model/RegisterRequest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.persistence.Column; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for user registration") -public class RegisterRequest { - - @Schema(description = "Email address of the user", example = "user@example.com") - @Email - @NotBlank - private String email; - - @Schema(description = "Password for the user account", example = "password123") - @NotBlank - @Column(name = "password_hash") - private String password; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java deleted file mode 100644 index 2f48bc6..0000000 --- a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; - -@Schema(description = "Request object for resetting password") -public class ResetPasswordRequest { - - @NotBlank(message = "Token is required") - @Schema(description = "Password reset token received via email", example = "abc-123-xyz") - private String token; - - @NotBlank(message = "New password is required") - @Schema(description = "New password for the account", example = "newPassword123") - private String newPassword; - - public ResetPasswordRequest() { - } - - public ResetPasswordRequest(String token, String newPassword) { - this.token = token; - this.newPassword = newPassword; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - - public String getNewPassword() { - return newPassword; - } - - public void setNewPassword(String newPassword) { - this.newPassword = newPassword; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java deleted file mode 100644 index 1d82c7b..0000000 --- a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class StartWatchingRequest { - - @NotNull - private Long profileId; - - private Long movieId; - - private Long episodeId; - - public Long getProfileId() { - return profileId; - } - - public void setProfileId(Long profileId) { - this.profileId = profileId; - } - - public Long getMovieId() { - return movieId; - } - - public void setMovieId(Long movieId) { - this.movieId = movieId; - } - - public Long getEpisodeId() { - return episodeId; - } - - public void setEpisodeId(Long episodeId) { - this.episodeId = episodeId; - } -} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java deleted file mode 100644 index b699bfa..0000000 --- a/src/main/java/com/example/streamflix/model/StreamValidationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.streamflix.model; - -import com.example.streamflix.enums.MediaQuality; - -public class StreamValidationResult { - private Long profileId; - private Long mediaId; - private MediaQuality requestedQuality; - private boolean allowed; - private String reason; - private MediaQuality suggestedQuality; - - // Getters and setters - public Long getProfileId() { return profileId; } - public void setProfileId(Long profileId) { this.profileId = profileId; } - - public Long getMediaId() { return mediaId; } - public void setMediaId(Long mediaId) { this.mediaId = mediaId; } - - public MediaQuality getRequestedQuality() { return requestedQuality; } - public void setRequestedQuality(MediaQuality requestedQuality) { - this.requestedQuality = requestedQuality; - } - - public boolean isAllowed() { return allowed; } - public void setAllowed(boolean allowed) { this.allowed = allowed; } - - public String getReason() { return reason; } - public void setReason(String reason) { this.reason = reason; } - - public MediaQuality getSuggestedQuality() { return suggestedQuality; } - public void setSuggestedQuality(MediaQuality suggestedQuality) { - this.suggestedQuality = suggestedQuality; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java deleted file mode 100644 index 0020b62..0000000 --- a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.streamflix.model; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TmdbMovieResponse { - - private Long id; - private String title; - private String overview; - - @JsonProperty("poster_path") - private String posterPath; - - @JsonProperty("backdrop_path") - private String backdropPath; - - @JsonProperty("vote_average") - private Double voteAverage; - - @JsonProperty("release_date") - private String releaseDate; - - // Getters and setters - public Long getId() { return id; } - public void setId(Long id) { this.id = id; } - - public String getTitle() { return title; } - public void setTitle(String title) { this.title = title; } - - public String getOverview() { return overview; } - public void setOverview(String overview) { this.overview = overview; } - - public String getPosterPath() { return posterPath; } - public void setPosterPath(String posterPath) { this.posterPath = posterPath; } - - public String getBackdropPath() { return backdropPath; } - public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } - - public Double getVoteAverage() { return voteAverage; } - public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } - - public String getReleaseDate() { return releaseDate; } - public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java deleted file mode 100644 index 6c02007..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.example.streamflix.model; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "Request object for updating an existing profile") -public class UpdateProfileRequest { - - @Schema(description = "New name of the profile", example = "Jane Doe") - private String name; - - @Schema(description = "New ID of the age rating for the profile", example = "2") - private Long ageRatingId; - - @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") - private String imageUrl; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Long getAgeRatingId() { - return ageRatingId; - } - - public void setAgeRatingId(Long ageRatingId) { - this.ageRatingId = ageRatingId; - } - - public String getImageUrl() { - return imageUrl; - } - - public void setImageUrl(String imageUrl) { - this.imageUrl = imageUrl; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java deleted file mode 100644 index 6cb83ad..0000000 --- a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpdateProgressRequest { - - @NotNull - private Integer lastPositionSeconds; - - @NotNull - private Integer durationWatchedSeconds; - - public Integer getLastPositionSeconds() { - return lastPositionSeconds; - } - - public void setLastPositionSeconds(Integer lastPositionSeconds) { - this.lastPositionSeconds = lastPositionSeconds; - } - - public Integer getDurationWatchedSeconds() { - return durationWatchedSeconds; - } - - public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { - this.durationWatchedSeconds = durationWatchedSeconds; - } -} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java deleted file mode 100644 index 71768fd..0000000 --- a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.streamflix.model; - -import jakarta.validation.constraints.NotNull; - -public class UpgradeSubscriptionRequest { - - @NotNull - private Long tierId; - - public Long getTierId() { - return tierId; - } - - public void setTierId(Long tierId) { - this.tierId = tierId; - } -} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java deleted file mode 100644 index 562787a..0000000 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Account; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface AccountRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java deleted file mode 100644 index 37ceced..0000000 --- a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.AgeRating; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface AgeRatingRepository extends JpaRepository { - boolean existsByLabel(String label); - Optional findByLabel(String label); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java deleted file mode 100644 index e87053b..0000000 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Employee; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface EmployeeRepository extends JpaRepository { - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java deleted file mode 100644 index c6f2021..0000000 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Episode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface EpisodeRepository extends JpaRepository { - List findBySeasonIdOrderByEpisodeNumber(Long seasonId); - Optional findByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java deleted file mode 100644 index 25e42a5..0000000 --- a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.InvitationStatus; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface InvitationStatusRepository extends JpaRepository { - Optional findByStatus(String status); -} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java deleted file mode 100644 index 8deb3ed..0000000 --- a/src/main/java/com/example/streamflix/repository/MediaRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Media; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface MediaRepository extends JpaRepository { - Optional findById(Long id); - boolean existsByTitle(String title); -} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java deleted file mode 100644 index 58ce6f2..0000000 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Movie; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface MovieRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java deleted file mode 100644 index 3041843..0000000 --- a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Preference; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface PreferenceRepository extends JpaRepository { - List findByProfileId(Long profileId); -} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java deleted file mode 100644 index ccb9a1a..0000000 --- a/src/main/java/com/example/streamflix/repository/ProfileRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Profile; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface ProfileRepository extends JpaRepository { - List findByAccountId(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java deleted file mode 100644 index baa7e2e..0000000 --- a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.QualityType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface QualityTypeRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java deleted file mode 100644 index bb297f2..0000000 --- a/src/main/java/com/example/streamflix/repository/ReferralRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Referral; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface ReferralRepository extends JpaRepository { - Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); - List findByInviterAccountId(Long inviterAccountId); - Optional findByInviteeAccountId(Long inviteeAccountId); - List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); -} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java deleted file mode 100644 index 67fbc41..0000000 --- a/src/main/java/com/example/streamflix/repository/RoleRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Role; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface RoleRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java deleted file mode 100644 index 3ef9e32..0000000 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Season; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface SeasonRepository extends JpaRepository { - List findBySeriesIdOrderBySeasonNumber(Long seriesId); -} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java deleted file mode 100644 index e15ec1b..0000000 --- a/src/main/java/com/example/streamflix/repository/SeriesRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Series; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SeriesRepository extends JpaRepository { -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java deleted file mode 100644 index 51a1c57..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.Subscription; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -@Repository -public interface SubscriptionRepository extends JpaRepository { - Optional findByAccountIdAndIsActiveTrue(Long accountId); -} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java deleted file mode 100644 index a5c808b..0000000 --- a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.SubscriptionTier; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface SubscriptionTierRepository extends JpaRepository { - Optional findByName(String name); -} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java deleted file mode 100644 index 6285d35..0000000 --- a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.VerificationToken; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface VerificationTokenRepository extends JpaRepository { - VerificationToken findByToken(String token); -} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java deleted file mode 100644 index 62f7a1b..0000000 --- a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.ViewingProgress; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface ViewingProgressRepository extends JpaRepository { - List findByProfileIdOrderByStartTimeDesc(Long profileId); - Optional findByProfileIdAndMovieId(Long profileId, Long movieId); - Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); -} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java deleted file mode 100644 index 6750ec5..0000000 --- a/src/main/java/com/example/streamflix/repository/WatchListRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.streamflix.repository; - -import com.example.streamflix.entity.WatchList; -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; -import java.util.Optional; - -public interface WatchListRepository extends JpaRepository { - List findByProfileIdOrderByAddedDateDesc(Long profileId); - Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); - void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); -} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java deleted file mode 100644 index c16f019..0000000 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.streamflix.security; - -import com.example.streamflix.entity.Account; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; - -public class CustomUserDetails implements UserDetails { - - private final Account account; - - public CustomUserDetails(Account account) { - this.account = account; - } - - public Account getAccount() { - return account; - } - - @Override - public Collection getAuthorities() { - return Collections.emptyList(); - } - - @Override - public String getPassword() { - return account.getPassword(); - } - - @Override - public String getUsername() { - return account.getEmail(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return !account.isBlocked(); - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return account.isVerified(); - } -} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java deleted file mode 100644 index e1aff8c..0000000 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.security.CustomUserDetails; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class AccountUserDetailsService implements UserDetailsService { - - private final AccountRepository accountRepository; - - public AccountUserDetailsService(AccountRepository accountRepository) { - this.accountRepository = accountRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Account account = accountRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - - return new CustomUserDetails(account); - } -} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java deleted file mode 100644 index b471762..0000000 --- a/src/main/java/com/example/streamflix/service/AgeRatingService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.repository.AgeRatingRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class AgeRatingService { - - private final AgeRatingRepository ageRatingRepository; - - public AgeRatingService(AgeRatingRepository ageRatingRepository) { - this.ageRatingRepository = ageRatingRepository; - } - - public Long findIdByLabel(String label) { - return ageRatingRepository.findByLabel(label) - .map(AgeRating::getId) - .orElseThrow(() -> - new IllegalArgumentException( - "AgeRating not found with name: " + label - ) - ); - } - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java deleted file mode 100644 index 1cd2caf..0000000 --- a/src/main/java/com/example/streamflix/service/ContentAccessService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.repository.SubscriptionRepository; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class ContentAccessService { - - private final SubscriptionRepository subscriptionRepository; - - public ContentAccessService(SubscriptionRepository subscriptionRepository) { - this.subscriptionRepository = subscriptionRepository; - } - - /** - * Checks if the content is allowed for the given profile based on age rating. - * - * @param profile The user profile attempting to access the content. - * @param media The media content being accessed. - * @return true if the profile's age rating allows access to the media, false otherwise. - */ - public boolean isContentAllowed(Profile profile, Media media) { - if (profile == null || media == null) { - return false; - } - - if (profile.getAgeRating() == null || media.getAgeRating() == null) { - return false; - } - - int profileMaxAge = profile.getAgeRating().getMinAge(); - int mediaMinAge = media.getAgeRating().getMinAge(); - - return profileMaxAge >= mediaMinAge; - } - - /** - * Checks if the requested quality is allowed for the user's subscription tier. - * - * @param account The user account. - * @param requestedQuality The quality of the stream being requested. - * @return true if the subscription tier supports the requested quality, false otherwise. - */ - public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { - if (account == null || requestedQuality == null) { - return false; - } - - Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); - if (activeSubscriptionOpt.isEmpty()) { - return false; // No active subscription - } - - SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); - if (tier == null || tier.getMaxQuality() == null) { - return false; // Subscription tier not configured properly - } - - String maxQuality = tier.getMaxQuality().getName(); - int maxQualityLevel = getQualityLevel(maxQuality); - int requestedQualityLevel = getQualityLevel(requestedQuality.name()); - - return requestedQualityLevel <= maxQualityLevel; - } - - private int getQualityLevel(String quality) { - switch (quality.toUpperCase()) { - case "SD": - return 1; - case "HD": - return 2; - case "UHD": - return 3; - default: - return 0; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java deleted file mode 100644 index 88292dd..0000000 --- a/src/main/java/com/example/streamflix/service/JwtService.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.streamflix.service; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; - -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Service -public class JwtService { - - @Value("${jwt.secret}") - private String secretKey; - - @Value("${jwt.expiration:36000000}") // 10 hours default - private long jwtExpiration; - - public String generateToken(String email, Long accountId) { - Map claims = new HashMap<>(); - claims.put("accountId", accountId); - - return Jwts.builder() - .setClaims(claims) - .setSubject(email) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } - - private Key getSigningKey() { - byte[] keyBytes = Decoders.BASE64.decode(secretKey); - return Keys.hmacShaKeyFor(keyBytes); - } - - public String extractEmail(String token) { - return extractAllClaims(token).getSubject(); - } - - public Long extractAccountId(String token) { - Claims claims = extractAllClaims(token); - return claims.get("accountId", Long.class); - } - - public boolean isTokenValid(String token, UserDetails userDetails) { - final String email = extractEmail(token); - return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); - } - - private boolean isTokenExpired(String token) { - return extractAllClaims(token).getExpiration().before(new Date()); - } - - private Claims extractAllClaims(String token) { - return Jwts.parser() - .verifyWith((javax.crypto.SecretKey) getSigningKey()) - .build() - .parseSignedClaims(token) - .getPayload(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java deleted file mode 100644 index 909f633..0000000 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.stream.Collectors; - -@Service -public class MediaService -{ - - private final MediaRepository mediaRepository; - private final ProfileRepository profileRepository; - private final AgeRatingRepository ageRatingRepository; - private final EpisodeRepository episodeRepository; - private final SeriesRepository seriesRepository; - private final TmdbService tmdbService; - private final ContentAccessService contentAccessService; - private final AgeRatingService ageRatingService; - private final SecurityUtils securityUtils; - - public MediaService(MediaRepository mediaRepository, - ProfileRepository profileRepository, - AgeRatingRepository ageRatingRepository, - EpisodeRepository episodeRepository, - SeriesRepository seriesRepository, - TmdbService tmdbService, - ContentAccessService contentAccessService, - AgeRatingService ageRatingService, - SecurityUtils securityUtils) - { - this.mediaRepository = mediaRepository; - this.profileRepository = profileRepository; - this.ageRatingRepository = ageRatingRepository; - this.episodeRepository = episodeRepository; - this.seriesRepository = seriesRepository; - this.tmdbService = tmdbService; - this.contentAccessService = contentAccessService; - this.ageRatingService = ageRatingService; - this.securityUtils = securityUtils; - } - - /** - * Get enriched media details with TMDB data - */ - public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - throw new SecurityException("Content not allowed for this profile's age rating"); - } - - // Build DTO with local data - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - } - - /** - * Get all media available for a profile (filtered by age rating) - */ - public List getAvailableMedia(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return mediaRepository.findAll().stream() - .filter(media -> contentAccessService.isContentAllowed(profile, media)) - .map(media -> { - MediaDetailsDto dto = buildMediaDto(media); - - // Enrich with TMDB data if external ID exists - if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { - enrichWithTmdbData(dto, media.getExternalId()); - } - - return dto; - }) - .collect(Collectors.toList()); - } - - /** - * Build basic MediaDetailsDto from Media entity - */ - private MediaDetailsDto buildMediaDto(Media media) { - MediaDetailsDto dto = new MediaDetailsDto(); - dto.setId(media.getId()); - dto.setTitle(media.getTitle()); - dto.setReleaseDate(media.getReleaseDate()); - dto.setAgeRating(media.getAgeRating().getLabel()); - dto.setExternalId(media.getExternalId()); - - return dto; - } - - /** - * Enrich DTO with TMDB data - */ - private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { - try { - Long tmdbId = Long.parseLong(externalId); - - // Fetch full details - var tmdbDetails = tmdbService.getMovieDetails(tmdbId); - if (tmdbDetails != null) { - if (tmdbDetails.getPosterPath() != null) { - dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); - } - - dto.setExternalRating(tmdbDetails.getVoteAverage()); - dto.setOverview(tmdbDetails.getOverview()); - - if (tmdbDetails.getBackdropPath() != null) { - dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); - } - } - } catch (NumberFormatException e) { - System.err.println("Invalid external ID format: " + externalId); - } catch (Exception e) { - System.err.println("Failed to fetch TMDB data: " + e.getMessage()); - } - } - - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker - - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } - - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); - - return episodeRepository.save(episode); - - } - -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - - private AgeRating getAgeRating(String label) { - return ageRatingRepository.findByLabel(label) - .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); - } - - -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java deleted file mode 100644 index 5dfe578..0000000 --- a/src/main/java/com/example/streamflix/service/ProfileService.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.AgeRating; -import com.example.streamflix.entity.Preference; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.enums.PreferenceType; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.AgeRatingRepository; -import com.example.streamflix.repository.PreferenceRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.List; - -@Service -public class ProfileService { - - private final ProfileRepository profileRepository; - private final AccountRepository accountRepository; - private final AgeRatingRepository ageRatingRepository; - private final PreferenceRepository preferenceRepository; - private final SecurityUtils securityUtils; - - private static final int MAX_PROFILES_PER_ACCOUNT = 5; - - public ProfileService(ProfileRepository profileRepository, - AccountRepository accountRepository, - AgeRatingRepository ageRatingRepository, - PreferenceRepository preferenceRepository, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.accountRepository = accountRepository; - this.ageRatingRepository = ageRatingRepository; - this.preferenceRepository = preferenceRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { - // Get authenticated user's accountId from JWT - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Account account = accountRepository.findById(authenticatedAccountId) - .orElseThrow(() -> new IllegalArgumentException("Account not found")); - - // Limit the number of profiles per account - List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); - if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { - throw new IllegalArgumentException("Maximum number of profiles reached for this account"); - } - - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - - Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); - return profileRepository.save(profile); - } - - public List getMyProfiles() { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - return profileRepository.findByAccountId(authenticatedAccountId); - } - - public Profile getProfileById(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - This is the key part! - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to access this profile"); - } - - return profile; - } - - @Transactional - public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to update this profile"); - } - - if (name != null) { - profile.setName(name); - } - if (ageRatingId != null) { - AgeRating ageRating = ageRatingRepository.findById(ageRatingId) - .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); - profile.setAgeRating(ageRating); - } - if (imageUrl != null) { - profile.setImageUrl(imageUrl); - } - - return profileRepository.save(profile); - } - - @Transactional - public void deleteProfile(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete this profile"); - } - - profileRepository.delete(profile); - } - - @Transactional - public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to modify preferences for this profile"); - } - - Preference preference = new Preference(profile, preferenceType, value); - return preferenceRepository.save(preference); - } - - public List getProfilePreferences(Long profileId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to view preferences for this profile"); - } - - return preferenceRepository.findByProfileId(profileId); - } - - @Transactional - public void deletePreference(Long profileId, Long preferenceId) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // AUTHORIZATION CHECK - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to delete preferences for this profile"); - } - - Preference preference = preferenceRepository.findById(preferenceId) - .orElseThrow(() -> new IllegalArgumentException("Preference not found")); - - // Verify the preference belongs to this profile - if (!preference.getProfile().getId().equals(profileId)) { - throw new IllegalArgumentException("Preference does not belong to this profile"); - } - - preferenceRepository.delete(preference); - } -} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java deleted file mode 100644 index fd2512b..0000000 --- a/src/main/java/com/example/streamflix/service/ReferralService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.InvitationStatus; -import com.example.streamflix.entity.Referral; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.model.InvitationResponse; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.InvitationStatusRepository; -import com.example.streamflix.repository.ReferralRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Service -public class ReferralService { - - private final ReferralRepository referralRepository; - private final AccountRepository accountRepository; - private final InvitationStatusRepository invitationStatusRepository; - private final SubscriptionRepository subscriptionRepository; - private final SecurityUtils securityUtils; - - public ReferralService(ReferralRepository referralRepository, - AccountRepository accountRepository, - InvitationStatusRepository invitationStatusRepository, - SubscriptionRepository subscriptionRepository, - SecurityUtils securityUtils) { - this.referralRepository = referralRepository; - this.accountRepository = accountRepository; - this.invitationStatusRepository = invitationStatusRepository; - this.subscriptionRepository = subscriptionRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public InvitationResponse createInvitation() { - Long inviterId = securityUtils.getAuthenticatedAccountId(); - Account inviterAccount = accountRepository.findById(inviterId) - .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); - - // Check if inviter has an active subscription - Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); - if (activeSubscription.isEmpty()) { - throw new IllegalArgumentException("You must have an active subscription to invite others"); - } - - InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") - .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); - - Referral referral = new Referral(inviterAccount); - referral.setStatus(pendingStatus); - referral.setDiscountApplied(false); - - Referral savedReferral = referralRepository.save(referral); - - // Generate a simple invite code based on the referral ID - // In a real application, you might want to use a more secure or obfuscated code - String inviteCode = "INVITE-" + savedReferral.getId(); - - return new InvitationResponse(savedReferral, inviteCode); - } - - @Transactional - public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { - // Decode the inviteCode to get referral ID - Long referralId; - try { - if (inviteCode.startsWith("INVITE-")) { - referralId = Long.parseLong(inviteCode.substring(7)); - } else { - throw new IllegalArgumentException("Invalid invite code format"); - } - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid invite code"); - } - - Referral referral = referralRepository.findById(referralId) - .orElseThrow(() -> new IllegalArgumentException("Referral not found")); - - if (!"Pending".equals(referral.getStatus().getStatus())) { - throw new IllegalArgumentException("Invitation is no longer valid"); - } - - if (referral.getInviteeAccount() != null) { - throw new IllegalArgumentException("Invitation has already been accepted"); - } - - if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { - throw new IllegalArgumentException("You cannot accept your own invitation"); - } - - Account inviteeAccount = accountRepository.findById(inviteeAccountId) - .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); - - InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") - .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); - - referral.setInviteeAccount(inviteeAccount); - referral.setStatus(acceptedStatus); - - return referralRepository.save(referral); - } - - public List getMyInvitations() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviterAccountId(accountId); - } - - public Optional getMyReferralStatus() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return referralRepository.findByInviteeAccountId(accountId); - } -} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java deleted file mode 100644 index e4de1e0..0000000 --- a/src/main/java/com/example/streamflix/service/RegistrationService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.enums.TokenType; -import com.example.streamflix.entity.Account; -import com.example.streamflix.model.RegisterRequest; -import com.example.streamflix.entity.VerificationToken; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.VerificationTokenRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.UUID; - -@Service -public class RegistrationService { - - private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); - - private final AccountRepository accountRepository; - private final VerificationTokenRepository tokenRepository; - private final PasswordEncoder passwordEncoder; - - public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { - this.accountRepository = accountRepository; - this.tokenRepository = tokenRepository; - this.passwordEncoder = passwordEncoder; - } - - @Transactional - public void register(RegisterRequest request) { - if (accountRepository.findByEmail(request.getEmail()).isPresent()) { - throw new IllegalArgumentException("Email already taken"); - } - - Account account = new Account(); - account.setEmail(request.getEmail()); - account.setPassword(passwordEncoder.encode(request.getPassword())); - account.setFailedLoginAttempts(0); - account.setBlocked(false); - account.setVerified(false); - - accountRepository.save(account); - - String token = UUID.randomUUID().toString(); - VerificationToken verificationToken = new VerificationToken( - token, - account, - LocalDateTime.now().plusHours(24), - TokenType.EMAIL_VERIFICATION - ); - - tokenRepository.save(verificationToken); - - // Log the token for testing purposes since email sending isn't implemented - logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java deleted file mode 100644 index 85e9443..0000000 --- a/src/main/java/com/example/streamflix/service/StreamingService.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.enums.MediaQuality; -import com.example.streamflix.model.StreamValidationResult; -import com.example.streamflix.repository.MediaRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; - -@Service -public class StreamingService { - - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ContentAccessService contentAccessService; - private final SecurityUtils securityUtils; - - public StreamingService(ProfileRepository profileRepository, - MediaRepository mediaRepository, - ContentAccessService contentAccessService, - SecurityUtils securityUtils) { - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.contentAccessService = contentAccessService; - this.securityUtils = securityUtils; - } - - /** - * Validates if a profile can stream media at the requested quality - */ - public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { - Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); - - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new IllegalArgumentException("Profile not found")); - - // Authorization check - if (!profile.getAccount().getId().equals(authenticatedAccountId)) { - throw new SecurityException("You are not authorized to use this profile"); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new IllegalArgumentException("Media not found")); - - StreamValidationResult result = new StreamValidationResult(); - result.setProfileId(profileId); - result.setMediaId(mediaId); - result.setRequestedQuality(requestedQuality); - - // Check age rating - if (!contentAccessService.isContentAllowed(profile, media)) { - result.setAllowed(false); - result.setReason("Content not allowed for this profile's age rating"); - return result; - } - - // Check subscription quality - Account account = profile.getAccount(); - if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { - result.setAllowed(false); - result.setReason("Your subscription does not support " + requestedQuality + " quality"); - result.setSuggestedQuality(getMaxAllowedQuality(account)); - return result; - } - - result.setAllowed(true); - result.setReason("Stream validated successfully"); - result.setSuggestedQuality(requestedQuality); - return result; - } - - private MediaQuality getMaxAllowedQuality(Account account) { - // Implement logic to get max quality from subscription - // This is a simplified version - if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { - return MediaQuality.UHD; - } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { - return MediaQuality.HD; - } - return MediaQuality.SD; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java deleted file mode 100644 index b71950d..0000000 --- a/src/main/java/com/example/streamflix/service/SubscriptionService.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Account; -import com.example.streamflix.entity.Subscription; -import com.example.streamflix.entity.SubscriptionTier; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.AccountRepository; -import com.example.streamflix.repository.SubscriptionRepository; -import com.example.streamflix.repository.SubscriptionTierRepository; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.time.LocalDate; -import java.util.Optional; - -@Service -public class SubscriptionService { - - private final SubscriptionRepository subscriptionRepository; - private final SubscriptionTierRepository subscriptionTierRepository; - private final AccountRepository accountRepository; - private final SecurityUtils securityUtils; - - public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { - this.subscriptionRepository = subscriptionRepository; - this.subscriptionTierRepository = subscriptionTierRepository; - this.accountRepository = accountRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public Subscription createTrialSubscription(Long accountId, Long tierId) { - Account account = accountRepository.findById(accountId) - .orElseThrow(() -> new NotFoundException("Account not found")); - if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { - throw new IllegalArgumentException("Account already has an active subscription"); - } - SubscriptionTier tier = subscriptionTierRepository.findById(tierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - Subscription subscription = new Subscription(); - subscription.setAccount(account); - subscription.setTier(tier); - subscription.setStartDate(LocalDate.now()); - subscription.setEndDate(LocalDate.now().plusDays(7)); - subscription.setTrial(true); - subscription.setActive(true); - - return subscriptionRepository.save(subscription); - } - - @Transactional - public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { - if (!securityUtils.isOwner(accountId)) { - throw new AccessDeniedException("You are not authorized to upgrade this subscription."); - } - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) - .orElseThrow(() -> new NotFoundException("Subscription tier not found")); - - if (subscription.getTrial()) { - subscription.setTrial(false); - subscription.setEndDate(null); - } - subscription.setTier(newTier); - - return subscriptionRepository.save(subscription); - } - - public Optional getMyActiveSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); - } - - @Transactional - public Subscription cancelSubscription() { - Long accountId = securityUtils.getAuthenticatedAccountId(); - Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) - .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); - - subscription.setActive(false); - // End date automatically set by database trigger - // subscription.setEndDate(LocalDate.now()); - - return subscriptionRepository.save(subscription); - } -} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java deleted file mode 100644 index 83afcae..0000000 --- a/src/main/java/com/example/streamflix/service/TmdbService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.model.TmdbMovieResponse; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -@Service -public class TmdbService { - - private final WebClient webClient; - - @Value("${tmdb.api.key}") - private String apiKey; - - @Value("${tmdb.api.url}") - private String apiUrl; - - public TmdbService(WebClient webClient) { - this.webClient = webClient; - } - - public TmdbMovieResponse getMovieDetails(Long tmdbId) { - return webClient.get() - .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) - .retrieve() - .bodyToMono(TmdbMovieResponse.class) - .block(); - } - - public String getMoviePosterUrl(Long tmdbId) { - TmdbMovieResponse movie = getMovieDetails(tmdbId); - if (movie != null && movie.getPosterPath() != null) { - return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); - } - return null; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java deleted file mode 100644 index dfbc9cd..0000000 --- a/src/main/java/com/example/streamflix/service/ViewingProgressService.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.Episode; -import com.example.streamflix.entity.Movie; -import com.example.streamflix.entity.Profile; -import com.example.streamflix.entity.ViewingProgress; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.EpisodeRepository; -import com.example.streamflix.repository.MovieRepository; -import com.example.streamflix.repository.ProfileRepository; -import com.example.streamflix.repository.ViewingProgressRepository; -import com.example.streamflix.util.SecurityUtils; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import jakarta.persistence.Query; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -public class ViewingProgressService { - - private final ViewingProgressRepository viewingProgressRepository; - private final ProfileRepository profileRepository; - private final MovieRepository movieRepository; - private final EpisodeRepository episodeRepository; - private final WatchListService watchListService; - private final SecurityUtils securityUtils; - - @PersistenceContext - private EntityManager entityManager; - - public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { - this.viewingProgressRepository = viewingProgressRepository; - this.profileRepository = profileRepository; - this.movieRepository = movieRepository; - this.episodeRepository = episodeRepository; - this.watchListService = watchListService; - this.securityUtils = securityUtils; - } - - @Transactional - public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { - throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); - } - - ViewingProgress viewingProgress = new ViewingProgress(); - viewingProgress.setProfile(profile); - - if (movieId != null) { - Movie movie = movieRepository.findById(movieId) - .orElseThrow(() -> new NotFoundException("Movie not found")); - viewingProgress.setMovie(movie); - } else { - Episode episode = episodeRepository.findById(episodeId) - .orElseThrow(() -> new NotFoundException("Episode not found")); - viewingProgress.setEpisode(episode); - } - - viewingProgress.setDurationWatchedSeconds(0); - viewingProgress.setLastPositionSeconds(0); - viewingProgress.setIsFinished(false); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setLastPositionSeconds(lastPositionSeconds); - viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); - - return viewingProgressRepository.save(viewingProgress); - } - - @Transactional - public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { - ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) - .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); - if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this progress."); - } - - viewingProgress.setIsFinished(true); - ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); - - // Watchlist auto-removal handled by database trigger - /* - Long profileId = savedProgress.getProfile().getId(); - Long mediaId = null; - if (savedProgress.getMovie() != null) { - mediaId = savedProgress.getMovie().getId(); - } else if (savedProgress.getEpisode() != null) { - mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); - } - - if (mediaId != null) { - watchListService.checkAndRemoveIfFinished(profileId, mediaId); - } - */ - - return savedProgress; - } - - public List getMyViewingHistory(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); - } - - public Map getProfileViewingStats(Long profileId) { - // Verify profile belongs to authenticated user - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new SecurityException("You are not authorized to access this profile"); - } - - // Call the database function using native query - Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); - query.setParameter("profileId", profileId); - - Object[] result = (Object[]) query.getSingleResult(); - - Map stats = new HashMap<>(); - stats.put("total_movies_watched", result[0]); - stats.put("total_episodes_watched", result[1]); - stats.put("total_watch_time_hours", result[2]); - stats.put("unique_content_watched", result[3]); - stats.put("finished_content_count", result[4]); - - return stats; - } -} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java deleted file mode 100644 index 9c8c01f..0000000 --- a/src/main/java/com/example/streamflix/service/WatchListService.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.example.streamflix.service; - -import com.example.streamflix.entity.*; -import com.example.streamflix.exception.NotFoundException; -import com.example.streamflix.repository.*; -import com.example.streamflix.util.SecurityUtils; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.nio.file.AccessDeniedException; -import java.util.List; - -@Service -public class WatchListService { - - private final WatchListRepository watchListRepository; - private final ProfileRepository profileRepository; - private final MediaRepository mediaRepository; - private final ViewingProgressRepository viewingProgressRepository; - private final SeriesRepository seriesRepository; - private final SecurityUtils securityUtils; - - public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { - this.watchListRepository = watchListRepository; - this.profileRepository = profileRepository; - this.mediaRepository = mediaRepository; - this.viewingProgressRepository = viewingProgressRepository; - this.seriesRepository = seriesRepository; - this.securityUtils = securityUtils; - } - - @Transactional - public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - Media media = mediaRepository.findById(mediaId) - .orElseThrow(() -> new NotFoundException("Media not found")); - - // Application-level check - also enforced by database trigger as safety net - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { - throw new IllegalArgumentException("Media already in watch list"); - } - - WatchList watchList = new WatchList(profile, media); - return watchListRepository.save(watchList); - } - - @Transactional - public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - - if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { - throw new NotFoundException("Media not found in watch list"); - } - - watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); - } - - public List getMyWatchList(Long profileId) throws AccessDeniedException { - Profile profile = profileRepository.findById(profileId) - .orElseThrow(() -> new NotFoundException("Profile not found")); - if (!securityUtils.isOwner(profile.getAccount().getId())) { - throw new AccessDeniedException("You are not authorized to access this profile."); - } - return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); - } - - // Logic moved to database trigger: trg_auto_remove_from_watchlist - /* - @Transactional - public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { - Media media = mediaRepository.findById(mediaId).orElse(null); - if (media == null) { - return; - } - - boolean isFinished = false; - if (media instanceof Movie) { - isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) - .map(ViewingProgress::getIsFinished) - .orElse(false); - } else if (media instanceof Series) { - Series series = seriesRepository.findById(mediaId).orElse(null); - if (series != null) { - isFinished = series.getSeasons().stream() - .flatMap(season -> season.getEpisodes().stream()) - .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) - .map(ViewingProgress::getIsFinished) - .orElse(false)); - } - } - - if (isFinished) { - try { - removeFromWatchList(profileId, mediaId); - } catch (NotFoundException | AccessDeniedException e) { - // Silently catch exception if item is not in the watch list or access is denied - } - } - } - */ -} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java deleted file mode 100644 index c668910..0000000 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.streamflix.util; - -import com.example.streamflix.service.JwtService; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -@Component -public class SecurityUtils { - - private final JwtService jwtService; - - public SecurityUtils(JwtService jwtService) { - this.jwtService = jwtService; - } - - /** - * Extracts the accountId from the JWT token in the current request - */ - public Long getAuthenticatedAccountId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes == null) { - throw new IllegalStateException("No request context available"); - } - - HttpServletRequest request = attributes.getRequest(); - String authHeader = request.getHeader("Authorization"); - - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - throw new IllegalStateException("No valid JWT token found"); - } - - String token = authHeader.substring(7); - return jwtService.extractAccountId(token); - } - - public String getAuthenticatedEmail() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - throw new IllegalStateException("User is not authenticated"); - } - return authentication.getName(); - } - - public boolean isOwner(Long accountId) { - try { - Long authenticatedAccountId = getAuthenticatedAccountId(); - return authenticatedAccountId.equals(accountId); - } catch (IllegalStateException e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java deleted file mode 100644 index 269bd8f..0000000 --- a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.streamflix; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class StreamflixApplicationTests { - - @Test - void contextLoads() { - } - -} From 1dc8b5b61e331d5e8bf76a3c02227d6827809908 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Wed, 7 Jan 2026 21:50:22 +0100 Subject: [PATCH 16/22] working post movie and series method (still need some fixes) --- .gitignore | 12 + .mvn/wrapper/maven-wrapper.properties | 3 + Dockerfile | 17 + README.md | 25 ++ backups/.gitkeep | 0 docker-compose.yml | 42 +++ docs/BACKUP_RECOVERY.md | 258 +++++++++++++++ init-db/03_schema.sql | 291 +++++++++++++++++ init-db/04_referral_logic.sql | 80 +++++ init-db/05_employee_views.sql | 59 ++++ init-db/06_additional_triggers.sql | 127 ++++++++ init-db/07_stored_procedures.sql | 157 ++++++++++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 100 ++++++ scripts/backup.ps1 | 56 ++++ scripts/backup.sh | 45 +++ scripts/restore.ps1 | 92 ++++++ scripts/restore.sh | 84 +++++ .../streamflix/StreamflixApplication.java | 16 + .../config/JwtAuthenticationFilter.java | 61 ++++ .../streamflix/config/ScheduledTasks.java | 37 +++ .../streamflix/config/SecurityConfig.java | 53 ++++ .../streamflix/config/WebClientConfig.java | 14 + .../controller/AnalyticsController.java | 105 +++++++ .../streamflix/controller/AuthController.java | 221 +++++++++++++ .../controller/MediaController.java | 88 ++++++ .../controller/ProfileController.java | 192 ++++++++++++ .../controller/ReferralController.java | 82 +++++ .../controller/SubscriptionController.java | 99 ++++++ .../controller/ViewingProgressController.java | 134 ++++++++ .../controller/WatchListController.java | 93 ++++++ .../example/streamflix/entity/Account.java | 109 +++++++ .../example/streamflix/entity/AgeRating.java | 55 ++++ .../streamflix/entity/ContentWarning.java | 42 +++ .../example/streamflix/entity/Employee.java | 63 ++++ .../example/streamflix/entity/Episode.java | 92 ++++++ .../streamflix/entity/InvitationStatus.java | 38 +++ .../com/example/streamflix/entity/Media.java | 98 ++++++ .../com/example/streamflix/entity/Movie.java | 46 +++ .../example/streamflix/entity/Preference.java | 71 +++++ .../example/streamflix/entity/Profile.java | 125 ++++++++ .../streamflix/entity/QualityType.java | 31 ++ .../example/streamflix/entity/Referral.java | 96 ++++++ .../com/example/streamflix/entity/Role.java | 38 +++ .../com/example/streamflix/entity/Season.java | 60 ++++ .../com/example/streamflix/entity/Series.java | 35 +++ .../streamflix/entity/Subscription.java | 90 ++++++ .../streamflix/entity/SubscriptionTier.java | 53 ++++ .../streamflix/entity/VerificationToken.java | 84 +++++ .../streamflix/entity/ViewingProgress.java | 127 ++++++++ .../example/streamflix/entity/WatchList.java | 74 +++++ .../streamflix/enums/MediaQuality.java | 7 + .../streamflix/enums/PreferenceType.java | 7 + .../example/streamflix/enums/TokenType.java | 6 + .../exception/GlobalExceptionHandler.java | 101 ++++++ .../exception/NotFoundException.java | 11 + .../model/AcceptInvitationRequest.java | 16 + .../model/AddToWatchListRequest.java | 28 ++ .../model/CreatePreferenceRequest.java | 34 ++ .../model/CreateProfileRequest.java | 56 ++++ .../streamflix/model/CreateTrialRequest.java | 28 ++ .../streamflix/model/ErrorResponse.java | 103 ++++++ .../model/ForgotPasswordRequest.java | 29 ++ .../streamflix/model/InvitationResponse.java | 29 ++ .../streamflix/model/LoginRequest.java | 36 +++ .../streamflix/model/MediaDetailsDto.java | 117 +++++++ .../streamflix/model/RegisterRequest.java | 36 +++ .../model/ResetPasswordRequest.java | 40 +++ .../model/StartWatchingRequest.java | 37 +++ .../model/StreamValidationResult.java | 35 +++ .../streamflix/model/TmdbMovieResponse.java | 44 +++ .../model/UpdateProfileRequest.java | 40 +++ .../model/UpdateProgressRequest.java | 28 ++ .../model/UpgradeSubscriptionRequest.java | 17 + .../repository/AccountRepository.java | 9 + .../repository/AgeRatingRepository.java | 13 + .../repository/EmployeeRepository.java | 12 + .../repository/EpisodeRepository.java | 14 + .../InvitationStatusRepository.java | 12 + .../repository/MediaRepository.java | 13 + .../repository/MovieRepository.java | 8 + .../repository/PreferenceRepository.java | 9 + .../repository/ProfileRepository.java | 12 + .../repository/QualityTypeRepository.java | 9 + .../repository/ReferralRepository.java | 16 + .../streamflix/repository/RoleRepository.java | 12 + .../repository/SeasonRepository.java | 9 + .../repository/SeriesRepository.java | 11 + .../repository/SubscriptionRepository.java | 12 + .../SubscriptionTierRepository.java | 9 + .../VerificationTokenRepository.java | 8 + .../repository/ViewingProgressRepository.java | 12 + .../repository/WatchListRepository.java | 12 + .../security/CustomUserDetails.java | 56 ++++ .../service/AccountUserDetailsService.java | 27 ++ .../streamflix/service/AgeRatingService.java | 28 ++ .../service/ContentAccessService.java | 82 +++++ .../streamflix/service/JwtService.java | 69 ++++ .../streamflix/service/MediaService.java | 241 ++++++++++++++ .../streamflix/service/ProfileService.java | 176 +++++++++++ .../streamflix/service/ReferralService.java | 119 +++++++ .../service/RegistrationService.java | 61 ++++ .../streamflix/service/StreamingService.java | 83 +++++ .../service/SubscriptionService.java | 90 ++++++ .../streamflix/service/TmdbService.java | 38 +++ .../service/ViewingProgressService.java | 154 +++++++++ .../streamflix/service/WatchListService.java | 111 +++++++ .../streamflix/util/SecurityUtils.java | 62 ++++ .../StreamflixApplicationTests.java | 13 + 110 files changed, 7061 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 backups/.gitkeep create mode 100644 docker-compose.yml create mode 100644 docs/BACKUP_RECOVERY.md create mode 100644 init-db/03_schema.sql create mode 100644 init-db/04_referral_logic.sql create mode 100644 init-db/05_employee_views.sql create mode 100644 init-db/06_additional_triggers.sql create mode 100644 init-db/07_stored_procedures.sql create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 scripts/backup.ps1 create mode 100644 scripts/backup.sh create mode 100644 scripts/restore.ps1 create mode 100644 scripts/restore.sh create mode 100644 src/main/java/com/example/streamflix/StreamflixApplication.java create mode 100644 src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/streamflix/config/ScheduledTasks.java create mode 100644 src/main/java/com/example/streamflix/config/SecurityConfig.java create mode 100644 src/main/java/com/example/streamflix/config/WebClientConfig.java create mode 100644 src/main/java/com/example/streamflix/controller/AnalyticsController.java create mode 100644 src/main/java/com/example/streamflix/controller/AuthController.java create mode 100644 src/main/java/com/example/streamflix/controller/MediaController.java create mode 100644 src/main/java/com/example/streamflix/controller/ProfileController.java create mode 100644 src/main/java/com/example/streamflix/controller/ReferralController.java create mode 100644 src/main/java/com/example/streamflix/controller/SubscriptionController.java create mode 100644 src/main/java/com/example/streamflix/controller/ViewingProgressController.java create mode 100644 src/main/java/com/example/streamflix/controller/WatchListController.java create mode 100644 src/main/java/com/example/streamflix/entity/Account.java create mode 100644 src/main/java/com/example/streamflix/entity/AgeRating.java create mode 100644 src/main/java/com/example/streamflix/entity/ContentWarning.java create mode 100644 src/main/java/com/example/streamflix/entity/Employee.java create mode 100644 src/main/java/com/example/streamflix/entity/Episode.java create mode 100644 src/main/java/com/example/streamflix/entity/InvitationStatus.java create mode 100644 src/main/java/com/example/streamflix/entity/Media.java create mode 100644 src/main/java/com/example/streamflix/entity/Movie.java create mode 100644 src/main/java/com/example/streamflix/entity/Preference.java create mode 100644 src/main/java/com/example/streamflix/entity/Profile.java create mode 100644 src/main/java/com/example/streamflix/entity/QualityType.java create mode 100644 src/main/java/com/example/streamflix/entity/Referral.java create mode 100644 src/main/java/com/example/streamflix/entity/Role.java create mode 100644 src/main/java/com/example/streamflix/entity/Season.java create mode 100644 src/main/java/com/example/streamflix/entity/Series.java create mode 100644 src/main/java/com/example/streamflix/entity/Subscription.java create mode 100644 src/main/java/com/example/streamflix/entity/SubscriptionTier.java create mode 100644 src/main/java/com/example/streamflix/entity/VerificationToken.java create mode 100644 src/main/java/com/example/streamflix/entity/ViewingProgress.java create mode 100644 src/main/java/com/example/streamflix/entity/WatchList.java create mode 100644 src/main/java/com/example/streamflix/enums/MediaQuality.java create mode 100644 src/main/java/com/example/streamflix/enums/PreferenceType.java create mode 100644 src/main/java/com/example/streamflix/enums/TokenType.java create mode 100644 src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/streamflix/exception/NotFoundException.java create mode 100644 src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/AddToWatchListRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/CreateTrialRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ErrorResponse.java create mode 100644 src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/InvitationResponse.java create mode 100644 src/main/java/com/example/streamflix/model/LoginRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaDetailsDto.java create mode 100644 src/main/java/com/example/streamflix/model/RegisterRequest.java create mode 100644 src/main/java/com/example/streamflix/model/ResetPasswordRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StartWatchingRequest.java create mode 100644 src/main/java/com/example/streamflix/model/StreamValidationResult.java create mode 100644 src/main/java/com/example/streamflix/model/TmdbMovieResponse.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProfileRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpdateProgressRequest.java create mode 100644 src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java create mode 100644 src/main/java/com/example/streamflix/repository/AccountRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/AgeRatingRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EmployeeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/EpisodeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MediaRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/MovieRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/PreferenceRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ProfileRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/QualityTypeRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ReferralRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/RoleRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeasonRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SeriesRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java create mode 100644 src/main/java/com/example/streamflix/repository/WatchListRepository.java create mode 100644 src/main/java/com/example/streamflix/security/CustomUserDetails.java create mode 100644 src/main/java/com/example/streamflix/service/AccountUserDetailsService.java create mode 100644 src/main/java/com/example/streamflix/service/AgeRatingService.java create mode 100644 src/main/java/com/example/streamflix/service/ContentAccessService.java create mode 100644 src/main/java/com/example/streamflix/service/JwtService.java create mode 100644 src/main/java/com/example/streamflix/service/MediaService.java create mode 100644 src/main/java/com/example/streamflix/service/ProfileService.java create mode 100644 src/main/java/com/example/streamflix/service/ReferralService.java create mode 100644 src/main/java/com/example/streamflix/service/RegistrationService.java create mode 100644 src/main/java/com/example/streamflix/service/StreamingService.java create mode 100644 src/main/java/com/example/streamflix/service/SubscriptionService.java create mode 100644 src/main/java/com/example/streamflix/service/TmdbService.java create mode 100644 src/main/java/com/example/streamflix/service/ViewingProgressService.java create mode 100644 src/main/java/com/example/streamflix/service/WatchListService.java create mode 100644 src/main/java/com/example/streamflix/util/SecurityUtils.java create mode 100644 src/test/java/com/example/streamflix/StreamflixApplicationTests.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..257eda7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Backups (sensitive data) +backups/*.sql +backups/*.sql.gz +backups/*.sql.zip +backups/*.dump +!backups/.gitkeep + +target/ +.idea/ +src/main/resources +**/application.properties +application.properties \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b6cf00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Build stage +FROM eclipse-temurin:25-jdk-alpine AS build +WORKDIR /app + +# Install Maven manually since an official 'maven:25' image is not yet available +RUN apk add --no-cache maven + +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Runtime stage +FROM eclipse-temurin:25-jre-alpine +WORKDIR /app +COPY --from=build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..54672b6 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +## 🔄 Backup & Recovery + +### Create Backup +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +### Restore from Backup +**Windows (PowerShell):** +```powershell +.\scripts\restore.ps1 .\backups\streamflix_backup_YYYYMMDD_HHMMSS.sql.zip +``` + +**Linux/Mac (Bash):** +```bash +./scripts/restore.sh ./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz +``` + +For detailed backup and recovery procedures, see [BACKUP_RECOVERY.md](docs/BACKUP_RECOVERY.md). diff --git a/backups/.gitkeep b/backups/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a2d1a89 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres + container_name: streamflix-db + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: streamflix + ports: + - "5432:5432" + volumes: + - ./init-db:/docker-entrypoint-initdb.d + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U admin -d streamflix"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - streamflix-network + + api: + build: . + container_name: streamflix-api + + depends_on: + db: + condition: service_healthy + + restart: on-failure + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/streamflix + SPRING_DATASOURCE_USERNAME: admin + SPRING_DATASOURCE_PASSWORD: admin123 + networks: + - streamflix-network + +networks: + streamflix-network: + driver: bridge \ No newline at end of file diff --git a/docs/BACKUP_RECOVERY.md b/docs/BACKUP_RECOVERY.md new file mode 100644 index 0000000..6489d68 --- /dev/null +++ b/docs/BACKUP_RECOVERY.md @@ -0,0 +1,258 @@ +# StreamFlix - Backup and Recovery Protocol + +## Overview +This document outlines the backup and recovery procedures for the StreamFlix PostgreSQL database. Following these procedures ensures data protection and business continuity. + +--- + +## 🎯 Backup Strategy + +### Automated Backups +- **Frequency**: Daily at 2:00 AM UTC +- **Retention Policy**: + - Daily backups: 7 days + - Weekly backups: 4 weeks + - Monthly backups: 12 months +- **Storage Location**: `./backups/` directory +- **Backup Method**: PostgreSQL `pg_dump` (logical backup) + +### Manual Backup + +**Windows (PowerShell):** +```powershell +.\scripts\backup.ps1 +``` + +**Linux/Mac (Bash):** +```bash +./scripts/backup.sh +``` + +**Output**: +- Windows: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.zip` +- Linux/Mac: `./backups/streamflix_backup_YYYYMMDD_HHMMSS.sql.gz` + +### What is Backed Up +- All database schemas +- All table data +- Stored procedures and functions +- Triggers +- Views +- Sequences and indexes +- User permissions (within database) + +### What is NOT Backed Up +- Docker container configurations (use version control) +- Application code (use Git) +- Environment variables (document separately) +- Application logs + +--- + +## 🔄 Recovery Procedures + +### Full Database Restore + +**Prerequisites**: +- Docker containers must be running (`docker-compose up -d`) +- Backup file must exist in `./backups/` directory + +**Steps**: + +1. **Identify the backup file**: + Check the `./backups/` directory for the latest file. + +2. **Execute restore script**: + + **Windows (PowerShell):** + ```powershell + .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip + ``` + + **Linux/Mac (Bash):** + ```bash + ./scripts/restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz + ``` + +3. **Confirm restoration**: + - Type `yes` when prompted + - Wait for completion message + +4. **Verify data integrity**: +```bash + # Connect to database + docker exec -it streamflix-db psql -U admin streamflix + + # Check table counts + SELECT COUNT(*) FROM accounts; + SELECT COUNT(*) FROM media; + SELECT COUNT(*) FROM viewing_progress; + + # Exit + \q +``` + +5. **Test application**: + - Open http://localhost:8080/swagger-ui.html + - Try login endpoint + - Verify API functionality + +### Partial Recovery (Specific Table) + +If only specific tables need recovery, you will need to extract the SQL file from the archive first. + +**Windows Example:** +```powershell +# Extract +Expand-Archive .\backups\streamflix_backup_20240103_120000.sql.zip -DestinationPath .\temp_restore + +# Filter for specific table (requires manual editing of the SQL file usually, or advanced grep) +# It is often easier to restore to a temporary database and export the specific table. +``` + +--- + +## 🚨 Disaster Recovery Scenarios + +### Scenario 1: Accidental Data Deletion +**RTO**: < 30 minutes +**RPO**: < 24 hours (last daily backup) + +**Steps**: +1. Identify the last good backup before deletion +2. Run restore script +3. Verify data integrity +4. Resume operations + +### Scenario 2: Database Corruption +**RTO**: < 1 hour +**RPO**: < 24 hours + +**Steps**: +1. Stop all services: `docker-compose down` +2. Remove corrupted volume: `docker volume rm streamflix_db_data` +3. Restart services: `docker-compose up -d` +4. Wait for database to initialize +5. Run restore script with latest backup +6. Verify and resume operations + +### Scenario 3: Complete System Failure +**RTO**: < 2 hours +**RPO**: < 24 hours + +**Steps**: +1. Provision new infrastructure +2. Install Docker and Docker Compose +3. Clone repository: `git clone ` +4. Copy backup files to new system +5. Start containers: `docker-compose up -d` +6. Restore database using the restore script +7. Configure environment variables +8. Verify system health +9. Update DNS/routing if needed + +--- + +## 📊 Recovery Objectives + +| Metric | Target | Description | +|--------|--------|-------------| +| **RTO** (Recovery Time Objective) | < 1 hour | Maximum acceptable downtime | +| **RPO** (Recovery Point Objective) | < 24 hours | Maximum acceptable data loss | +| **Backup Window** | 2:00 AM - 2:30 AM UTC | Time when backups are performed | +| **Backup Success Rate** | > 99% | Target for successful backups | + +--- + +## 🔐 Security Considerations + +### Backup Security +- Backups contain sensitive user data (emails, passwords, personal info) +- **Never commit backup files to version control** +- Store backups in secure location with restricted access +- Consider encrypting backups for production. + +### Access Control +- Limit who can execute backup/restore scripts +- Audit all restore operations +- Maintain logs of backup/restore activities + +--- + +## 🧪 Testing Recovery + +**Monthly Recovery Test** (Recommended): + +1. Create test environment +2. Perform full restore +3. Verify all functionality +4. Document any issues +5. Update procedures if needed + +--- + +## 📝 Backup Monitoring + +### Verify Backup Success +Check that new files are appearing in the `backups/` folder daily and that their size is consistent. + +### Backup Health Indicators +- ✅ Backup file created daily +- ✅ File size is reasonable (similar to previous backups) +- ✅ No errors in Docker logs +- ⚠️ Missing backup = investigate immediately +- ⚠️ Drastically different file size = investigate + +--- + +## 🔧 Troubleshooting + +### Problem: Backup script fails +**Solution**: +```bash +# Check if container is running +docker ps | grep streamflix-db + +# Check Docker logs +docker logs streamflix-db + +# Verify disk space +df -h +``` + +### Problem: Restore fails with "database in use" +**Solution**: +The restore script attempts to stop the API container automatically. If it fails, manually stop any services connecting to the database: +```bash +docker-compose stop api +``` + +### Problem: Backup directory full +**Solution**: +The backup script automatically cleans up files older than the last 7 backups. If you need to clear more space, manually delete old files in the `backups/` directory. + +--- + +## 📞 Emergency Contacts + +- **Database Administrator**: [Your Name/Contact] +- **System Administrator**: [Contact] +- **On-Call Engineer**: [Contact] + +--- + +## 📅 Maintenance Schedule + +| Task | Frequency | Responsible | +|------|-----------|-------------| +| Verify automated backups | Daily | System | +| Test restore procedure | Monthly | DBA | +| Review backup logs | Weekly | DBA | +| Update recovery procedures | Quarterly | Team | +| Disaster recovery drill | Annually | Team | + +--- + +**Last Updated**: [Current Date] +**Document Version**: 1.1 +**Next Review Date**: [Date + 3 months] diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql new file mode 100644 index 0000000..e5b33aa --- /dev/null +++ b/init-db/03_schema.sql @@ -0,0 +1,291 @@ +-- 03_schema.sql +-- Purpose: Creates the database schema (tables) and inserts initial lookup data. +-- Execution Order: 1 (after default postgres init) + +-- Cleanup existing tables +DROP TABLE IF EXISTS employee, role CASCADE; +DROP TABLE IF EXISTS viewing_progress, watch_list CASCADE; +DROP TABLE IF EXISTS episode, season, series, movie, media_content_warning, media_available_quality, media CASCADE; +DROP TABLE IF EXISTS referral, invitation_status, subscription, subscription_tier CASCADE; +DROP TABLE IF EXISTS preference, profile CASCADE; +DROP TABLE IF EXISTS verification_token, accounts CASCADE; +DROP TABLE IF EXISTS content_warning, age_rating, quality_type CASCADE; + +-- Lookup table for video qualities (SD, HD, UHD) +CREATE TABLE quality_type ( + id SERIAL PRIMARY KEY, + name VARCHAR(10) NOT NULL UNIQUE +); + +-- Lookup table for age classifications (e.g., '12+', '18+') +CREATE TABLE age_rating ( + id SERIAL PRIMARY KEY, + label VARCHAR(20) NOT NULL, + min_age INT NOT NULL +); + +-- Lookup table for viewing guidelines (Violence, Fear, etc.) +CREATE TABLE content_warning ( + id SERIAL PRIMARY KEY, + description VARCHAR(50) NOT NULL +); + +-- Lookup table for invitation statuses +CREATE TABLE invitation_status ( + id SERIAL PRIMARY KEY, + status VARCHAR(20) NOT NULL UNIQUE +); + +-- Lookup table for internal employee roles +CREATE TABLE role ( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL UNIQUE +); + +-- Main user accounts +CREATE TABLE accounts ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_verified BOOLEAN DEFAULT FALSE, + failed_login_attempts INT DEFAULT 0, + is_blocked BOOLEAN DEFAULT FALSE, + discount_used BOOLEAN DEFAULT FALSE +); + +-- Verification and password recovery tokens +CREATE TABLE verification_token ( + id SERIAL PRIMARY KEY, + account_id INT REFERENCES accounts(id) ON DELETE CASCADE, + token VARCHAR(255) NOT NULL, + expiry_date TIMESTAMP NOT NULL, + token_type VARCHAR(50) NOT NULL -- 'EMAIL_VERIFICATION' or 'PASSWORD_RESET' +); + +-- User profiles within an account +CREATE TABLE profile ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + name VARCHAR(50) NOT NULL, + image_url VARCHAR(255), + birth_date DATE, + CONSTRAINT fk_profile_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE +); + +-- User-specific preferences +CREATE TABLE preference ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + preference_type VARCHAR(50) NOT NULL, -- e.g., 'GENRE', 'CONTENT_FILTER' + value VARCHAR(100) NOT NULL +); + +-- Subscription tiers (SD, HD, UHD) and their monthly prices +CREATE TABLE subscription_tier ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) NOT NULL, + price DECIMAL(10, 2) NOT NULL, + max_quality_id INT REFERENCES quality_type(id) +); + +-- Active subscriptions for accounts +CREATE TABLE subscription ( + id SERIAL PRIMARY KEY, + account_id INT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + tier_id INT NOT NULL REFERENCES subscription_tier(id), + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + end_date DATE, + is_trial BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE +); + +-- Referral system between users +CREATE TABLE referral ( + id SERIAL PRIMARY KEY, + inviter_account_id INT NOT NULL REFERENCES accounts(id), + invitee_account_id INT REFERENCES accounts(id), + status_id INT REFERENCES invitation_status(id), + invite_date DATE DEFAULT CURRENT_DATE, + discount_applied BOOLEAN DEFAULT FALSE +); + +-- Base table for all content (with dtype for JPA inheritance) +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + age_rating_id INT NOT NULL REFERENCES age_rating(id), + title VARCHAR(255) NOT NULL, + release_date DATE, + external_id VARCHAR(255), + dtype VARCHAR(31) NOT NULL -- Discriminator column for JPA inheritance (Movie/Series) +); + +-- Junction table for available qualities per title +CREATE TABLE media_available_quality ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + quality_type_id INT REFERENCES quality_type(id), + PRIMARY KEY (media_id, quality_type_id) +); + +-- Junction table for content warnings +CREATE TABLE media_content_warning ( + media_id INT REFERENCES media(id) ON DELETE CASCADE, + content_warning_id INT REFERENCES content_warning(id), + PRIMARY KEY (media_id, content_warning_id) +); + +-- Movie-specific data +CREATE TABLE movie ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL +); + +-- Series-specific data +CREATE TABLE series ( + media_id INT PRIMARY KEY REFERENCES media(id) ON DELETE CASCADE, + description TEXT +); + +-- Seasons belonging to a series +CREATE TABLE season ( + id SERIAL PRIMARY KEY, + series_id INT NOT NULL REFERENCES series(media_id) ON DELETE CASCADE, + season_number INT NOT NULL +); + +-- Episodes belonging to a season +CREATE TABLE episode ( + id SERIAL PRIMARY KEY, + season_id INT NOT NULL REFERENCES season(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + duration_seconds INT NOT NULL, + video_url VARCHAR(255) NOT NULL, + episode_number INT NOT NULL +); + +-- Tracking viewing history and "continue watching" +CREATE TABLE viewing_progress ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + movie_id INT REFERENCES movie(media_id), + episode_id INT REFERENCES episode(id), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + duration_watched_seconds INT DEFAULT 0, + last_position_seconds INT DEFAULT 0, + is_finished BOOLEAN DEFAULT FALSE, + CHECK (movie_id IS NOT NULL OR episode_id IS NOT NULL) +); + +-- Personal watch lists for profiles +CREATE TABLE watch_list ( + id SERIAL PRIMARY KEY, + profile_id INT NOT NULL REFERENCES profile(id) ON DELETE CASCADE, + media_id INT NOT NULL REFERENCES media(id) ON DELETE CASCADE, + added_date DATE DEFAULT CURRENT_DATE +); + +-- Internal staff management +CREATE TABLE employee ( + id SERIAL PRIMARY KEY, + role_id INT NOT NULL REFERENCES role(id), + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL +); + +-- Insert initial lookup data +-- Age Ratings +INSERT INTO age_rating (label, min_age) VALUES + ('AL', 0), + ('6+', 6), + ('9+', 9), + ('12+', 12), + ('14+', 14), + ('16+', 16), + ('18+', 18); + +-- Content Warnings +INSERT INTO content_warning (description) VALUES + ('Violence'), + ('Fear'), + ('Sex'), + ('Discrimination'), + ('Drug Abuse'), + ('Coarse Language'); + +-- Playback Qualities +INSERT INTO quality_type (name) VALUES + ('SD'), + ('HD'), + ('UHD'); + +-- Subscription Tiers +INSERT INTO subscription_tier (name, price, max_quality_id) VALUES + ('SD Subscription', 7.99, (SELECT id FROM quality_type WHERE name = 'SD')), + ('HD Subscription', 10.99, (SELECT id FROM quality_type WHERE name = 'HD')), + ('UHD Subscription', 13.99, (SELECT id FROM quality_type WHERE name = 'UHD')); + +-- Internal Employee Roles +INSERT INTO role (name) VALUES + ('Junior'), + ('Mid-level'), + ('Senior'); + +-- Invitation Statuses +INSERT INTO invitation_status (status) VALUES + ('Pending'), + ('Accepted'), + ('Expired'); + +-- Sample test data for movies (with TMDB external IDs) +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (1, 'The Shawshank Redemption', '1994-09-23', '278', 'Movie'), + (1, 'Inception', '2010-07-16', '27205', 'Movie'), + (3, 'The Dark Knight', '2008-07-18', '155', 'Movie'), + (5, 'Pulp Fiction', '1994-10-14', '680', 'Movie'), + (1, 'Forrest Gump', '1994-07-06', '13', 'Movie'), + (3, 'The Matrix', '1999-03-31', '603', 'Movie'); + +-- Insert corresponding movie records +INSERT INTO movie (media_id, duration_seconds, video_url) VALUES + (1, 8520, 'https://streamflix.example.com/movies/shawshank.mp4'), + (2, 8880, 'https://streamflix.example.com/movies/inception.mp4'), + (3, 9120, 'https://streamflix.example.com/movies/dark-knight.mp4'), + (4, 9240, 'https://streamflix.example.com/movies/pulp-fiction.mp4'), + (5, 8520, 'https://streamflix.example.com/movies/forrest-gump.mp4'), + (6, 8160, 'https://streamflix.example.com/movies/matrix.mp4'); + +-- Sample test data for a series +INSERT INTO media (age_rating_id, title, release_date, external_id, dtype) VALUES + (4, 'Breaking Bad', '2008-01-20', '1396', 'Series'); + +INSERT INTO series (media_id, description) VALUES + (7, 'A high school chemistry teacher turned methamphetamine manufacturer partners with a former student.'); + +-- Add seasons for Breaking Bad +INSERT INTO season (series_id, season_number) VALUES + (7, 1), + (7, 2); + +-- Add sample episodes +INSERT INTO episode (season_id, title, duration_seconds, video_url, episode_number) VALUES + (1, 'Pilot', 3480, 'https://streamflix.example.com/series/breaking-bad/s01e01.mp4', 1), + (1, 'Cat''s in the Bag...', 2880, 'https://streamflix.example.com/series/breaking-bad/s01e02.mp4', 2), + (2, 'Seven Thirty-Seven', 2820, 'https://streamflix.example.com/series/breaking-bad/s02e01.mp4', 1); + +-- Add available qualities for media +INSERT INTO media_available_quality (media_id, quality_type_id) VALUES + (1, 1), (1, 2), (1, 3), -- Shawshank: SD, HD, UHD + (2, 2), (2, 3), -- Inception: HD, UHD + (3, 1), (3, 2), (3, 3), -- Dark Knight: SD, HD, UHD + (4, 2), (4, 3), -- Pulp Fiction: HD, UHD + (5, 1), (5, 2), -- Forrest Gump: SD, HD + (6, 1), (6, 2), (6, 3), -- Matrix: SD, HD, UHD + (7, 2), (7, 3); -- Breaking Bad: HD, UHD + +-- Add content warnings for media +INSERT INTO media_content_warning (media_id, content_warning_id) VALUES + (3, 1), (3, 2), -- Dark Knight: Violence, Fear + (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language + (6, 1), (6, 2), -- Matrix: Violence, Fear + (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language diff --git a/init-db/04_referral_logic.sql b/init-db/04_referral_logic.sql new file mode 100644 index 0000000..6233786 --- /dev/null +++ b/init-db/04_referral_logic.sql @@ -0,0 +1,80 @@ +-- 04_referral_logic.sql +-- Purpose: Creates stored procedures and triggers for the referral system logic. +-- Execution Order: 2 (after schema creation) + +-- Stored Procedure to apply referral discounts +-- This procedure checks if both the inviter and invitee are eligible for a discount +-- and updates their account status and the referral record accordingly. +CREATE OR REPLACE PROCEDURE apply_referral_discount(p_referral_id INT) +LANGUAGE plpgsql +AS $$ +DECLARE + v_inviter_id INT; + v_invitee_id INT; + v_inviter_discount_used BOOLEAN; + v_invitee_discount_used BOOLEAN; +BEGIN + -- Retrieve inviter and invitee IDs from the referral record + SELECT inviter_account_id, invitee_account_id + INTO v_inviter_id, v_invitee_id + FROM referral + WHERE id = p_referral_id; + + -- Check current discount status for both accounts + SELECT discount_used INTO v_inviter_discount_used FROM accounts WHERE id = v_inviter_id; + SELECT discount_used INTO v_invitee_discount_used FROM accounts WHERE id = v_invitee_id; + + -- Apply discount only if neither account has used a discount yet + IF v_inviter_discount_used = FALSE AND v_invitee_discount_used = FALSE THEN + -- Update inviter account + UPDATE accounts SET discount_used = TRUE WHERE id = v_inviter_id; + + -- Update invitee account + UPDATE accounts SET discount_used = TRUE WHERE id = v_invitee_id; + + -- Mark the referral as having the discount applied + UPDATE referral SET discount_applied = TRUE WHERE id = p_referral_id; + + RAISE NOTICE 'Referral discount successfully applied for Referral ID: %', p_referral_id; + ELSE + RAISE NOTICE 'Discount not applied. One or both accounts have already used a discount.'; + END IF; +END; +$$; + +-- Trigger Function to handle subscription changes +-- This function is called by the trigger to check if a subscription update warrants a referral discount. +CREATE OR REPLACE FUNCTION check_referral_on_subscription_change() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +DECLARE + v_referral_id INT; +BEGIN + -- Check if the subscription is now Active and NOT a Trial + -- This handles both new subscriptions (INSERT) and updates (UPDATE) + IF NEW.is_active = TRUE AND NEW.is_trial = FALSE THEN + + -- Find if the account owner was an invitee in a pending referral + SELECT id INTO v_referral_id + FROM referral + WHERE invitee_account_id = NEW.account_id + AND discount_applied = FALSE + LIMIT 1; + + -- If a qualifying referral exists, apply the discount + IF v_referral_id IS NOT NULL THEN + CALL apply_referral_discount(v_referral_id); + END IF; + END IF; + + RETURN NEW; +END; +$$; + +-- Trigger on the subscription table +-- Fires after a row is inserted or updated to check for referral completion +CREATE TRIGGER trg_apply_discount_on_paid_subscription +AFTER INSERT OR UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION check_referral_on_subscription_change(); diff --git a/init-db/05_employee_views.sql b/init-db/05_employee_views.sql new file mode 100644 index 0000000..cd77cd2 --- /dev/null +++ b/init-db/05_employee_views.sql @@ -0,0 +1,59 @@ +-- 05_employee_views.sql +-- Purpose: Creates database views for different employee roles (Junior, Mid-level, Senior). +-- Execution Order: 3 (after schema and logic creation) + +-- View for Junior Employees +-- Junior employees can only see basic account information for support purposes. +-- They do NOT have access to passwords, login attempts, or any financial/subscription data. +CREATE OR REPLACE VIEW view_junior_accounts AS +SELECT + id AS account_id, + email, + is_verified, + is_blocked +FROM + accounts; + +-- View for Mid-level Employees +-- Mid-level employees can see profile details and account status to help with content issues. +-- They can see which profiles belong to which account and their age ratings. +-- They still do NOT have access to financial data (subscriptions, prices) or passwords. +CREATE OR REPLACE VIEW view_midlevel_profiles AS +SELECT + p.id AS profile_id, + p.name AS profile_name, + a.id AS account_id, + a.email AS account_email, + ar.label AS age_rating_label, + a.is_verified, + a.is_blocked +FROM + profile p + JOIN + accounts a ON p.account_id = a.id + JOIN + age_rating ar ON p.age_rating_id = ar.id; + +-- View for Senior Employees +-- Senior employees have full visibility into accounts and their subscription status. +-- This includes financial data like subscription tiers, prices, and discount usage. +-- This view joins accounts with subscription details to show the complete customer picture. +CREATE OR REPLACE VIEW view_senior_full_access AS +SELECT + a.id AS account_id, + a.email, + a.is_verified, + a.is_blocked, + a.discount_used, + st.name AS subscription_tier_name, + st.price AS subscription_price, + s.is_active, + s.start_date, + s.end_date, + s.is_trial +FROM + accounts a + LEFT JOIN + subscription s ON a.id = s.account_id + LEFT JOIN + subscription_tier st ON s.tier_id = st.id; diff --git a/init-db/06_additional_triggers.sql b/init-db/06_additional_triggers.sql new file mode 100644 index 0000000..b791d43 --- /dev/null +++ b/init-db/06_additional_triggers.sql @@ -0,0 +1,127 @@ +-- 06_additional_triggers.sql +-- Purpose: Adds triggers for automatic watchlist management +-- Execution Order: After 05_employee_views.sql + +-- Function to automatically remove media from watchlist when finished +CREATE OR REPLACE FUNCTION auto_remove_finished_from_watchlist() +RETURNS TRIGGER AS $$ +DECLARE + v_series_id INT; + v_has_unfinished_episodes BOOLEAN; +BEGIN + -- Only proceed if the viewing progress is marked as finished + IF NEW.is_finished = TRUE THEN + + -- CASE 1: It's a Movie + IF NEW.movie_id IS NOT NULL THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.movie_id; + + -- CASE 2: It's an Episode + ELSIF NEW.episode_id IS NOT NULL THEN + -- Get the series_id for this episode + -- episode -> season -> series + SELECT s.series_id INTO v_series_id + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE e.id = NEW.episode_id; + + -- Check if there are any episodes in this series that are NOT finished for this profile + -- An episode is unfinished if: + -- 1. It exists in the series + -- 2. AND (There is no viewing_progress OR viewing_progress.is_finished is FALSE) + + SELECT EXISTS ( + SELECT 1 + FROM episode e + JOIN season s ON e.season_id = s.id + WHERE s.series_id = v_series_id + AND NOT EXISTS ( + SELECT 1 + FROM viewing_progress vp + WHERE vp.episode_id = e.id + AND vp.profile_id = NEW.profile_id + AND vp.is_finished = TRUE + ) + ) INTO v_has_unfinished_episodes; + + -- If no unfinished episodes found (meaning ALL are finished), remove series from watchlist + IF v_has_unfinished_episodes = FALSE THEN + DELETE FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = v_series_id; + END IF; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_auto_remove_from_watchlist ON viewing_progress; + +CREATE TRIGGER trg_auto_remove_from_watchlist +AFTER INSERT OR UPDATE ON viewing_progress +FOR EACH ROW +EXECUTE FUNCTION auto_remove_finished_from_watchlist(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Auto-update Subscription End Date +-- Purpose: Automatically sets end_date to CURRENT_DATE when a subscription is cancelled +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION update_subscription_end_date() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if subscription is being cancelled (is_active changing from TRUE to FALSE) + IF OLD.is_active = TRUE AND NEW.is_active = FALSE THEN + -- Only update end_date if it's not already set to today + -- This handles cases where end_date might be NULL or set to a future date + IF NEW.end_date IS NULL OR NEW.end_date != CURRENT_DATE THEN + NEW.end_date := CURRENT_DATE; + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_update_subscription_end_date ON subscription; + +CREATE TRIGGER trg_update_subscription_end_date +BEFORE UPDATE ON subscription +FOR EACH ROW +EXECUTE FUNCTION update_subscription_end_date(); + +-- -------------------------------------------------------------------------------------- +-- TRIGGER: Prevent Duplicate Watchlist Entries +-- Purpose: Prevents duplicate entries in the watch_list table at the database level +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE FUNCTION prevent_duplicate_watchlist() +RETURNS TRIGGER AS $$ +BEGIN + -- Check if a record already EXISTS with the same profile_id AND media_id + IF EXISTS ( + SELECT 1 + FROM watch_list + WHERE profile_id = NEW.profile_id + AND media_id = NEW.media_id + ) THEN + RAISE EXCEPTION 'Media already in watchlist for this profile'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Create the trigger +DROP TRIGGER IF EXISTS trg_prevent_duplicate_watchlist ON watch_list; + +CREATE TRIGGER trg_prevent_duplicate_watchlist +BEFORE INSERT ON watch_list +FOR EACH ROW +EXECUTE FUNCTION prevent_duplicate_watchlist(); diff --git a/init-db/07_stored_procedures.sql b/init-db/07_stored_procedures.sql new file mode 100644 index 0000000..a46e04e --- /dev/null +++ b/init-db/07_stored_procedures.sql @@ -0,0 +1,157 @@ +-- 07_stored_procedures.sql +-- Purpose: Adds stored procedures and functions for analytics and reporting +-- Execution Order: After 06_additional_triggers.sql + +-- MAINTENANCE PROCEDURES +-- These procedures should be run periodically for database maintenance +-- - clean_expired_tokens(): Remove expired verification tokens (recommended: daily) + +-- Function to get viewing statistics for a specific profile +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_profile_viewing_stats(p_profile_id BIGINT) +RETURNS TABLE ( + total_movies_watched BIGINT, + total_episodes_watched BIGINT, + total_watch_time_hours NUMERIC, + unique_content_watched BIGINT, + finished_content_count BIGINT +) AS $$ +BEGIN + RETURN QUERY + WITH stats AS ( + SELECT + -- Count distinct movies watched + COUNT(DISTINCT vp.movie_id) FILTER (WHERE vp.movie_id IS NOT NULL) AS movies_count, + + -- Count distinct episodes watched + COUNT(DISTINCT vp.episode_id) FILTER (WHERE vp.episode_id IS NOT NULL) AS episodes_count, + + -- Sum duration watched in seconds and convert to hours + COALESCE(SUM(vp.duration_watched_seconds), 0) / 3600.0 AS total_hours, + + -- Count finished content + COUNT(*) FILTER (WHERE vp.is_finished = TRUE) AS finished_count + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id + ), + unique_media AS ( + -- Get unique media IDs (movies directly, series via episodes) + SELECT COUNT(DISTINCT media_id) AS unique_count + FROM ( + -- Movies + SELECT vp.movie_id AS media_id + FROM viewing_progress vp + WHERE vp.profile_id = p_profile_id AND vp.movie_id IS NOT NULL + + UNION + + -- Series (via episodes) + SELECT s.series_id AS media_id + FROM viewing_progress vp + JOIN episode e ON vp.episode_id = e.id + JOIN season s ON e.season_id = s.id + WHERE vp.profile_id = p_profile_id AND vp.episode_id IS NOT NULL + ) distinct_content + ) + SELECT + COALESCE(s.movies_count, 0)::BIGINT, + COALESCE(s.episodes_count, 0)::BIGINT, + ROUND(COALESCE(s.total_hours, 0), 2), + COALESCE(u.unique_count, 0)::BIGINT, + COALESCE(s.finished_count, 0)::BIGINT + FROM stats s, unique_media u; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- FUNCTION: Get Popular Content +-- Purpose: Returns the most popular content based on viewing statistics +-- -------------------------------------------------------------------------------------- + +DROP FUNCTION IF EXISTS get_popular_content(INT); +DROP FUNCTION IF EXISTS get_popular_content(BIGINT); + +-- Updated to accept BIGINT to match Java Long type +CREATE OR REPLACE FUNCTION get_popular_content(p_limit BIGINT DEFAULT 10) +RETURNS TABLE ( + media_id INT, + title VARCHAR, + media_type VARCHAR, + view_count BIGINT, + unique_viewers BIGINT, + completion_rate NUMERIC, + avg_watch_time_minutes NUMERIC +) AS $$ +BEGIN + RETURN QUERY + WITH content_stats AS ( + -- Movies + SELECT + m.media_id, + med.title, + 'Movie'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM movie m + JOIN media med ON m.media_id = med.id + JOIN viewing_progress vp ON m.media_id = vp.movie_id + GROUP BY m.media_id, med.title + + UNION ALL + + -- Series (aggregated by series) + SELECT + s.media_id, + med.title, + 'Series'::VARCHAR AS media_type, + COUNT(vp.id) AS total_views, + COUNT(DISTINCT vp.profile_id) AS distinct_viewers, + COUNT(vp.id) FILTER (WHERE vp.is_finished = TRUE) AS finished_views, + AVG(vp.duration_watched_seconds) AS avg_duration + FROM series s + JOIN media med ON s.media_id = med.id + JOIN season sea ON s.media_id = sea.series_id + JOIN episode e ON sea.id = e.season_id + JOIN viewing_progress vp ON e.id = vp.episode_id + GROUP BY s.media_id, med.title + ) + SELECT + cs.media_id, + cs.title, + cs.media_type, + cs.total_views::BIGINT, + cs.distinct_viewers::BIGINT, + ROUND( + CASE + WHEN cs.total_views > 0 THEN (cs.finished_views::NUMERIC / cs.total_views::NUMERIC) * 100.0 + ELSE 0 + END, 2 + ) AS completion_rate, + ROUND((cs.avg_duration / 60.0)::NUMERIC, 2) AS avg_watch_time_minutes + FROM content_stats cs + ORDER BY cs.total_views DESC + LIMIT p_limit; +END; +$$ LANGUAGE plpgsql; + +-- -------------------------------------------------------------------------------------- +-- PROCEDURE: Clean Expired Tokens +-- Purpose: Removes expired verification tokens from the database +-- -------------------------------------------------------------------------------------- + +CREATE OR REPLACE PROCEDURE clean_expired_tokens() +LANGUAGE plpgsql +AS $$ +DECLARE + deleted_count INT; +BEGIN + DELETE FROM verification_token + WHERE expiry_date < NOW(); + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RAISE NOTICE 'Cleaned up % expired verification tokens', deleted_count; +END; +$$; diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1d87747 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.1 + + + com.example + streamflix + 0.0.1-SNAPSHOT + streamflix + streamflix + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.5 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/scripts/backup.ps1 b/scripts/backup.ps1 new file mode 100644 index 0000000..a05412a --- /dev/null +++ b/scripts/backup.ps1 @@ -0,0 +1,56 @@ +# StreamFlix Database Backup Script (Windows) +# This script creates a compressed backup of the PostgreSQL database using PowerShell + +$ErrorActionPreference = "Stop" + +# Configuration +$BackupDir = ".\backups" +$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$BackupFile = "$BackupDir\streamflix_backup_$Timestamp.sql" +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Create backup directory if it doesn't exist +if (-not (Test-Path -Path $BackupDir)) { + New-Item -ItemType Directory -Path $BackupDir | Out-Null +} + +Write-Host "Starting database backup..." +Write-Host "Timestamp: $Timestamp" + +try { + # Method: Generate dump inside container then copy out to avoid PowerShell encoding issues with piping + Write-Host "Generating dump inside container..." + docker exec $ContainerName pg_dump -U $DbUser $DbName -f /tmp/temp_backup.sql + + if ($LASTEXITCODE -ne 0) { throw "pg_dump failed" } + + Write-Host "Copying backup to host..." + docker cp "$($ContainerName):/tmp/temp_backup.sql" $BackupFile + + # Clean up inside container + docker exec $ContainerName rm /tmp/temp_backup.sql + + # Compress the backup (Zip) + $ZipFile = "$BackupFile.zip" + Compress-Archive -Path $BackupFile -DestinationPath $ZipFile + Remove-Item $BackupFile + + $Size = (Get-Item $ZipFile).Length / 1MB + Write-Host "[SUCCESS] Backup completed successfully" -ForegroundColor Green + Write-Host "Backup file: $ZipFile" + Write-Host ("Backup size: {0:N2} MB" -f $Size) + + # Keep only the last 7 backups + Get-ChildItem -Path $BackupDir -Filter "streamflix_backup_*.sql.zip" | + Sort-Object CreationTime -Descending | + Select-Object -Skip 7 | + Remove-Item + + Write-Host "Cleaned up old backups (keeping last 7)" + +} catch { + Write-Host "[ERROR] Backup failed: $_" -ForegroundColor Red + exit 1 +} diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..3420cd8 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# StreamFlix Database Backup Script +# This script creates a compressed backup of the PostgreSQL database + +set -e # Exit on error + +# Configuration +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/streamflix_backup_$TIMESTAMP.sql" +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Create backup directory if it doesn't exist +mkdir -p "$BACKUP_DIR" + +echo "Starting database backup..." +echo "Timestamp: $TIMESTAMP" + +# Execute pg_dump inside the Docker container +if docker exec $CONTAINER_NAME pg_dump -U $DB_USER $DB_NAME > "$BACKUP_FILE"; then + # Compress the backup + gzip "$BACKUP_FILE" + + BACKUP_SIZE=$(du -h "$BACKUP_FILE.gz" | cut -f1) + echo -e "${GREEN}✓ Backup completed successfully${NC}" + echo "Backup file: $BACKUP_FILE.gz" + echo "Backup size: $BACKUP_SIZE" + + # Keep only the last 7 backups (optional cleanup) + cd "$BACKUP_DIR" + ls -t streamflix_backup_*.sql.gz | tail -n +8 | xargs -r rm + echo "Cleaned up old backups (keeping last 7)" +else + echo -e "${RED}✗ Backup failed${NC}" + exit 1 +fi + +echo "Backup process completed at $(date)" diff --git a/scripts/restore.ps1 b/scripts/restore.ps1 new file mode 100644 index 0000000..49e2eb9 --- /dev/null +++ b/scripts/restore.ps1 @@ -0,0 +1,92 @@ +# StreamFlix Database Restore Script (Windows) +# This script restores the PostgreSQL database from a backup file + +$ErrorActionPreference = "Stop" + +# Configuration +$ContainerName = "streamflix-db" +$DbName = "streamflix" +$DbUser = "admin" + +# Check if backup file is provided +if ($args.Count -eq 0) { + Write-Host "[ERROR] No backup file specified" -ForegroundColor Red + Write-Host "Usage: .\scripts\restore.ps1 " + Write-Host "Example: .\scripts\restore.ps1 .\backups\streamflix_backup_20240103_120000.sql.zip" + exit 1 +} + +$BackupFile = $args[0] + +# Check if file exists +if (-not (Test-Path -Path $BackupFile)) { + Write-Host "[ERROR] Backup file not found: $BackupFile" -ForegroundColor Red + exit 1 +} + +Write-Host "WARNING: This will overwrite the current database!" -ForegroundColor Yellow +Write-Host "Backup file: $BackupFile" +$confirmation = Read-Host "Are you sure you want to continue? (yes/no)" +if ($confirmation -notmatch "^[Yy][Ee][Ss]$") { + Write-Host "Restore cancelled" + exit 0 +} + +Write-Host "Starting database restore..." + +try { + $RestoreFile = $BackupFile + $TempDir = $null + + # Decompress if zipped + if ($BackupFile.EndsWith(".zip")) { + Write-Host "Decompressing backup file..." + $TempDir = Join-Path $env:TEMP "streamflix_restore_$(Get-Date -Format 'yyyyMMddHHmmss')" + New-Item -ItemType Directory -Path $TempDir | Out-Null + + Expand-Archive -Path $BackupFile -DestinationPath $TempDir -Force + + # Find the .sql file inside + $SqlFile = Get-ChildItem -Path $TempDir -Filter "*.sql" | Select-Object -First 1 + if (-not $SqlFile) { + throw "No .sql file found in the archive" + } + $RestoreFile = $SqlFile.FullName + } + + # Stop API container + Write-Host "Stopping API container..." + docker-compose stop api + + # Copy file to container + Write-Host "Copying dump to container..." + docker cp "$RestoreFile" "$($ContainerName):/tmp/restore.sql" + + # Restore + Write-Host "Restoring database..." + # Using bash -c to handle redirection inside the container + docker exec $ContainerName bash -c "psql -U $DbUser $DbName < /tmp/restore.sql" + + if ($LASTEXITCODE -ne 0) { throw "psql restore failed" } + + # Cleanup container file + docker exec $ContainerName rm /tmp/restore.sql + + Write-Host "[SUCCESS] Database restored successfully" -ForegroundColor Green + + # Restart API + Write-Host "Restarting API container..." + docker-compose start api + +} catch { + Write-Host "[ERROR] Restore failed: $_" -ForegroundColor Red + + # Try to restart API anyway + docker-compose start api + exit 1 +} finally { + # Cleanup temp dir if it exists + if ($TempDir -and (Test-Path $TempDir)) { + Remove-Item -Path $TempDir -Recurse -Force + } +} diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..85f9c5b --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# StreamFlix Database Restore Script +# This script restores the PostgreSQL database from a backup file + +set -e # Exit on error + +# Configuration +CONTAINER_NAME="streamflix-db" +DB_NAME="streamflix" +DB_USER="admin" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if backup file is provided +if [ -z "$1" ]; then + echo -e "${RED}Error: No backup file specified${NC}" + echo "Usage: ./restore.sh " + echo "Example: ./restore.sh ./backups/streamflix_backup_20240103_120000.sql.gz" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check if file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found: $BACKUP_FILE${NC}" + exit 1 +fi + +echo -e "${YELLOW}WARNING: This will overwrite the current database!${NC}" +echo "Backup file: $BACKUP_FILE" +read -p "Are you sure you want to continue? (yes/no): " -r +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "Restore cancelled" + exit 0 +fi + +echo "Starting database restore..." + +# Decompress if gzipped +if [[ "$BACKUP_FILE" == *.gz ]]; then + echo "Decompressing backup file..." + TEMP_FILE="${BACKUP_FILE%.gz}" + gunzip -c "$BACKUP_FILE" > "$TEMP_FILE" + RESTORE_FILE="$TEMP_FILE" +else + RESTORE_FILE="$BACKUP_FILE" +fi + +# Stop API container to prevent connections during restore +echo "Stopping API container..." +docker-compose stop api || true + +# Drop existing connections and restore +echo "Restoring database..." +if cat "$RESTORE_FILE" | docker exec -i $CONTAINER_NAME psql -U $DB_USER $DB_NAME; then + echo -e "${GREEN}✓ Database restored successfully${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]]; then + rm "$RESTORE_FILE" + fi + + # Restart API container + echo "Restarting API container..." + docker-compose start api + + echo -e "${GREEN}Restore completed successfully at $(date)${NC}" +else + echo -e "${RED}✗ Restore failed${NC}" + + # Clean up temp file if created + if [[ "$BACKUP_FILE" == *.gz ]] && [ -f "$RESTORE_FILE" ]; then + rm "$RESTORE_FILE" + fi + + # Try to restart API anyway + docker-compose start api + exit 1 +fi diff --git a/src/main/java/com/example/streamflix/StreamflixApplication.java b/src/main/java/com/example/streamflix/StreamflixApplication.java new file mode 100644 index 0000000..736387f --- /dev/null +++ b/src/main/java/com/example/streamflix/StreamflixApplication.java @@ -0,0 +1,16 @@ +package com.example.streamflix; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "StreamFlix API", version = "1.0", description = "API documentation for StreamFlix application")) +public class StreamflixApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamflixApplication.class, args); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..4bf5612 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.example.streamflix.config; + +import com.example.streamflix.service.AccountUserDetailsService; +import com.example.streamflix.service.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final AccountUserDetailsService userDetailsService; + + public JwtAuthenticationFilter(JwtService jwtService, AccountUserDetailsService userDetailsService) { + this.jwtService = jwtService; + this.userDetailsService = userDetailsService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtService.extractEmail(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(email); + + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/config/ScheduledTasks.java b/src/main/java/com/example/streamflix/config/ScheduledTasks.java new file mode 100644 index 0000000..c364739 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/ScheduledTasks.java @@ -0,0 +1,37 @@ +package com.example.streamflix.config; + +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +public class ScheduledTasks { + + private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class); + private final EntityManager entityManager; + + public ScheduledTasks(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Runs daily at 2 AM to clean up expired verification tokens + */ + @Scheduled(cron = "0 0 2 * * *") + @Transactional + public void cleanupExpiredTokens() { + try { + logger.info("Starting scheduled cleanup of expired verification tokens"); + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + logger.info("Completed scheduled cleanup of expired verification tokens"); + } catch (Exception e) { + logger.error("Error during scheduled token cleanup: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java new file mode 100644 index 0000000..065bc3b --- /dev/null +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com.example.streamflix.config; + +import io.netty.handler.codec.http.HttpMethod; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() + .requestMatchers("/api/v1/media/createNewMedia").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers("/api/v1/analytics/admin/**").authenticated() + .requestMatchers("/api/v1/analytics/**").permitAll() + .requestMatchers("/api/v1/**").authenticated() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } +} diff --git a/src/main/java/com/example/streamflix/config/WebClientConfig.java b/src/main/java/com/example/streamflix/config/WebClientConfig.java new file mode 100644 index 0000000..0949ab4 --- /dev/null +++ b/src/main/java/com/example/streamflix/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.example.streamflix.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/example/streamflix/controller/AnalyticsController.java b/src/main/java/com/example/streamflix/controller/AnalyticsController.java new file mode 100644 index 0000000..eb3f9bc --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AnalyticsController.java @@ -0,0 +1,105 @@ +package com.example.streamflix.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/analytics") +@Tag(name = "Analytics", description = "Analytics and statistics endpoints") +public class AnalyticsController { + + private final EntityManager entityManager; + + public AnalyticsController(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Operation(summary = "Get popular content", + description = "Returns the most popular content based on view count and engagement") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved popular content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "400", description = "Invalid limit parameter") + }) + @GetMapping("/popular") + public ResponseEntity getPopularContent( + @RequestParam(defaultValue = "10") @Parameter(description = "Number of results to return") Integer limit + ) { + if (limit < 1 || limit > 100) { + return ResponseEntity.badRequest().body("Limit must be between 1 and 100"); + } + + try { + // Explicitly cast the parameter to BIGINT to match the PostgreSQL function signature + Query query = entityManager.createNativeQuery( + "SELECT * FROM get_popular_content(CAST(:limit AS BIGINT))", Tuple.class); + query.setParameter("limit", limit); + + List results = query.getResultList(); + + List> popularContent = results.stream() + .map(tuple -> { + Map item = new HashMap<>(); + item.put("mediaId", tuple.get("media_id")); + item.put("title", tuple.get("title")); + item.put("mediaType", tuple.get("media_type")); + item.put("viewCount", tuple.get("view_count")); + item.put("uniqueViewers", tuple.get("unique_viewers")); + item.put("completionRate", tuple.get("completion_rate")); + item.put("avgWatchTimeMinutes", tuple.get("avg_watch_time_minutes")); + return item; + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(popularContent); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error retrieving popular content: " + e.getMessage()); + } + } + + @Operation(summary = "Clean expired verification tokens", + description = "Admin endpoint to remove expired verification tokens from database") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cleaned expired tokens", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "500", description = "Error during cleanup") + }) + @PostMapping("/admin/cleanup-tokens") + public ResponseEntity cleanupExpiredTokens() { + try { + entityManager.createNativeQuery("CALL clean_expired_tokens()") + .executeUpdate(); + + Map response = new HashMap<>(); + response.put("message", "Expired tokens cleaned successfully"); + response.put("timestamp", LocalDateTime.now().toString()); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put("error", "Failed to clean expired tokens"); + errorResponse.put("details", e.getMessage()); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/AuthController.java b/src/main/java/com/example/streamflix/controller/AuthController.java new file mode 100644 index 0000000..beeb19f --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/AuthController.java @@ -0,0 +1,221 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.model.ForgotPasswordRequest; +import com.example.streamflix.model.LoginRequest; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.model.ResetPasswordRequest; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import com.example.streamflix.service.JwtService; +import com.example.streamflix.service.RegistrationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "Authentication", description = "Operations related to user authentication and registration") +public class AuthController { + + private final RegistrationService registrationService; + private final AuthenticationManager authenticationManager; + private final JwtService jwtService; + private final AccountRepository accountRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final PasswordEncoder passwordEncoder; + + public AuthController(RegistrationService registrationService, + AuthenticationManager authenticationManager, + JwtService jwtService, + AccountRepository accountRepository, + VerificationTokenRepository verificationTokenRepository, + PasswordEncoder passwordEncoder) { + this.registrationService = registrationService; + this.authenticationManager = authenticationManager; + this.jwtService = jwtService; + this.accountRepository = accountRepository; + this.verificationTokenRepository = verificationTokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Operation(summary = "Register a new user", description = "Register a new user with email and password") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "User registered successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data or email already exists") + }) + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + registrationService.register(request); + return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully"); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Login user", description = "Authenticate user and return JWT token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account not verified"), + @ApiResponse(responseCode = "403", description = "Account is blocked") + }) + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + if (account.isBlocked()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Account is temporarily blocked due to multiple failed login attempts"); + } + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) + ); + + if (authentication.isAuthenticated()) { + Account account = accountRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new RuntimeException("Account not found")); + + // Reset failed login attempts on successful login + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + String token = jwtService.generateToken(account.getEmail(), account.getId()); + + Map response = new HashMap<>(); + response.put("token", token); + response.put("email", account.getEmail()); + response.put("accountId", account.getId().toString()); + + return ResponseEntity.ok(response); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } + } catch (DisabledException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Account is not verified. Please verify your email."); + } catch (AuthenticationException e) { + if (accountOptional.isPresent()) { + Account account = accountOptional.get(); + int attempts = account.getFailedLoginAttempts() + 1; + account.setFailedLoginAttempts(attempts); + if (attempts >= 3) { + account.setBlocked(true); + } + accountRepository.save(account); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred"); + } + } + + @Operation(summary = "Verify email", description = "Verify user email using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email verified successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token"), + @ApiResponse(responseCode = "404", description = "Token not found") + }) + @GetMapping("/verify") + public ResponseEntity verifyAccount(@Parameter(description = "Verification token") @RequestParam("token") String token) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(token); + + if (verificationToken == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Token not found"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setVerified(true); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Email verified successfully"); + } + + @Operation(summary = "Forgot password", description = "Initiate password reset process") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password reset link has been sent"), + @ApiResponse(responseCode = "404", description = "Account not found") + }) + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { + Optional accountOptional = accountRepository.findByEmail(request.getEmail()); + if (accountOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Account not found"); + } + + Account account = accountOptional.get(); + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(1), + TokenType.PASSWORD_RESET + ); + verificationTokenRepository.save(verificationToken); + + // In a real application, you would send an email with the token here + // For now, we just return a success message + + return ResponseEntity.ok("Password reset link has been sent"); + } + + @Operation(summary = "Reset password", description = "Reset user password using a token") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Password has been reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid or expired token") + }) + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + VerificationToken verificationToken = verificationTokenRepository.findByToken(request.getToken()); + + if (verificationToken == null || verificationToken.getType() != TokenType.PASSWORD_RESET) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token"); + } + + if (verificationToken.getExpiryDate().isBefore(LocalDateTime.now())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Token expired"); + } + + Account account = verificationToken.getAccount(); + account.setPassword(passwordEncoder.encode(request.getNewPassword())); + account.setBlocked(false); + account.setFailedLoginAttempts(0); + accountRepository.save(account); + + verificationTokenRepository.delete(verificationToken); + + return ResponseEntity.ok("Password has been reset successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java new file mode 100644 index 0000000..59babe7 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -0,0 +1,88 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Media; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/media") +@Tag(name = "Media", description = "Operations related to media content and streaming") +public class MediaController { + + private final MediaService mediaService; + private final StreamingService streamingService; + + public MediaController(MediaService mediaService, StreamingService streamingService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + } + + @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved available media", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity> getAvailableMedia( + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getAvailableMedia(profileId)); + } + + @Operation(summary = "Get media details", description = "Retrieve detailed information about a specific media") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved media details", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = MediaDetailsDto.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/profile/{profileId}") + public ResponseEntity getMediaDetails( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @PathVariable Long profileId) { + return ResponseEntity.ok(mediaService.getMediaDetails(mediaId, profileId)); + } + + @Operation(summary = "Validate stream", description = "Validate if a media can be streamed with the requested quality") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Stream validation result", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = StreamValidationResult.class))), + @ApiResponse(responseCode = "404", description = "Media or profile not found") + }) + @GetMapping("/{mediaId}/validate-stream") + public ResponseEntity validateStream( + @Parameter(description = "ID of the media") @PathVariable Long mediaId, + @Parameter(description = "ID of the profile") @RequestParam Long profileId, + @Parameter(description = "Requested media quality") @RequestParam MediaQuality quality) { + return ResponseEntity.ok( + streamingService.validateStream(profileId, mediaId, quality) + ); + } + + @Operation(summary = "create new media", description = "create media based on type") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), + @ApiResponse(responseCode = "400", description = "Media upload failed") + }) + @PostMapping("/createNewMedia") + public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ + + Object created = mediaService.createMedia(mediaDetailsRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ProfileController.java b/src/main/java/com/example/streamflix/controller/ProfileController.java new file mode 100644 index 0000000..a591357 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ProfileController.java @@ -0,0 +1,192 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.model.CreatePreferenceRequest; +import com.example.streamflix.model.CreateProfileRequest; +import com.example.streamflix.model.UpdateProfileRequest; +import com.example.streamflix.service.ProfileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/profiles") +@Tag(name = "Profiles", description = "Operations related to user profiles") +public class ProfileController { + + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) { + this.profileService = profileService; + } + + @Operation(summary = "Get my profiles", description = "Retrieve all profiles associated with the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profiles", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))) + }) + @GetMapping + public ResponseEntity> getMyProfiles() { + List profiles = profileService.getMyProfiles(); + return ResponseEntity.ok(profiles); + } + + @Operation(summary = "Get profile by ID", description = "Retrieve a specific profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved profile", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}") + public ResponseEntity getProfileById(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Profile profile = profileService.getProfileById(profileId); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Create a new profile", description = "Create a new profile for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Profile created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping + public ResponseEntity createProfile(@Valid @RequestBody CreateProfileRequest request) { + try { + Profile profile = profileService.createProfile( + request.getName(), + request.getAgeRatingId(), + request.getImageUrl(), + request.getBirthDate() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(profile); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Update a profile", description = "Update an existing profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile updated successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Profile.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PutMapping("/{profileId}") + public ResponseEntity updateProfile( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody UpdateProfileRequest request) { + try { + Profile profile = profileService.updateProfile( + profileId, + request.getName(), + request.getAgeRatingId(), + request.getImageUrl() + ); + return ResponseEntity.ok(profile); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a profile", description = "Delete a profile by its ID") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Profile deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @DeleteMapping("/{profileId}") + public ResponseEntity deleteProfile(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + profileService.deleteProfile(profileId); + return ResponseEntity.ok("Profile deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Add a preference", description = "Add a preference to a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Preference added successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @PostMapping("/{profileId}/preferences") + public ResponseEntity addPreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Valid @RequestBody CreatePreferenceRequest request) { + try { + Preference preference = profileService.addPreference( + profileId, + request.getPreferenceType(), + request.getValue() + ); + return ResponseEntity.status(HttpStatus.CREATED).body(preference); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } + + @Operation(summary = "Get profile preferences", description = "Retrieve all preferences for a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved preferences", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Preference.class))), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/{profileId}/preferences") + public ResponseEntity getPreferences(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List preferences = profileService.getProfilePreferences(profileId); + return ResponseEntity.ok(preferences); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); + } + } + + @Operation(summary = "Delete a preference", description = "Delete a preference from a profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Preference deleted successfully"), + @ApiResponse(responseCode = "403", description = "Access denied to the profile"), + @ApiResponse(responseCode = "400", description = "Invalid input data") + }) + @DeleteMapping("/{profileId}/preferences/{preferenceId}") + public ResponseEntity deletePreference( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the preference") @PathVariable Long preferenceId) { + try { + profileService.deletePreference(profileId, preferenceId); + return ResponseEntity.ok("Preference deleted successfully"); + } catch (SecurityException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage()); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ReferralController.java b/src/main/java/com/example/streamflix/controller/ReferralController.java new file mode 100644 index 0000000..8013175 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ReferralController.java @@ -0,0 +1,82 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Referral; +import com.example.streamflix.model.AcceptInvitationRequest; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.service.ReferralService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/referrals") +@Tag(name = "Referrals", description = "Operations for managing referrals and invitations") +public class ReferralController { + + private final ReferralService referralService; + private final SecurityUtils securityUtils; + + public ReferralController(ReferralService referralService, SecurityUtils securityUtils) { + this.referralService = referralService; + this.securityUtils = securityUtils; + } + + @PostMapping("/create-invitation") + @Operation(summary = "Create an invitation link to invite others") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Invitation created successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvitationResponse.class))), + @ApiResponse(responseCode = "400", description = "User does not have an active subscription") + }) + public ResponseEntity createInvitation() { + InvitationResponse response = referralService.createInvitation(); + return new ResponseEntity<>(response, HttpStatus.CREATED); + } + + @PostMapping("/accept-invitation") + @Operation(summary = "Accept an invitation using invite code") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Invitation accepted successfully", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "400", description = "Invalid invite code or invitation already accepted"), + @ApiResponse(responseCode = "404", description = "Referral not found") + }) + public ResponseEntity acceptInvitation(@Valid @RequestBody AcceptInvitationRequest request) { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Referral referral = referralService.acceptInvitation(request.getInviteCode(), accountId); + return ResponseEntity.ok(referral); + } + + @GetMapping("/my-invitations") + @Operation(summary = "Get all invitations I have sent") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved invitations", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))) + }) + public ResponseEntity> getMyInvitations() { + return ResponseEntity.ok(referralService.getMyInvitations()); + } + + @GetMapping("/my-referral-status") + @Operation(summary = "Check if I was invited by someone") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved referral status", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Referral.class))), + @ApiResponse(responseCode = "404", description = "Referral status not found") + }) + public ResponseEntity getMyReferralStatus() { + return referralService.getMyReferralStatus() + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/SubscriptionController.java b/src/main/java/com/example/streamflix/controller/SubscriptionController.java new file mode 100644 index 0000000..6ba5a35 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SubscriptionController.java @@ -0,0 +1,99 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.CreateTrialRequest; +import com.example.streamflix.model.UpgradeSubscriptionRequest; +import com.example.streamflix.service.SubscriptionService; +import com.example.streamflix.util.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/subscriptions") +@Tag(name = "Subscriptions", description = "Operations for managing user subscriptions") +public class SubscriptionController { + + private final SubscriptionService subscriptionService; + private final SecurityUtils securityUtils; + + public SubscriptionController(SubscriptionService subscriptionService, SecurityUtils securityUtils) { + this.subscriptionService = subscriptionService; + this.securityUtils = securityUtils; + } + + @Operation(summary = "Create a 7-day trial subscription", description = "Start a new trial subscription for the user") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully created trial subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "400", description = "Invalid input") + }) + @PostMapping("/trial") + public ResponseEntity createTrialSubscription(@Valid @RequestBody CreateTrialRequest request) { + try { + Subscription subscription = subscriptionService.createTrialSubscription(request.getAccountId(), request.getTierId()); + return new ResponseEntity<>(subscription, HttpStatus.CREATED); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Upgrade to paid subscription", description = "Upgrade an existing subscription to a paid tier") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully upgraded subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/upgrade") + public ResponseEntity upgradeSubscription(@Valid @RequestBody UpgradeSubscriptionRequest request) { + try { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionService.upgradeToPaidSubscription(accountId, request.getTierId()); + return ResponseEntity.ok(subscription); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get my active subscription", description = "Retrieve the currently active subscription for the authenticated user") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved subscription", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Subscription.class))), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/my-subscription") + public ResponseEntity getMyActiveSubscription() { + Optional subscription = subscriptionService.getMyActiveSubscription(); + return subscription.map(ResponseEntity::ok) + .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); + } + + @Operation(summary = "Cancel active subscription", description = "Cancel the user's active subscription") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully cancelled subscription"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/cancel") + public ResponseEntity cancelSubscription() { + try { + subscriptionService.cancelSubscription(); + return ResponseEntity.ok("Subscription cancelled successfully"); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/ViewingProgressController.java b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java new file mode 100644 index 0000000..2f70dc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/ViewingProgressController.java @@ -0,0 +1,134 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.StartWatchingRequest; +import com.example.streamflix.model.UpdateProgressRequest; +import com.example.streamflix.service.ViewingProgressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/viewing-progress") +@Tag(name = "Viewing Progress", description = "Operations for tracking viewing progress") +public class ViewingProgressController { + + private final ViewingProgressService viewingProgressService; + + public ViewingProgressController(ViewingProgressService viewingProgressService) { + this.viewingProgressService = viewingProgressService; + } + + @Operation(summary = "Start watching a movie or episode", description = "Initialize viewing progress for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully started watching", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping("/start") + public ResponseEntity startWatching(@Valid @RequestBody StartWatchingRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.startWatching(request.getProfileId(), request.getMovieId(), request.getEpisodeId()); + return new ResponseEntity<>(viewingProgress, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Update viewing progress", description = "Update the current position and duration watched for a media item") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully updated progress", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}") + public ResponseEntity updateProgress( + @Parameter(description = "ID of the viewing progress") @PathVariable Long progressId, + @Valid @RequestBody UpdateProgressRequest request) { + try { + ViewingProgress viewingProgress = viewingProgressService.updateProgress(progressId, request.getLastPositionSeconds(), request.getDurationWatchedSeconds()); + return ResponseEntity.ok(viewingProgress); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Mark content as finished", description = "Mark a media item as completely watched") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully marked as finished"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PutMapping("/{progressId}/finish") + public ResponseEntity markAsFinished(@Parameter(description = "ID of the viewing progress") @PathVariable Long progressId) { + try { + viewingProgressService.markAsFinished(progressId); + return ResponseEntity.ok("Marked as finished"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing history for a profile", description = "Retrieve the viewing history for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved viewing history", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = ViewingProgress.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyViewingHistory(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List viewingHistory = viewingProgressService.getMyViewingHistory(profileId); + return ResponseEntity.ok(viewingHistory); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get viewing statistics for a profile", description = "Retrieve viewing statistics for a specific profile") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved statistics", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Map.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Profile not found") + }) + @GetMapping("/profile/{profileId}/stats") + public ResponseEntity getViewingStats(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + Map stats = viewingProgressService.getProfileViewingStats(profileId); + return ResponseEntity.ok(stats); + } catch (SecurityException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/WatchListController.java b/src/main/java/com/example/streamflix/controller/WatchListController.java new file mode 100644 index 0000000..642b8b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/WatchListController.java @@ -0,0 +1,93 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.WatchList; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.AddToWatchListRequest; +import com.example.streamflix.service.WatchListService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@RestController +@RequestMapping("/api/v1/watchlist") +@Tag(name = "Watch List", description = "Operations for managing user watch lists") +public class WatchListController { + + private final WatchListService watchListService; + + public WatchListController(WatchListService watchListService) { + this.watchListService = watchListService; + } + + @Operation(summary = "Add media to watch list", description = "Add a media item to a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Successfully added to watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "400", description = "Invalid input"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @PostMapping + public ResponseEntity addToWatchList(@Valid @RequestBody AddToWatchListRequest request) { + try { + WatchList watchList = watchListService.addToWatchList(request.getProfileId(), request.getMediaId()); + return new ResponseEntity<>(watchList, HttpStatus.CREATED); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } catch (IllegalArgumentException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @Operation(summary = "Remove media from watch list", description = "Remove a media item from a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully removed from watch list"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @DeleteMapping("/profile/{profileId}/media/{mediaId}") + public ResponseEntity removeFromWatchList( + @Parameter(description = "ID of the profile") @PathVariable Long profileId, + @Parameter(description = "ID of the media") @PathVariable Long mediaId) { + try { + watchListService.removeFromWatchList(profileId, mediaId); + return ResponseEntity.ok("Removed from watch list"); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } + + @Operation(summary = "Get user's watch list", description = "Retrieve all items in a profile's watch list") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved watch list", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WatchList.class))), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not Found") + }) + @GetMapping("/profile/{profileId}") + public ResponseEntity getMyWatchList(@Parameter(description = "ID of the profile") @PathVariable Long profileId) { + try { + List watchList = watchListService.getMyWatchList(profileId); + return ResponseEntity.ok(watchList); + } catch (AccessDeniedException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } catch (NotFoundException e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java new file mode 100644 index 0000000..800f360 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -0,0 +1,109 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "accounts") +@Schema(description = "Account entity representing a user account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the account", example = "1") + private Long id; + + @Column(unique = true, nullable = false) + @Schema(description = "Email address of the account", example = "user@example.com") + private String email; + + @JsonIgnore + @Column(nullable = false, name = "password_hash") + @Schema(description = "Hashed password of the account") + private String password; + + @JsonIgnore + @Column(name = "failed_login_attempts") + @Schema(description = "Number of failed login attempts", example = "0") + private int failedLoginAttempts = 0; + + @JsonIgnore + @Column(name = "is_blocked") + @Schema(description = "Indicates if the account is blocked", example = "false") + private boolean isBlocked = false; + + @JsonIgnore + @Column(name = "is_verified") + @Schema(description = "Indicates if the account is verified", example = "false") + private boolean isVerified = false; + + @JsonIgnore + @Column(name = "discount_used") + @Schema(description = "Indicates if the discount has been used", example = "false") + private boolean discountUsed = false; + + public Account() { + } + + public Account(String email, String password) { + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getFailedLoginAttempts() { + return failedLoginAttempts; + } + + public void setFailedLoginAttempts(int failedLoginAttempts) { + this.failedLoginAttempts = failedLoginAttempts; + } + + public boolean isBlocked() { + return isBlocked; + } + + public void setBlocked(boolean blocked) { + isBlocked = blocked; + } + + public boolean isVerified() { + return isVerified; + } + + public void setVerified(boolean verified) { + isVerified = verified; + } + + public boolean isDiscountUsed() { + return discountUsed; + } + + public void setDiscountUsed(boolean discountUsed) { + this.discountUsed = discountUsed; + } +} diff --git a/src/main/java/com/example/streamflix/entity/AgeRating.java b/src/main/java/com/example/streamflix/entity/AgeRating.java new file mode 100644 index 0000000..5e35a42 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/AgeRating.java @@ -0,0 +1,55 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "age_rating") +@Schema(description = "AgeRating entity representing age restrictions") +public class AgeRating { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the age rating", example = "1") + private Long id; + + @Column(nullable = false, length = 20) + @Schema(description = "Label of the age rating", example = "PG-13") + private String label; + + @Column(name = "min_age", nullable = false) + @Schema(description = "Minimum age required for this rating", example = "13") + private int minAge; + + public AgeRating() { + } + + public AgeRating(String label, int minAge) { + this.label = label; + this.minAge = minAge; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public int getMinAge() { + return minAge; + } + + public void setMinAge(int minAge) { + this.minAge = minAge; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ContentWarning.java b/src/main/java/com/example/streamflix/entity/ContentWarning.java new file mode 100644 index 0000000..34af7f6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ContentWarning.java @@ -0,0 +1,42 @@ +package com.example.streamflix.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "content_warning") +@Schema(description = "ContentWarning entity representing content warnings for media") +public class ContentWarning { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the content warning", example = "1") + private Long id; + + @Column(nullable = false, length = 50) + @Schema(description = "Description of the content warning", example = "Violence") + private String description; + + public ContentWarning() { + } + + public ContentWarning(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java new file mode 100644 index 0000000..095d9ec --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -0,0 +1,63 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "employee") +public class Employee { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + @Column(nullable = false, length = 100) + private String name; + + @Column(unique = true, nullable = false) + private String email; + + public Employee() { + } + + public Employee(Role role, String name, String email) { + this.role = role; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java new file mode 100644 index 0000000..4ec6fa6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -0,0 +1,92 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "episode") +public class Episode extends Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "season_id", nullable = false) + @JsonBackReference + private Season season; + + private String title; + + @Column(name = "duration_seconds") + private int durationSeconds; + + @Column(name = "video_url") + private String videoUrl; + + @Column(name = "episode_number") + private int episodeNumber; + + @OneToMany(mappedBy = "episode", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Season getSeason() { + return season; + } + + public void setSeason(Season season) { + this.season = season; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) { + this.episodeNumber = episodeNumber; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} diff --git a/src/main/java/com/example/streamflix/entity/InvitationStatus.java b/src/main/java/com/example/streamflix/entity/InvitationStatus.java new file mode 100644 index 0000000..e4ff5ae --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/InvitationStatus.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "invitation_status") +public class InvitationStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String status; + + public InvitationStatus() { + } + + public InvitationStatus(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java new file mode 100644 index 0000000..b7c9085 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -0,0 +1,98 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +@DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) +@Table(name = "media") +@Schema(description = "Abstract Media entity representing common media properties") +public abstract class Media { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the media", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the media") + private AgeRating ageRating; + + @Column(nullable = false) + @Schema(description = "Title of the media", example = "Inception") + private String title; + + @Column(name = "release_date") + @Schema(description = "Release date of the media", example = "2010-07-16") + private LocalDate releaseDate; + + @Column(name = "external_id") + @Schema(description = "External ID for the media (e.g., TMDB ID)", example = "27205") + private String externalId; + + @OneToMany(mappedBy = "media", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchLists; + + public Media() { + } + + public Media(AgeRating ageRating, String title, LocalDate releaseDate) { + this.ageRating = ageRating; + this.title = title; + this.releaseDate = releaseDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public List getWatchLists() { + return watchLists; + } + + public void setWatchLists(List watchLists) { + this.watchLists = watchLists; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Movie.java b/src/main/java/com/example/streamflix/entity/Movie.java new file mode 100644 index 0000000..1f32d6f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Movie.java @@ -0,0 +1,46 @@ +// Movie.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "movie") +@DiscriminatorValue("Movie") +@PrimaryKeyJoinColumn(name = "media_id") +public class Movie extends Media { + + @Column(name = "duration_seconds", nullable = false) + private Integer durationSeconds; + + @Column(name = "video_url", nullable = false) + private String videoUrl; + + @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + public Movie() {} + + public Movie(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + Integer durationSeconds, String videoUrl) { + super(ageRating, title, releaseDate); + this.durationSeconds = durationSeconds; + this.videoUrl = videoUrl; + } + + public Integer getDurationSeconds() { return durationSeconds; } + public void setDurationSeconds(Integer durationSeconds) { this.durationSeconds = durationSeconds; } + + public String getVideoUrl() { return videoUrl; } + public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl; } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Preference.java b/src/main/java/com/example/streamflix/entity/Preference.java new file mode 100644 index 0000000..cd114a4 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Preference.java @@ -0,0 +1,71 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; + +@Entity +@Table(name = "preference") +@Schema(description = "Preference entity representing user preferences") +public class Preference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the preference", example = "1") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + @Schema(description = "Profile associated with the preference") + private Profile profile; + + @Enumerated(EnumType.STRING) + @Column(name = "preference_type", nullable = false) + @Schema(description = "Type of the preference", example = "GENRE") + private PreferenceType preferenceType; + + @Column(nullable = false, length = 100) + @Schema(description = "Value of the preference", example = "Action") + private String value; + + public Preference() { + } + + public Preference(Profile profile, PreferenceType preferenceType, String value) { + this.profile = profile; + this.preferenceType = preferenceType; + this.value = value; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Profile.java b/src/main/java/com/example/streamflix/entity/Profile.java new file mode 100644 index 0000000..0014d08 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Profile.java @@ -0,0 +1,125 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.List; + +@Entity +@Table(name = "profile") +@Schema(description = "Profile entity representing a user profile") +public class Profile { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the profile", example = "1") + private Long id; + + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id", nullable = false) + @Schema(description = "Account associated with the profile") + private Account account; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "age_rating_id", nullable = false) + @Schema(description = "Age rating associated with the profile") + private AgeRating ageRating; + + @Column(nullable = false, length = 50) + @Schema(description = "Name of the profile", example = "John Doe") + private String name; + + @Column(name = "image_url") + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Column(name = "birth_date") + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List viewingProgresses; + + @OneToMany(mappedBy = "profile", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List watchList; + + public Profile() { + } + + public Profile(Account account, AgeRating ageRating, String name, String imageUrl, LocalDate birthDate) { + this.account = account; + this.ageRating = ageRating; + this.name = name; + this.imageUrl = imageUrl; + this.birthDate = birthDate; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public AgeRating getAgeRating() { + return ageRating; + } + + public void setAgeRating(AgeRating ageRating) { + this.ageRating = ageRating; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public List getViewingProgresses() { + return viewingProgresses; + } + + public void setViewingProgresses(List viewingProgresses) { + this.viewingProgresses = viewingProgresses; + } + + public List getWatchList() { + return watchList; + } + + public void setWatchList(List watchList) { + this.watchList = watchList; + } +} diff --git a/src/main/java/com/example/streamflix/entity/QualityType.java b/src/main/java/com/example/streamflix/entity/QualityType.java new file mode 100644 index 0000000..f40d20f --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/QualityType.java @@ -0,0 +1,31 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "quality_type") +public class QualityType { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Referral.java b/src/main/java/com/example/streamflix/entity/Referral.java new file mode 100644 index 0000000..e7ae865 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Referral.java @@ -0,0 +1,96 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "referral") +public class Referral { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inviter_account_id", nullable = false) + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviterAccount; + + @ManyToOne + @JoinColumn(name = "invitee_account_id") + @JsonIgnoreProperties({"password", "failedLoginAttempts", "blocked", "verified", "discountUsed"}) + private Account inviteeAccount; + + @ManyToOne + @JoinColumn(name = "status_id") + private InvitationStatus status; + + @Column(name = "invite_date") + private LocalDate inviteDate; + + @Column(name = "discount_applied") + private Boolean discountApplied = false; + + public Referral() { + } + + public Referral(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + @PrePersist + protected void onCreate() { + if (inviteDate == null) { + inviteDate = LocalDate.now(); + } + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getInviterAccount() { + return inviterAccount; + } + + public void setInviterAccount(Account inviterAccount) { + this.inviterAccount = inviterAccount; + } + + public Account getInviteeAccount() { + return inviteeAccount; + } + + public void setInviteeAccount(Account inviteeAccount) { + this.inviteeAccount = inviteeAccount; + } + + public InvitationStatus getStatus() { + return status; + } + + public void setStatus(InvitationStatus status) { + this.status = status; + } + + public LocalDate getInviteDate() { + return inviteDate; + } + + public void setInviteDate(LocalDate inviteDate) { + this.inviteDate = inviteDate; + } + + public Boolean getDiscountApplied() { + return discountApplied; + } + + public void setDiscountApplied(Boolean discountApplied) { + this.discountApplied = discountApplied; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Role.java b/src/main/java/com/example/streamflix/entity/Role.java new file mode 100644 index 0000000..501b996 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Role.java @@ -0,0 +1,38 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "role") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; + + public Role() { + } + + public Role(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Season.java b/src/main/java/com/example/streamflix/entity/Season.java new file mode 100644 index 0000000..a726051 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Season.java @@ -0,0 +1,60 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.List; + +@Entity +@Table(name = "season") +public class Season { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + @JsonBackReference + private Series series; + + @Column(name = "season_number") + private int seasonNumber; + + @OneToMany(mappedBy = "season", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JsonManagedReference + private List episodes; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Series getSeries() { + return series; + } + + public void setSeries(Series series) { + this.series = series; + } + + public int getSeasonNumber() { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) { + this.seasonNumber = seasonNumber; + } + + public List getEpisodes() { + return episodes; + } + + public void setEpisodes(List episodes) { + this.episodes = episodes; + } +} diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java new file mode 100644 index 0000000..a2a05c6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -0,0 +1,35 @@ +// Series.java +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "series") +@DiscriminatorValue("Series") +@PrimaryKeyJoinColumn(name = "media_id") +public class Series extends Media { + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @OneToMany(mappedBy = "series", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private List seasons = new ArrayList<>(); + + public Series() {} + + public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate, + String description) { + super(ageRating, title, releaseDate); + this.description = description; + } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public List getSeasons() { return seasons; } + public void setSeasons(List seasons) { this.seasons = seasons; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Subscription.java b/src/main/java/com/example/streamflix/entity/Subscription.java new file mode 100644 index 0000000..ad20509 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/Subscription.java @@ -0,0 +1,90 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "subscription") +public class Subscription { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "account_id", nullable = false) + private Account account; + + @ManyToOne + @JoinColumn(name = "tier_id", nullable = false) + private SubscriptionTier tier; + + @Column(name = "start_date", nullable = false) + private LocalDate startDate; + + @Column(name = "end_date") + private LocalDate endDate; + + @Column(name = "is_trial") + private Boolean isTrial; + + @Column(name = "is_active") + private Boolean isActive; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public SubscriptionTier getTier() { + return tier; + } + + public void setTier(SubscriptionTier tier) { + this.tier = tier; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Boolean getTrial() { + return isTrial; + } + + public void setTrial(Boolean trial) { + isTrial = trial; + } + + public Boolean getActive() { + return isActive; + } + + public void setActive(Boolean active) { + isActive = active; + } +} diff --git a/src/main/java/com/example/streamflix/entity/SubscriptionTier.java b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java new file mode 100644 index 0000000..e1e89b6 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/SubscriptionTier.java @@ -0,0 +1,53 @@ +package com.example.streamflix.entity; + +import jakarta.persistence.*; + +@Entity +@Table(name = "subscription_tier") +public class SubscriptionTier { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + private java.math.BigDecimal price; + + @ManyToOne + @JoinColumn(name = "max_quality_id") + private QualityType maxQuality; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public java.math.BigDecimal getPrice() { + return price; + } + + public void setPrice(java.math.BigDecimal price) { + this.price = price; + } + + public QualityType getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(QualityType maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/src/main/java/com/example/streamflix/entity/VerificationToken.java b/src/main/java/com/example/streamflix/entity/VerificationToken.java new file mode 100644 index 0000000..a2dcb57 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/VerificationToken.java @@ -0,0 +1,84 @@ +package com.example.streamflix.entity; + +import com.example.streamflix.enums.TokenType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "verification_token") +@Schema(description = "VerificationToken entity for account verification") +public class VerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Schema(description = "Unique identifier of the token", example = "1") + private Long id; + + @Schema(description = "The verification token string", example = "abc123xyz") + private String token; + + @OneToOne(targetEntity = Account.class, fetch = FetchType.EAGER) + @JoinColumn(nullable = false, name = "account_id") + @Schema(description = "Account associated with the token") + private Account account; + + @Column(name = "expiry_date") + @Schema(description = "Expiration date and time of the token") + private LocalDateTime expiryDate; + + @Enumerated(EnumType.STRING) + @Column(name = "token_type") + @Schema(description = "Type of the token", example = "EMAIL_VERIFICATION") + private TokenType type; + + public VerificationToken() { + } + + public VerificationToken(String token, Account account, LocalDateTime expiryDate, TokenType type) { + this.token = token; + this.account = account; + this.expiryDate = expiryDate; + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public TokenType getType() { + return type; + } + + public void setType(TokenType type) { + this.type = type; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/ViewingProgress.java b/src/main/java/com/example/streamflix/entity/ViewingProgress.java new file mode 100644 index 0000000..242ad1e --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/ViewingProgress.java @@ -0,0 +1,127 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import org.hibernate.annotations.Check; +import java.time.LocalDateTime; + +@Entity +@Table(name = "viewing_progress") +@Check(constraints = "movie_id IS NOT NULL OR episode_id IS NOT NULL") +public class ViewingProgress { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id") + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "movie_id", nullable = true) + @JsonBackReference + private Movie movie; + + @ManyToOne + @JoinColumn(name = "episode_id", nullable = true) + @JsonBackReference + private Episode episode; + + @Column(name = "start_time") + private LocalDateTime startTime; + + @Column(name = "duration_watched_seconds") + private Integer durationWatchedSeconds; + + @Column(name = "last_position_seconds") + private Integer lastPositionSeconds; + + @Column(name = "is_finished") + private Boolean isFinished; + + public ViewingProgress() { + } + + public ViewingProgress(Profile profile, Movie movie, Episode episode, LocalDateTime startTime, Integer durationWatchedSeconds, Integer lastPositionSeconds, Boolean isFinished) { + this.profile = profile; + this.movie = movie; + this.episode = episode; + this.startTime = startTime; + this.durationWatchedSeconds = durationWatchedSeconds; + this.lastPositionSeconds = lastPositionSeconds; + this.isFinished = isFinished; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Movie getMovie() { + return movie; + } + + public void setMovie(Movie movie) { + this.movie = movie; + } + + public Episode getEpisode() { + return episode; + } + + public void setEpisode(Episode episode) { + this.episode = episode; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Boolean getIsFinished() { + return isFinished; + } + + public void setIsFinished(Boolean isFinished) { + this.isFinished = isFinished; + } + + @PrePersist + protected void onCreate() { + if (startTime == null) { + startTime = LocalDateTime.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/entity/WatchList.java b/src/main/java/com/example/streamflix/entity/WatchList.java new file mode 100644 index 0000000..f8c3f00 --- /dev/null +++ b/src/main/java/com/example/streamflix/entity/WatchList.java @@ -0,0 +1,74 @@ +package com.example.streamflix.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +@Table(name = "watch_list") +public class WatchList { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "profile_id", nullable = false) + @JsonBackReference + private Profile profile; + + @ManyToOne + @JoinColumn(name = "media_id", nullable = false) + @JsonBackReference + private Media media; + + @Column(name = "added_date") + private LocalDate addedDate; + + public WatchList() { + } + + public WatchList(Profile profile, Media media) { + this.profile = profile; + this.media = media; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public Media getMedia() { + return media; + } + + public void setMedia(Media media) { + this.media = media; + } + + public LocalDate getAddedDate() { + return addedDate; + } + + public void setAddedDate(LocalDate addedDate) { + this.addedDate = addedDate; + } + + @PrePersist + protected void onCreate() { + if (addedDate == null) { + addedDate = LocalDate.now(); + } + } +} diff --git a/src/main/java/com/example/streamflix/enums/MediaQuality.java b/src/main/java/com/example/streamflix/enums/MediaQuality.java new file mode 100644 index 0000000..ba21818 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/MediaQuality.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum MediaQuality { + SD, + HD, + UHD +} diff --git a/src/main/java/com/example/streamflix/enums/PreferenceType.java b/src/main/java/com/example/streamflix/enums/PreferenceType.java new file mode 100644 index 0000000..99b3c5e --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/PreferenceType.java @@ -0,0 +1,7 @@ +package com.example.streamflix.enums; + +public enum PreferenceType { + GENRE, + CONTENT_FILTER, + MIN_AGE +} diff --git a/src/main/java/com/example/streamflix/enums/TokenType.java b/src/main/java/com/example/streamflix/enums/TokenType.java new file mode 100644 index 0000000..0e50073 --- /dev/null +++ b/src/main/java/com/example/streamflix/enums/TokenType.java @@ -0,0 +1,6 @@ +package com.example.streamflix.enums; + +public enum TokenType { + EMAIL_VERIFICATION, + PASSWORD_RESET +} diff --git a/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a651cb0 --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package com.example.streamflix.exception; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import com.example.streamflix.model.ErrorResponse; +import java.util.Arrays; + + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(SecurityException.class) + public ResponseEntity handleSecurityException(SecurityException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.FORBIDDEN.value(), + "Forbidden", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.NOT_FOUND.value(), + "Not Found", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) { + String message = String.format("Invalid value '%s' for parameter '%s'.", ex.getValue(), ex.getName()); + + if (ex.getRequiredType() != null && ex.getRequiredType().isEnum()) { + message += String.format(" Allowed values are: %s", Arrays.toString(ex.getRequiredType().getEnumConstants())); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Bad Request", + message, + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex, WebRequest request) { + + String message = ex.getMessage(); + if (message != null && message.contains("Media already in watchlist")) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.CONFLICT.value(), + "Conflict", + "Media already in watchlist for this profile", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.CONFLICT); + } + + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.BAD_REQUEST.value(), + "Data Integrity Violation", + "Database constraint violation occurred", + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex, WebRequest request) { + ErrorResponse errorResponse = new ErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "Internal Server Error", + ex.getMessage(), + request.getDescription(false).replace("uri=", "") + ); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/com/example/streamflix/exception/NotFoundException.java b/src/main/java/com/example/streamflix/exception/NotFoundException.java new file mode 100644 index 0000000..53cbcec --- /dev/null +++ b/src/main/java/com/example/streamflix/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package com.example.streamflix.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } + + public NotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java new file mode 100644 index 0000000..c6efd38 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AcceptInvitationRequest.java @@ -0,0 +1,16 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotBlank; + +public class AcceptInvitationRequest { + @NotBlank(message = "Invite code is required") + private String inviteCode; + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java new file mode 100644 index 0000000..c2816b9 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/AddToWatchListRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class AddToWatchListRequest { + + @NotNull + private Long profileId; + + @NotNull + private Long mediaId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMediaId() { + return mediaId; + } + + public void setMediaId(Long mediaId) { + this.mediaId = mediaId; + } +} diff --git a/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java new file mode 100644 index 0000000..b18fe7e --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreatePreferenceRequest.java @@ -0,0 +1,34 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.PreferenceType; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "Request object for creating a new preference") +public class CreatePreferenceRequest { + + @Schema(description = "Type of the preference", example = "GENRE") + @NotNull(message = "Preference type is required") + private PreferenceType preferenceType; + + @Schema(description = "Value of the preference", example = "Action") + @NotBlank(message = "Value is required") + private String value; + + public PreferenceType getPreferenceType() { + return preferenceType; + } + + public void setPreferenceType(PreferenceType preferenceType) { + this.preferenceType = preferenceType; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateProfileRequest.java b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java new file mode 100644 index 0000000..56eac15 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateProfileRequest.java @@ -0,0 +1,56 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +@Schema(description = "Request object for creating a new profile") +public class CreateProfileRequest { + + @Schema(description = "Name of the profile", example = "John Doe") + @NotBlank(message = "Name is required") + private String name; + + @Schema(description = "ID of the age rating for the profile", example = "1") + @NotNull(message = "Age rating ID is required") + private Long ageRatingId; + + @Schema(description = "URL of the profile image", example = "http://example.com/image.jpg") + private String imageUrl; + + @Schema(description = "Birth date of the profile owner", example = "1990-01-01") + private LocalDate birthDate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/CreateTrialRequest.java b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java new file mode 100644 index 0000000..1106234 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/CreateTrialRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class CreateTrialRequest { + + @NotNull + private Long accountId; + + @NotNull + private Long tierId; + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/model/ErrorResponse.java b/src/main/java/com/example/streamflix/model/ErrorResponse.java new file mode 100644 index 0000000..2670e3f --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ErrorResponse.java @@ -0,0 +1,103 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.LocalDateTime; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private String path; + private List validationErrors; + + public ErrorResponse() { + this.timestamp = LocalDateTime.now(); + } + + public ErrorResponse(int status, String error, String message, String path) { + this(); + this.status = status; + this.error = error; + this.message = message; + this.path = path; + } + + // Getters and Setters + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public List getValidationErrors() { + return validationErrors; + } + + public void setValidationErrors(List validationErrors) { + this.validationErrors = validationErrors; + } + + public static class ValidationError { + private String field; + private String message; + + public ValidationError(String field, String message) { + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java new file mode 100644 index 0000000..8d87484 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ForgotPasswordRequest.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for initiating password reset") +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Schema(description = "User's email address", example = "user@example.com") + private String email; + + public ForgotPasswordRequest() { + } + + public ForgotPasswordRequest(String email) { + this.email = email; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/InvitationResponse.java b/src/main/java/com/example/streamflix/model/InvitationResponse.java new file mode 100644 index 0000000..bdd3d0a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/InvitationResponse.java @@ -0,0 +1,29 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Referral; + +public class InvitationResponse { + private Referral referral; + private String inviteCode; + + public InvitationResponse(Referral referral, String inviteCode) { + this.referral = referral; + this.inviteCode = inviteCode; + } + + public Referral getReferral() { + return referral; + } + + public void setReferral(Referral referral) { + this.referral = referral; + } + + public String getInviteCode() { + return inviteCode; + } + + public void setInviteCode(String inviteCode) { + this.inviteCode = inviteCode; + } +} diff --git a/src/main/java/com/example/streamflix/model/LoginRequest.java b/src/main/java/com/example/streamflix/model/LoginRequest.java new file mode 100644 index 0000000..9103cc4 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/LoginRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user login") +public class LoginRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaDetailsDto.java b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java new file mode 100644 index 0000000..4496e93 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaDetailsDto.java @@ -0,0 +1,117 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MediaDetailsDto { + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private Long seriesId; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + // Getters and setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getReleaseDate() { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) { + this.releaseDate = releaseDate; + } + + public String getAgeRating() { + return ageRating; + } + + public void setAgeRating(String ageRating) { + this.ageRating = ageRating; + } + + public String getExternalId() { + return externalId; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public String getPosterUrl() { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) { + this.backdropUrl = backdropUrl; + } + + public String getOverview() { + return overview; + } + + public void setOverview(String overview) { + this.overview = overview; + } + + public Double getExternalRating() { + return externalRating; + } + + public void setExternalRating(Double externalRating) { + this.externalRating = externalRating; + } + + public String getMediaType() { + return this.mediaType; + } + + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + public Long getSeriesId() { + return this.seriesId; + } + + public void setSeriesId(Long seriesId) { + this.seriesId = seriesId; + } + + public int getDurationInSecond() { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) { + this.durationInSecond = durationInSecond; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/RegisterRequest.java b/src/main/java/com/example/streamflix/model/RegisterRequest.java new file mode 100644 index 0000000..65f4593 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/RegisterRequest.java @@ -0,0 +1,36 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.Column; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for user registration") +public class RegisterRequest { + + @Schema(description = "Email address of the user", example = "user@example.com") + @Email + @NotBlank + private String email; + + @Schema(description = "Password for the user account", example = "password123") + @NotBlank + @Column(name = "password_hash") + private String password; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java new file mode 100644 index 0000000..2f48bc6 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/ResetPasswordRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "Request object for resetting password") +public class ResetPasswordRequest { + + @NotBlank(message = "Token is required") + @Schema(description = "Password reset token received via email", example = "abc-123-xyz") + private String token; + + @NotBlank(message = "New password is required") + @Schema(description = "New password for the account", example = "newPassword123") + private String newPassword; + + public ResetPasswordRequest() { + } + + public ResetPasswordRequest(String token, String newPassword) { + this.token = token; + this.newPassword = newPassword; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/StartWatchingRequest.java b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java new file mode 100644 index 0000000..1d82c7b --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StartWatchingRequest.java @@ -0,0 +1,37 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class StartWatchingRequest { + + @NotNull + private Long profileId; + + private Long movieId; + + private Long episodeId; + + public Long getProfileId() { + return profileId; + } + + public void setProfileId(Long profileId) { + this.profileId = profileId; + } + + public Long getMovieId() { + return movieId; + } + + public void setMovieId(Long movieId) { + this.movieId = movieId; + } + + public Long getEpisodeId() { + return episodeId; + } + + public void setEpisodeId(Long episodeId) { + this.episodeId = episodeId; + } +} diff --git a/src/main/java/com/example/streamflix/model/StreamValidationResult.java b/src/main/java/com/example/streamflix/model/StreamValidationResult.java new file mode 100644 index 0000000..b699bfa --- /dev/null +++ b/src/main/java/com/example/streamflix/model/StreamValidationResult.java @@ -0,0 +1,35 @@ +package com.example.streamflix.model; + +import com.example.streamflix.enums.MediaQuality; + +public class StreamValidationResult { + private Long profileId; + private Long mediaId; + private MediaQuality requestedQuality; + private boolean allowed; + private String reason; + private MediaQuality suggestedQuality; + + // Getters and setters + public Long getProfileId() { return profileId; } + public void setProfileId(Long profileId) { this.profileId = profileId; } + + public Long getMediaId() { return mediaId; } + public void setMediaId(Long mediaId) { this.mediaId = mediaId; } + + public MediaQuality getRequestedQuality() { return requestedQuality; } + public void setRequestedQuality(MediaQuality requestedQuality) { + this.requestedQuality = requestedQuality; + } + + public boolean isAllowed() { return allowed; } + public void setAllowed(boolean allowed) { this.allowed = allowed; } + + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + + public MediaQuality getSuggestedQuality() { return suggestedQuality; } + public void setSuggestedQuality(MediaQuality suggestedQuality) { + this.suggestedQuality = suggestedQuality; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java new file mode 100644 index 0000000..0020b62 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/TmdbMovieResponse.java @@ -0,0 +1,44 @@ +package com.example.streamflix.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TmdbMovieResponse { + + private Long id; + private String title; + private String overview; + + @JsonProperty("poster_path") + private String posterPath; + + @JsonProperty("backdrop_path") + private String backdropPath; + + @JsonProperty("vote_average") + private Double voteAverage; + + @JsonProperty("release_date") + private String releaseDate; + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getOverview() { return overview; } + public void setOverview(String overview) { this.overview = overview; } + + public String getPosterPath() { return posterPath; } + public void setPosterPath(String posterPath) { this.posterPath = posterPath; } + + public String getBackdropPath() { return backdropPath; } + public void setBackdropPath(String backdropPath) { this.backdropPath = backdropPath; } + + public Double getVoteAverage() { return voteAverage; } + public void setVoteAverage(Double voteAverage) { this.voteAverage = voteAverage; } + + public String getReleaseDate() { return releaseDate; } + public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java new file mode 100644 index 0000000..6c02007 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProfileRequest.java @@ -0,0 +1,40 @@ +package com.example.streamflix.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "Request object for updating an existing profile") +public class UpdateProfileRequest { + + @Schema(description = "New name of the profile", example = "Jane Doe") + private String name; + + @Schema(description = "New ID of the age rating for the profile", example = "2") + private Long ageRatingId; + + @Schema(description = "New URL of the profile image", example = "http://example.com/new_image.jpg") + private String imageUrl; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAgeRatingId() { + return ageRatingId; + } + + public void setAgeRatingId(Long ageRatingId) { + this.ageRatingId = ageRatingId; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java new file mode 100644 index 0000000..6cb83ad --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpdateProgressRequest.java @@ -0,0 +1,28 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpdateProgressRequest { + + @NotNull + private Integer lastPositionSeconds; + + @NotNull + private Integer durationWatchedSeconds; + + public Integer getLastPositionSeconds() { + return lastPositionSeconds; + } + + public void setLastPositionSeconds(Integer lastPositionSeconds) { + this.lastPositionSeconds = lastPositionSeconds; + } + + public Integer getDurationWatchedSeconds() { + return durationWatchedSeconds; + } + + public void setDurationWatchedSeconds(Integer durationWatchedSeconds) { + this.durationWatchedSeconds = durationWatchedSeconds; + } +} diff --git a/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java new file mode 100644 index 0000000..71768fd --- /dev/null +++ b/src/main/java/com/example/streamflix/model/UpgradeSubscriptionRequest.java @@ -0,0 +1,17 @@ +package com.example.streamflix.model; + +import jakarta.validation.constraints.NotNull; + +public class UpgradeSubscriptionRequest { + + @NotNull + private Long tierId; + + public Long getTierId() { + return tierId; + } + + public void setTierId(Long tierId) { + this.tierId = tierId; + } +} diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java new file mode 100644 index 0000000..562787a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java new file mode 100644 index 0000000..37ceced --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/AgeRatingRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.AgeRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AgeRatingRepository extends JpaRepository { + boolean existsByLabel(String label); + Optional findByLabel(String label); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java new file mode 100644 index 0000000..e87053b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Employee; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java new file mode 100644 index 0000000..c6f2021 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -0,0 +1,14 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface EpisodeRepository extends JpaRepository { + List findBySeasonIdOrderByEpisodeNumber(Long seasonId); + Optional findByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java new file mode 100644 index 0000000..25e42a5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/InvitationStatusRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.InvitationStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface InvitationStatusRepository extends JpaRepository { + Optional findByStatus(String status); +} diff --git a/src/main/java/com/example/streamflix/repository/MediaRepository.java b/src/main/java/com/example/streamflix/repository/MediaRepository.java new file mode 100644 index 0000000..8deb3ed --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MediaRepository.java @@ -0,0 +1,13 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Media; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface MediaRepository extends JpaRepository { + Optional findById(Long id); + boolean existsByTitle(String title); +} diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java new file mode 100644 index 0000000..58ce6f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Movie; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface MovieRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/PreferenceRepository.java b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java new file mode 100644 index 0000000..3041843 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/PreferenceRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Preference; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface PreferenceRepository extends JpaRepository { + List findByProfileId(Long profileId); +} diff --git a/src/main/java/com/example/streamflix/repository/ProfileRepository.java b/src/main/java/com/example/streamflix/repository/ProfileRepository.java new file mode 100644 index 0000000..ccb9a1a --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ProfileRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface ProfileRepository extends JpaRepository { + List findByAccountId(Long accountId); +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java new file mode 100644 index 0000000..baa7e2e --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/QualityTypeRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.QualityType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QualityTypeRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/ReferralRepository.java b/src/main/java/com/example/streamflix/repository/ReferralRepository.java new file mode 100644 index 0000000..bb297f2 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ReferralRepository.java @@ -0,0 +1,16 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Referral; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReferralRepository extends JpaRepository { + Optional findByInviterAccountIdAndInviteeAccountId(Long inviterAccountId, Long inviteeAccountId); + List findByInviterAccountId(Long inviterAccountId); + Optional findByInviteeAccountId(Long inviteeAccountId); + List findByDiscountAppliedFalseAndInviteeAccountIdIsNotNull(); +} diff --git a/src/main/java/com/example/streamflix/repository/RoleRepository.java b/src/main/java/com/example/streamflix/repository/RoleRepository.java new file mode 100644 index 0000000..67fbc41 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java new file mode 100644 index 0000000..3ef9e32 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Season; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface SeasonRepository extends JpaRepository { + List findBySeriesIdOrderBySeasonNumber(Long seriesId); +} diff --git a/src/main/java/com/example/streamflix/repository/SeriesRepository.java b/src/main/java/com/example/streamflix/repository/SeriesRepository.java new file mode 100644 index 0000000..e15ec1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SeriesRepository.java @@ -0,0 +1,11 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Series; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SeriesRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java new file mode 100644 index 0000000..51a1c57 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.Subscription; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SubscriptionRepository extends JpaRepository { + Optional findByAccountIdAndIsActiveTrue(Long accountId); +} diff --git a/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java new file mode 100644 index 0000000..a5c808b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/SubscriptionTierRepository.java @@ -0,0 +1,9 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.SubscriptionTier; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + +public interface SubscriptionTierRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java new file mode 100644 index 0000000..6285d35 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/VerificationTokenRepository.java @@ -0,0 +1,8 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.VerificationToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VerificationTokenRepository extends JpaRepository { + VerificationToken findByToken(String token); +} diff --git a/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java new file mode 100644 index 0000000..62f7a1b --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/ViewingProgressRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.ViewingProgress; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface ViewingProgressRepository extends JpaRepository { + List findByProfileIdOrderByStartTimeDesc(Long profileId); + Optional findByProfileIdAndMovieId(Long profileId, Long movieId); + Optional findByProfileIdAndEpisodeId(Long profileId, Long episodeId); +} diff --git a/src/main/java/com/example/streamflix/repository/WatchListRepository.java b/src/main/java/com/example/streamflix/repository/WatchListRepository.java new file mode 100644 index 0000000..6750ec5 --- /dev/null +++ b/src/main/java/com/example/streamflix/repository/WatchListRepository.java @@ -0,0 +1,12 @@ +package com.example.streamflix.repository; + +import com.example.streamflix.entity.WatchList; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; +import java.util.Optional; + +public interface WatchListRepository extends JpaRepository { + List findByProfileIdOrderByAddedDateDesc(Long profileId); + Optional findByProfileIdAndMediaId(Long profileId, Long mediaId); + void deleteByProfileIdAndMediaId(Long profileId, Long mediaId); +} diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java new file mode 100644 index 0000000..c16f019 --- /dev/null +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.streamflix.security; + +import com.example.streamflix.entity.Account; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +public class CustomUserDetails implements UserDetails { + + private final Account account; + + public CustomUserDetails(Account account) { + this.account = account; + } + + public Account getAccount() { + return account; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return account.getPassword(); + } + + @Override + public String getUsername() { + return account.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !account.isBlocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return account.isVerified(); + } +} diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java new file mode 100644 index 0000000..e1aff8c --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.security.CustomUserDetails; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class AccountUserDetailsService implements UserDetailsService { + + private final AccountRepository accountRepository; + + public AccountUserDetailsService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new CustomUserDetails(account); + } +} diff --git a/src/main/java/com/example/streamflix/service/AgeRatingService.java b/src/main/java/com/example/streamflix/service/AgeRatingService.java new file mode 100644 index 0000000..b471762 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/AgeRatingService.java @@ -0,0 +1,28 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.repository.AgeRatingRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AgeRatingService { + + private final AgeRatingRepository ageRatingRepository; + + public AgeRatingService(AgeRatingRepository ageRatingRepository) { + this.ageRatingRepository = ageRatingRepository; + } + + public Long findIdByLabel(String label) { + return ageRatingRepository.findByLabel(label) + .map(AgeRating::getId) + .orElseThrow(() -> + new IllegalArgumentException( + "AgeRating not found with name: " + label + ) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ContentAccessService.java b/src/main/java/com/example/streamflix/service/ContentAccessService.java new file mode 100644 index 0000000..1cd2caf --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ContentAccessService.java @@ -0,0 +1,82 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.repository.SubscriptionRepository; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class ContentAccessService { + + private final SubscriptionRepository subscriptionRepository; + + public ContentAccessService(SubscriptionRepository subscriptionRepository) { + this.subscriptionRepository = subscriptionRepository; + } + + /** + * Checks if the content is allowed for the given profile based on age rating. + * + * @param profile The user profile attempting to access the content. + * @param media The media content being accessed. + * @return true if the profile's age rating allows access to the media, false otherwise. + */ + public boolean isContentAllowed(Profile profile, Media media) { + if (profile == null || media == null) { + return false; + } + + if (profile.getAgeRating() == null || media.getAgeRating() == null) { + return false; + } + + int profileMaxAge = profile.getAgeRating().getMinAge(); + int mediaMinAge = media.getAgeRating().getMinAge(); + + return profileMaxAge >= mediaMinAge; + } + + /** + * Checks if the requested quality is allowed for the user's subscription tier. + * + * @param account The user account. + * @param requestedQuality The quality of the stream being requested. + * @return true if the subscription tier supports the requested quality, false otherwise. + */ + public boolean isQualityAllowed(Account account, MediaQuality requestedQuality) { + if (account == null || requestedQuality == null) { + return false; + } + + Optional activeSubscriptionOpt = subscriptionRepository.findByAccountIdAndIsActiveTrue(account.getId()); + if (activeSubscriptionOpt.isEmpty()) { + return false; // No active subscription + } + + SubscriptionTier tier = activeSubscriptionOpt.get().getTier(); + if (tier == null || tier.getMaxQuality() == null) { + return false; // Subscription tier not configured properly + } + + String maxQuality = tier.getMaxQuality().getName(); + int maxQualityLevel = getQualityLevel(maxQuality); + int requestedQualityLevel = getQualityLevel(requestedQuality.name()); + + return requestedQualityLevel <= maxQualityLevel; + } + + private int getQualityLevel(String quality) { + switch (quality.toUpperCase()) { + case "SD": + return 1; + case "HD": + return 2; + case "UHD": + return 3; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/JwtService.java b/src/main/java/com/example/streamflix/service/JwtService.java new file mode 100644 index 0000000..88292dd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/JwtService.java @@ -0,0 +1,69 @@ +package com.example.streamflix.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.secret}") + private String secretKey; + + @Value("${jwt.expiration:36000000}") // 10 hours default + private long jwtExpiration; + + public String generateToken(String email, Long accountId) { + Map claims = new HashMap<>(); + claims.put("accountId", accountId); + + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + private Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + return Keys.hmacShaKeyFor(keyBytes); + } + + public String extractEmail(String token) { + return extractAllClaims(token).getSubject(); + } + + public Long extractAccountId(String token) { + Claims claims = extractAllClaims(token); + return claims.get("accountId", Long.class); + } + + public boolean isTokenValid(String token, UserDetails userDetails) { + final String email = extractEmail(token); + return (email.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + private boolean isTokenExpired(String token) { + return extractAllClaims(token).getExpiration().before(new Date()); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith((javax.crypto.SecretKey) getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java new file mode 100644 index 0000000..909f633 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -0,0 +1,241 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaDetailsDto; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MediaService +{ + + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public MediaService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + /** + * Get enriched media details with TMDB data + */ + public MediaDetailsDto getMediaDetails(Long mediaId, Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + throw new SecurityException("Content not allowed for this profile's age rating"); + } + + // Build DTO with local data + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + } + + /** + * Get all media available for a profile (filtered by age rating) + */ + public List getAvailableMedia(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return mediaRepository.findAll().stream() + .filter(media -> contentAccessService.isContentAllowed(profile, media)) + .map(media -> { + MediaDetailsDto dto = buildMediaDto(media); + + // Enrich with TMDB data if external ID exists + if (media.getExternalId() != null && !media.getExternalId().isEmpty()) { + enrichWithTmdbData(dto, media.getExternalId()); + } + + return dto; + }) + .collect(Collectors.toList()); + } + + /** + * Build basic MediaDetailsDto from Media entity + */ + private MediaDetailsDto buildMediaDto(Media media) { + MediaDetailsDto dto = new MediaDetailsDto(); + dto.setId(media.getId()); + dto.setTitle(media.getTitle()); + dto.setReleaseDate(media.getReleaseDate()); + dto.setAgeRating(media.getAgeRating().getLabel()); + dto.setExternalId(media.getExternalId()); + + return dto; + } + + /** + * Enrich DTO with TMDB data + */ + private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { + try { + Long tmdbId = Long.parseLong(externalId); + + // Fetch full details + var tmdbDetails = tmdbService.getMovieDetails(tmdbId); + if (tmdbDetails != null) { + if (tmdbDetails.getPosterPath() != null) { + dto.setPosterUrl("https://image.tmdb.org/t/p/w500" + tmdbDetails.getPosterPath()); + } + + dto.setExternalRating(tmdbDetails.getVoteAverage()); + dto.setOverview(tmdbDetails.getOverview()); + + if (tmdbDetails.getBackdropPath() != null) { + dto.setBackdropUrl("https://image.tmdb.org/t/p/original" + tmdbDetails.getBackdropPath()); + } + } + } catch (NumberFormatException e) { + System.err.println("Invalid external ID format: " + externalId); + } catch (Exception e) { + System.err.println("Failed to fetch TMDB data: " + e.getMessage()); + } + } + + public Media createMedia(MediaDetailsDto mediaDetailRequest) { + // to add Admin role checker + + if (mediaDetailRequest == null) { + throw new RuntimeException("Request is null"); + } + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { + if(mediaDetailRequest.getSeriesId() == null){ + throw new RuntimeException("For episode there must be a series id"); + } + + return insertEpisode(mediaDetailRequest); + } + + Media mediaToCreate; + + if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { + mediaToCreate = insertMovie(mediaDetailRequest); + } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { + mediaToCreate = insertSeries(mediaDetailRequest); + } else { + throw new RuntimeException("Incorrect media type"); + } + + return mediaRepository.save(mediaToCreate); + + } + + private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Movie movie = new Movie(); + movie.setTitle(mediaDetailRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); + movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); + movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); + + return movie; + + } + + private Series insertSeries(MediaDetailsDto mediaDetailRequest) { + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Series series = new Series(); + series.setTitle(mediaDetailRequest.getTitle()); + series.setAgeRating(ageRating); + + return series; + } + + private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { + + if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { + throw new IllegalStateException("Episode already exists"); + } + + AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); + + Episode episode = new Episode(); + episode.setTitle(mediaDetailRequest.getTitle()); + + return episodeRepository.save(episode); + + } + +// private Long getExistingAgeRatingId(String ageRatingLabel) +// { +// return ageRatingRepository.findByLabel(ageRatingLabel) +// .map(AgeRating::getId) +// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); +// } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ProfileService.java b/src/main/java/com/example/streamflix/service/ProfileService.java new file mode 100644 index 0000000..5dfe578 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ProfileService.java @@ -0,0 +1,176 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Preference; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.enums.PreferenceType; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.AgeRatingRepository; +import com.example.streamflix.repository.PreferenceRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class ProfileService { + + private final ProfileRepository profileRepository; + private final AccountRepository accountRepository; + private final AgeRatingRepository ageRatingRepository; + private final PreferenceRepository preferenceRepository; + private final SecurityUtils securityUtils; + + private static final int MAX_PROFILES_PER_ACCOUNT = 5; + + public ProfileService(ProfileRepository profileRepository, + AccountRepository accountRepository, + AgeRatingRepository ageRatingRepository, + PreferenceRepository preferenceRepository, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.accountRepository = accountRepository; + this.ageRatingRepository = ageRatingRepository; + this.preferenceRepository = preferenceRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Profile createProfile(String name, Long ageRatingId, String imageUrl, LocalDate birthDate) { + // Get authenticated user's accountId from JWT + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Account account = accountRepository.findById(authenticatedAccountId) + .orElseThrow(() -> new IllegalArgumentException("Account not found")); + + // Limit the number of profiles per account + List existingProfiles = profileRepository.findByAccountId(authenticatedAccountId); + if (existingProfiles.size() >= MAX_PROFILES_PER_ACCOUNT) { + throw new IllegalArgumentException("Maximum number of profiles reached for this account"); + } + + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + + Profile profile = new Profile(account, ageRating, name, imageUrl, birthDate); + return profileRepository.save(profile); + } + + public List getMyProfiles() { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + return profileRepository.findByAccountId(authenticatedAccountId); + } + + public Profile getProfileById(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK - This is the key part! + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to access this profile"); + } + + return profile; + } + + @Transactional + public Profile updateProfile(Long profileId, String name, Long ageRatingId, String imageUrl) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to update this profile"); + } + + if (name != null) { + profile.setName(name); + } + if (ageRatingId != null) { + AgeRating ageRating = ageRatingRepository.findById(ageRatingId) + .orElseThrow(() -> new IllegalArgumentException("Age rating not found")); + profile.setAgeRating(ageRating); + } + if (imageUrl != null) { + profile.setImageUrl(imageUrl); + } + + return profileRepository.save(profile); + } + + @Transactional + public void deleteProfile(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete this profile"); + } + + profileRepository.delete(profile); + } + + @Transactional + public Preference addPreference(Long profileId, PreferenceType preferenceType, String value) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to modify preferences for this profile"); + } + + Preference preference = new Preference(profile, preferenceType, value); + return preferenceRepository.save(preference); + } + + public List getProfilePreferences(Long profileId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to view preferences for this profile"); + } + + return preferenceRepository.findByProfileId(profileId); + } + + @Transactional + public void deletePreference(Long profileId, Long preferenceId) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // AUTHORIZATION CHECK + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to delete preferences for this profile"); + } + + Preference preference = preferenceRepository.findById(preferenceId) + .orElseThrow(() -> new IllegalArgumentException("Preference not found")); + + // Verify the preference belongs to this profile + if (!preference.getProfile().getId().equals(profileId)) { + throw new IllegalArgumentException("Preference does not belong to this profile"); + } + + preferenceRepository.delete(preference); + } +} diff --git a/src/main/java/com/example/streamflix/service/ReferralService.java b/src/main/java/com/example/streamflix/service/ReferralService.java new file mode 100644 index 0000000..fd2512b --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ReferralService.java @@ -0,0 +1,119 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.InvitationStatus; +import com.example.streamflix.entity.Referral; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.model.InvitationResponse; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.InvitationStatusRepository; +import com.example.streamflix.repository.ReferralRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Service +public class ReferralService { + + private final ReferralRepository referralRepository; + private final AccountRepository accountRepository; + private final InvitationStatusRepository invitationStatusRepository; + private final SubscriptionRepository subscriptionRepository; + private final SecurityUtils securityUtils; + + public ReferralService(ReferralRepository referralRepository, + AccountRepository accountRepository, + InvitationStatusRepository invitationStatusRepository, + SubscriptionRepository subscriptionRepository, + SecurityUtils securityUtils) { + this.referralRepository = referralRepository; + this.accountRepository = accountRepository; + this.invitationStatusRepository = invitationStatusRepository; + this.subscriptionRepository = subscriptionRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public InvitationResponse createInvitation() { + Long inviterId = securityUtils.getAuthenticatedAccountId(); + Account inviterAccount = accountRepository.findById(inviterId) + .orElseThrow(() -> new IllegalArgumentException("Inviter account not found")); + + // Check if inviter has an active subscription + Optional activeSubscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(inviterId); + if (activeSubscription.isEmpty()) { + throw new IllegalArgumentException("You must have an active subscription to invite others"); + } + + InvitationStatus pendingStatus = invitationStatusRepository.findByStatus("Pending") + .orElseThrow(() -> new IllegalStateException("Pending status not found in database")); + + Referral referral = new Referral(inviterAccount); + referral.setStatus(pendingStatus); + referral.setDiscountApplied(false); + + Referral savedReferral = referralRepository.save(referral); + + // Generate a simple invite code based on the referral ID + // In a real application, you might want to use a more secure or obfuscated code + String inviteCode = "INVITE-" + savedReferral.getId(); + + return new InvitationResponse(savedReferral, inviteCode); + } + + @Transactional + public Referral acceptInvitation(String inviteCode, Long inviteeAccountId) { + // Decode the inviteCode to get referral ID + Long referralId; + try { + if (inviteCode.startsWith("INVITE-")) { + referralId = Long.parseLong(inviteCode.substring(7)); + } else { + throw new IllegalArgumentException("Invalid invite code format"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid invite code"); + } + + Referral referral = referralRepository.findById(referralId) + .orElseThrow(() -> new IllegalArgumentException("Referral not found")); + + if (!"Pending".equals(referral.getStatus().getStatus())) { + throw new IllegalArgumentException("Invitation is no longer valid"); + } + + if (referral.getInviteeAccount() != null) { + throw new IllegalArgumentException("Invitation has already been accepted"); + } + + if (referral.getInviterAccount().getId().equals(inviteeAccountId)) { + throw new IllegalArgumentException("You cannot accept your own invitation"); + } + + Account inviteeAccount = accountRepository.findById(inviteeAccountId) + .orElseThrow(() -> new IllegalArgumentException("Invitee account not found")); + + InvitationStatus acceptedStatus = invitationStatusRepository.findByStatus("Accepted") + .orElseThrow(() -> new IllegalStateException("Accepted status not found in database")); + + referral.setInviteeAccount(inviteeAccount); + referral.setStatus(acceptedStatus); + + return referralRepository.save(referral); + } + + public List getMyInvitations() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviterAccountId(accountId); + } + + public Optional getMyReferralStatus() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return referralRepository.findByInviteeAccountId(accountId); + } +} diff --git a/src/main/java/com/example/streamflix/service/RegistrationService.java b/src/main/java/com/example/streamflix/service/RegistrationService.java new file mode 100644 index 0000000..e4de1e0 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/RegistrationService.java @@ -0,0 +1,61 @@ +package com.example.streamflix.service; + +import com.example.streamflix.enums.TokenType; +import com.example.streamflix.entity.Account; +import com.example.streamflix.model.RegisterRequest; +import com.example.streamflix.entity.VerificationToken; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.VerificationTokenRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class RegistrationService { + + private static final Logger logger = LoggerFactory.getLogger(RegistrationService.class); + + private final AccountRepository accountRepository; + private final VerificationTokenRepository tokenRepository; + private final PasswordEncoder passwordEncoder; + + public RegistrationService(AccountRepository accountRepository, VerificationTokenRepository tokenRepository, PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.tokenRepository = tokenRepository; + this.passwordEncoder = passwordEncoder; + } + + @Transactional + public void register(RegisterRequest request) { + if (accountRepository.findByEmail(request.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already taken"); + } + + Account account = new Account(); + account.setEmail(request.getEmail()); + account.setPassword(passwordEncoder.encode(request.getPassword())); + account.setFailedLoginAttempts(0); + account.setBlocked(false); + account.setVerified(false); + + accountRepository.save(account); + + String token = UUID.randomUUID().toString(); + VerificationToken verificationToken = new VerificationToken( + token, + account, + LocalDateTime.now().plusHours(24), + TokenType.EMAIL_VERIFICATION + ); + + tokenRepository.save(verificationToken); + + // Log the token for testing purposes since email sending isn't implemented + logger.info("GENERATED VERIFICATION TOKEN for {}: {}", request.getEmail(), token); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/StreamingService.java b/src/main/java/com/example/streamflix/service/StreamingService.java new file mode 100644 index 0000000..85e9443 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/StreamingService.java @@ -0,0 +1,83 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.enums.MediaQuality; +import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.repository.MediaRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; + +@Service +public class StreamingService { + + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ContentAccessService contentAccessService; + private final SecurityUtils securityUtils; + + public StreamingService(ProfileRepository profileRepository, + MediaRepository mediaRepository, + ContentAccessService contentAccessService, + SecurityUtils securityUtils) { + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.contentAccessService = contentAccessService; + this.securityUtils = securityUtils; + } + + /** + * Validates if a profile can stream media at the requested quality + */ + public StreamValidationResult validateStream(Long profileId, Long mediaId, MediaQuality requestedQuality) { + Long authenticatedAccountId = securityUtils.getAuthenticatedAccountId(); + + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new IllegalArgumentException("Profile not found")); + + // Authorization check + if (!profile.getAccount().getId().equals(authenticatedAccountId)) { + throw new SecurityException("You are not authorized to use this profile"); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new IllegalArgumentException("Media not found")); + + StreamValidationResult result = new StreamValidationResult(); + result.setProfileId(profileId); + result.setMediaId(mediaId); + result.setRequestedQuality(requestedQuality); + + // Check age rating + if (!contentAccessService.isContentAllowed(profile, media)) { + result.setAllowed(false); + result.setReason("Content not allowed for this profile's age rating"); + return result; + } + + // Check subscription quality + Account account = profile.getAccount(); + if (!contentAccessService.isQualityAllowed(account, requestedQuality)) { + result.setAllowed(false); + result.setReason("Your subscription does not support " + requestedQuality + " quality"); + result.setSuggestedQuality(getMaxAllowedQuality(account)); + return result; + } + + result.setAllowed(true); + result.setReason("Stream validated successfully"); + result.setSuggestedQuality(requestedQuality); + return result; + } + + private MediaQuality getMaxAllowedQuality(Account account) { + // Implement logic to get max quality from subscription + // This is a simplified version + if (contentAccessService.isQualityAllowed(account, MediaQuality.UHD)) { + return MediaQuality.UHD; + } else if (contentAccessService.isQualityAllowed(account, MediaQuality.HD)) { + return MediaQuality.HD; + } + return MediaQuality.SD; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SubscriptionService.java b/src/main/java/com/example/streamflix/service/SubscriptionService.java new file mode 100644 index 0000000..b71950d --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SubscriptionService.java @@ -0,0 +1,90 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Subscription; +import com.example.streamflix.entity.SubscriptionTier; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.SubscriptionRepository; +import com.example.streamflix.repository.SubscriptionTierRepository; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.time.LocalDate; +import java.util.Optional; + +@Service +public class SubscriptionService { + + private final SubscriptionRepository subscriptionRepository; + private final SubscriptionTierRepository subscriptionTierRepository; + private final AccountRepository accountRepository; + private final SecurityUtils securityUtils; + + public SubscriptionService(SubscriptionRepository subscriptionRepository, SubscriptionTierRepository subscriptionTierRepository, AccountRepository accountRepository, SecurityUtils securityUtils) { + this.subscriptionRepository = subscriptionRepository; + this.subscriptionTierRepository = subscriptionTierRepository; + this.accountRepository = accountRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public Subscription createTrialSubscription(Long accountId, Long tierId) { + Account account = accountRepository.findById(accountId) + .orElseThrow(() -> new NotFoundException("Account not found")); + if (subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId).isPresent()) { + throw new IllegalArgumentException("Account already has an active subscription"); + } + SubscriptionTier tier = subscriptionTierRepository.findById(tierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + Subscription subscription = new Subscription(); + subscription.setAccount(account); + subscription.setTier(tier); + subscription.setStartDate(LocalDate.now()); + subscription.setEndDate(LocalDate.now().plusDays(7)); + subscription.setTrial(true); + subscription.setActive(true); + + return subscriptionRepository.save(subscription); + } + + @Transactional + public Subscription upgradeToPaidSubscription(Long accountId, Long newTierId) throws AccessDeniedException { + if (!securityUtils.isOwner(accountId)) { + throw new AccessDeniedException("You are not authorized to upgrade this subscription."); + } + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + SubscriptionTier newTier = subscriptionTierRepository.findById(newTierId) + .orElseThrow(() -> new NotFoundException("Subscription tier not found")); + + if (subscription.getTrial()) { + subscription.setTrial(false); + subscription.setEndDate(null); + } + subscription.setTier(newTier); + + return subscriptionRepository.save(subscription); + } + + public Optional getMyActiveSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + return subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId); + } + + @Transactional + public Subscription cancelSubscription() { + Long accountId = securityUtils.getAuthenticatedAccountId(); + Subscription subscription = subscriptionRepository.findByAccountIdAndIsActiveTrue(accountId) + .orElseThrow(() -> new NotFoundException("No active subscription found for this account")); + + subscription.setActive(false); + // End date automatically set by database trigger + // subscription.setEndDate(LocalDate.now()); + + return subscriptionRepository.save(subscription); + } +} diff --git a/src/main/java/com/example/streamflix/service/TmdbService.java b/src/main/java/com/example/streamflix/service/TmdbService.java new file mode 100644 index 0000000..83afcae --- /dev/null +++ b/src/main/java/com/example/streamflix/service/TmdbService.java @@ -0,0 +1,38 @@ +package com.example.streamflix.service; + +import com.example.streamflix.model.TmdbMovieResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TmdbService { + + private final WebClient webClient; + + @Value("${tmdb.api.key}") + private String apiKey; + + @Value("${tmdb.api.url}") + private String apiUrl; + + public TmdbService(WebClient webClient) { + this.webClient = webClient; + } + + public TmdbMovieResponse getMovieDetails(Long tmdbId) { + return webClient.get() + .uri(apiUrl + "/movie/{id}?api_key={apiKey}", tmdbId, apiKey) + .retrieve() + .bodyToMono(TmdbMovieResponse.class) + .block(); + } + + public String getMoviePosterUrl(Long tmdbId) { + TmdbMovieResponse movie = getMovieDetails(tmdbId); + if (movie != null && movie.getPosterPath() != null) { + return "https://image.tmdb.org/t/p/w500" + movie.getPosterPath(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/ViewingProgressService.java b/src/main/java/com/example/streamflix/service/ViewingProgressService.java new file mode 100644 index 0000000..dfbc9cd --- /dev/null +++ b/src/main/java/com/example/streamflix/service/ViewingProgressService.java @@ -0,0 +1,154 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Profile; +import com.example.streamflix.entity.ViewingProgress; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.EpisodeRepository; +import com.example.streamflix.repository.MovieRepository; +import com.example.streamflix.repository.ProfileRepository; +import com.example.streamflix.repository.ViewingProgressRepository; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ViewingProgressService { + + private final ViewingProgressRepository viewingProgressRepository; + private final ProfileRepository profileRepository; + private final MovieRepository movieRepository; + private final EpisodeRepository episodeRepository; + private final WatchListService watchListService; + private final SecurityUtils securityUtils; + + @PersistenceContext + private EntityManager entityManager; + + public ViewingProgressService(ViewingProgressRepository viewingProgressRepository, ProfileRepository profileRepository, MovieRepository movieRepository, EpisodeRepository episodeRepository, @Lazy WatchListService watchListService, SecurityUtils securityUtils) { + this.viewingProgressRepository = viewingProgressRepository; + this.profileRepository = profileRepository; + this.movieRepository = movieRepository; + this.episodeRepository = episodeRepository; + this.watchListService = watchListService; + this.securityUtils = securityUtils; + } + + @Transactional + public ViewingProgress startWatching(Long profileId, Long movieId, Long episodeId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if ((movieId == null && episodeId == null) || (movieId != null && episodeId != null)) { + throw new IllegalArgumentException("Either movieId or episodeId must be provided, but not both."); + } + + ViewingProgress viewingProgress = new ViewingProgress(); + viewingProgress.setProfile(profile); + + if (movieId != null) { + Movie movie = movieRepository.findById(movieId) + .orElseThrow(() -> new NotFoundException("Movie not found")); + viewingProgress.setMovie(movie); + } else { + Episode episode = episodeRepository.findById(episodeId) + .orElseThrow(() -> new NotFoundException("Episode not found")); + viewingProgress.setEpisode(episode); + } + + viewingProgress.setDurationWatchedSeconds(0); + viewingProgress.setLastPositionSeconds(0); + viewingProgress.setIsFinished(false); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress updateProgress(Long progressId, Integer lastPositionSeconds, Integer durationWatchedSeconds) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setLastPositionSeconds(lastPositionSeconds); + viewingProgress.setDurationWatchedSeconds(durationWatchedSeconds); + + return viewingProgressRepository.save(viewingProgress); + } + + @Transactional + public ViewingProgress markAsFinished(Long progressId) throws AccessDeniedException { + ViewingProgress viewingProgress = viewingProgressRepository.findById(progressId) + .orElseThrow(() -> new NotFoundException("ViewingProgress not found")); + if (!securityUtils.isOwner(viewingProgress.getProfile().getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this progress."); + } + + viewingProgress.setIsFinished(true); + ViewingProgress savedProgress = viewingProgressRepository.save(viewingProgress); + + // Watchlist auto-removal handled by database trigger + /* + Long profileId = savedProgress.getProfile().getId(); + Long mediaId = null; + if (savedProgress.getMovie() != null) { + mediaId = savedProgress.getMovie().getId(); + } else if (savedProgress.getEpisode() != null) { + mediaId = savedProgress.getEpisode().getSeason().getSeries().getId(); + } + + if (mediaId != null) { + watchListService.checkAndRemoveIfFinished(profileId, mediaId); + } + */ + + return savedProgress; + } + + public List getMyViewingHistory(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return viewingProgressRepository.findByProfileIdOrderByStartTimeDesc(profileId); + } + + public Map getProfileViewingStats(Long profileId) { + // Verify profile belongs to authenticated user + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new SecurityException("You are not authorized to access this profile"); + } + + // Call the database function using native query + Query query = entityManager.createNativeQuery("SELECT * FROM get_profile_viewing_stats(CAST(:profileId AS BIGINT))"); + query.setParameter("profileId", profileId); + + Object[] result = (Object[]) query.getSingleResult(); + + Map stats = new HashMap<>(); + stats.put("total_movies_watched", result[0]); + stats.put("total_episodes_watched", result[1]); + stats.put("total_watch_time_hours", result[2]); + stats.put("unique_content_watched", result[3]); + stats.put("finished_content_count", result[4]); + + return stats; + } +} diff --git a/src/main/java/com/example/streamflix/service/WatchListService.java b/src/main/java/com/example/streamflix/service/WatchListService.java new file mode 100644 index 0000000..9c8c01f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/WatchListService.java @@ -0,0 +1,111 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.*; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.file.AccessDeniedException; +import java.util.List; + +@Service +public class WatchListService { + + private final WatchListRepository watchListRepository; + private final ProfileRepository profileRepository; + private final MediaRepository mediaRepository; + private final ViewingProgressRepository viewingProgressRepository; + private final SeriesRepository seriesRepository; + private final SecurityUtils securityUtils; + + public WatchListService(WatchListRepository watchListRepository, ProfileRepository profileRepository, MediaRepository mediaRepository, ViewingProgressRepository viewingProgressRepository, SeriesRepository seriesRepository, SecurityUtils securityUtils) { + this.watchListRepository = watchListRepository; + this.profileRepository = profileRepository; + this.mediaRepository = mediaRepository; + this.viewingProgressRepository = viewingProgressRepository; + this.seriesRepository = seriesRepository; + this.securityUtils = securityUtils; + } + + @Transactional + public WatchList addToWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); + + // Application-level check - also enforced by database trigger as safety net + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isPresent()) { + throw new IllegalArgumentException("Media already in watch list"); + } + + WatchList watchList = new WatchList(profile, media); + return watchListRepository.save(watchList); + } + + @Transactional + public void removeFromWatchList(Long profileId, Long mediaId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + + if (watchListRepository.findByProfileIdAndMediaId(profileId, mediaId).isEmpty()) { + throw new NotFoundException("Media not found in watch list"); + } + + watchListRepository.deleteByProfileIdAndMediaId(profileId, mediaId); + } + + public List getMyWatchList(Long profileId) throws AccessDeniedException { + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new NotFoundException("Profile not found")); + if (!securityUtils.isOwner(profile.getAccount().getId())) { + throw new AccessDeniedException("You are not authorized to access this profile."); + } + return watchListRepository.findByProfileIdOrderByAddedDateDesc(profileId); + } + + // Logic moved to database trigger: trg_auto_remove_from_watchlist + /* + @Transactional + public void checkAndRemoveIfFinished(Long profileId, Long mediaId) { + Media media = mediaRepository.findById(mediaId).orElse(null); + if (media == null) { + return; + } + + boolean isFinished = false; + if (media instanceof Movie) { + isFinished = viewingProgressRepository.findByProfileIdAndMovieId(profileId, mediaId) + .map(ViewingProgress::getIsFinished) + .orElse(false); + } else if (media instanceof Series) { + Series series = seriesRepository.findById(mediaId).orElse(null); + if (series != null) { + isFinished = series.getSeasons().stream() + .flatMap(season -> season.getEpisodes().stream()) + .allMatch(episode -> viewingProgressRepository.findByProfileIdAndEpisodeId(profileId, episode.getId()) + .map(ViewingProgress::getIsFinished) + .orElse(false)); + } + } + + if (isFinished) { + try { + removeFromWatchList(profileId, mediaId); + } catch (NotFoundException | AccessDeniedException e) { + // Silently catch exception if item is not in the watch list or access is denied + } + } + } + */ +} diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java new file mode 100644 index 0000000..c668910 --- /dev/null +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -0,0 +1,62 @@ +package com.example.streamflix.util; + +import com.example.streamflix.service.JwtService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Component +public class SecurityUtils { + + private final JwtService jwtService; + + public SecurityUtils(JwtService jwtService) { + this.jwtService = jwtService; + } + + /** + * Extracts the accountId from the JWT token in the current request + */ + public Long getAuthenticatedAccountId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + throw new IllegalStateException("No request context available"); + } + + HttpServletRequest request = attributes.getRequest(); + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new IllegalStateException("No valid JWT token found"); + } + + String token = authHeader.substring(7); + return jwtService.extractAccountId(token); + } + + public String getAuthenticatedEmail() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + throw new IllegalStateException("User is not authenticated"); + } + return authentication.getName(); + } + + public boolean isOwner(Long accountId) { + try { + Long authenticatedAccountId = getAuthenticatedAccountId(); + return authenticatedAccountId.equals(accountId); + } catch (IllegalStateException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/streamflix/StreamflixApplicationTests.java b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java new file mode 100644 index 0000000..269bd8f --- /dev/null +++ b/src/test/java/com/example/streamflix/StreamflixApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.streamflix; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StreamflixApplicationTests { + + @Test + void contextLoads() { + } + +} From da5e90798992cdb0f1fb796186dc866d9043ebca Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Thu, 8 Jan 2026 22:41:19 +0100 Subject: [PATCH 17/22] working post movie and series method based on account Roles (still need some fixes for Series) --- init-db/03_schema.sql | 3 ++- .../streamflix/config/SecurityConfig.java | 5 ++++- .../com/example/streamflix/entity/Account.java | 14 ++++++++++++++ .../streamflix/security/CustomUserDetails.java | 18 +++++++++++++++++- .../streamflix/service/MediaService.java | 4 +--- .../example/streamflix/util/SecurityUtils.java | 3 +++ 6 files changed, 41 insertions(+), 6 deletions(-) diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql index e5b33aa..fde572c 100644 --- a/init-db/03_schema.sql +++ b/init-db/03_schema.sql @@ -50,7 +50,8 @@ CREATE TABLE accounts ( is_verified BOOLEAN DEFAULT FALSE, failed_login_attempts INT DEFAULT 0, is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE + discount_used BOOLEAN DEFAULT FALSE, + role_id int references role(id) ); -- Verification and password recovery tokens diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java index 065bc3b..373c551 100644 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -31,7 +31,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() - .requestMatchers("/api/v1/media/createNewMedia").permitAll() +// .requestMatchers("/api/v1/media/createNewMedia").permitAll() +// .requestMatchers("/api/v1/media/createNewMedia").hasAnyRole("Junior", "Mid-level", "Senior") + .requestMatchers(org.springframework.http.HttpMethod.POST, "/api/v1/media/createNewMedia") + .hasAnyRole("Junior", "Mid-level", "Senior") .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/api/v1/analytics/admin/**").authenticated() .requestMatchers("/api/v1/analytics/**").permitAll() diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java index 800f360..6b0bc25 100644 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -43,6 +43,12 @@ public class Account { @Schema(description = "Indicates if the discount has been used", example = "false") private boolean discountUsed = false; + @JsonIgnore + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "role_id") + @Schema(description = "Account role", example = "") + private Role role; + public Account() { } @@ -106,4 +112,12 @@ public boolean isDiscountUsed() { public void setDiscountUsed(boolean discountUsed) { this.discountUsed = discountUsed; } + + public Role getRole() { + return this.role; + } + + public void setRole(Role role) { + this.role = role; + } } diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java index c16f019..55922d9 100644 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -2,10 +2,12 @@ import com.example.streamflix.entity.Account; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; +import java.util.List; public class CustomUserDetails implements UserDetails { @@ -21,7 +23,21 @@ public Account getAccount() { @Override public Collection getAuthorities() { - return Collections.emptyList(); + if (account.getRole() == null || account.getRole().getId() == null) { + return List.of(); + } + + String name = ""; + if (account.getRole().getId() == 1) name = "Junior"; + else if (account.getRole().getId() == 2) name = "Mid-level"; + else if (account.getRole().getId() == 3) name = "Senior"; + else name = null; + + if(name == null) { + return List.of(); + } + + return List.of(new SimpleGrantedAuthority("ROLE_" + name)); } @Override diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java index 909f633..0757f09 100644 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -149,7 +149,6 @@ private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { } public Media createMedia(MediaDetailsDto mediaDetailRequest) { - // to add Admin role checker if (mediaDetailRequest == null) { throw new RuntimeException("Request is null"); @@ -216,10 +215,9 @@ private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { throw new IllegalStateException("Episode already exists"); } - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - Episode episode = new Episode(); episode.setTitle(mediaDetailRequest.getTitle()); +// episode.setSeason(mediaDetailRequest.); return episodeRepository.save(episode); diff --git a/src/main/java/com/example/streamflix/util/SecurityUtils.java b/src/main/java/com/example/streamflix/util/SecurityUtils.java index c668910..d4a36ac 100644 --- a/src/main/java/com/example/streamflix/util/SecurityUtils.java +++ b/src/main/java/com/example/streamflix/util/SecurityUtils.java @@ -3,11 +3,14 @@ import com.example.streamflix.service.JwtService; import jakarta.servlet.http.HttpServletRequest; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.*; + @Component public class SecurityUtils { From 9936f5974e099dd5ebf462503408c947043ea6c4 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Sun, 11 Jan 2026 13:26:45 +0100 Subject: [PATCH 18/22] working post movie and series querys --- .../streamflix/config/SecurityConfig.java | 2 +- .../controller/MediaController.java | 60 ++++- .../example/streamflix/entity/Episode.java | 3 +- .../com/example/streamflix/entity/Media.java | 3 + .../com/example/streamflix/entity/Series.java | 25 ++- .../streamflix/model/EpisodeRequest.java | 71 ++++++ .../model/MediaCreationRequest.java | 208 +++++++++++++++++ .../example/streamflix/model/MovieMapper.java | 5 + .../streamflix/model/MovieRequest.java | 131 +++++++++++ .../model/SeriesCreationRequest.java | 212 ++++++++++++++++++ .../repository/EpisodeRepository.java | 3 + .../repository/MovieRepository.java | 3 + .../repository/SeasonRepository.java | 5 + .../streamflix/service/MediaService.java | 129 +++++------ .../streamflix/service/MovieService.java | 134 +++++++++++ .../streamflix/service/SeriesService.java | 182 +++++++++++++++ 16 files changed, 1091 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/example/streamflix/model/EpisodeRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MediaCreationRequest.java create mode 100644 src/main/java/com/example/streamflix/model/MovieMapper.java create mode 100644 src/main/java/com/example/streamflix/model/MovieRequest.java create mode 100644 src/main/java/com/example/streamflix/model/SeriesCreationRequest.java create mode 100644 src/main/java/com/example/streamflix/service/MovieService.java create mode 100644 src/main/java/com/example/streamflix/service/SeriesService.java diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java index 373c551..d916d81 100644 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -33,7 +33,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() // .requestMatchers("/api/v1/media/createNewMedia").permitAll() // .requestMatchers("/api/v1/media/createNewMedia").hasAnyRole("Junior", "Mid-level", "Senior") - .requestMatchers(org.springframework.http.HttpMethod.POST, "/api/v1/media/createNewMedia") + .requestMatchers(org.springframework.http.HttpMethod.POST, "/api/v1/media/admin/**") .hasAnyRole("Junior", "Mid-level", "Senior") .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/api/v1/analytics/admin/**").authenticated() diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java index 59babe7..788e016 100644 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -1,10 +1,12 @@ package com.example.streamflix.controller; import com.example.streamflix.entity.Media; -import com.example.streamflix.model.MediaDetailsDto; -import com.example.streamflix.model.StreamValidationResult; +import com.example.streamflix.entity.Series; +import com.example.streamflix.model.*; import com.example.streamflix.enums.MediaQuality; import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.MovieService; +import com.example.streamflix.service.SeriesService; import com.example.streamflix.service.StreamingService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -26,10 +28,14 @@ public class MediaController { private final MediaService mediaService; private final StreamingService streamingService; + private final MovieService movieService; + private final SeriesService seriesService; - public MediaController(MediaService mediaService, StreamingService streamingService) { + public MediaController(MediaService mediaService, StreamingService streamingService, MovieService movieService, SeriesService seriesService) { this.mediaService = mediaService; this.streamingService = streamingService; + this.movieService = movieService; + this.seriesService = seriesService; } @Operation(summary = "Get available media", description = "Retrieve a list of media available for a specific profile") @@ -78,11 +84,53 @@ public ResponseEntity validateStream( @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), @ApiResponse(responseCode = "400", description = "Media upload failed") }) - @PostMapping("/createNewMedia") - public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ +// @PostMapping("/createNewMedia") +// public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ +// +// Object created = mediaService.createMedia(mediaDetailsRequest); +// return ResponseEntity.status(HttpStatus.CREATED).body(created); +// } - Object created = mediaService.createMedia(mediaDetailsRequest); + @PostMapping("/admin/movie") + public ResponseEntity createNewMovie(@RequestBody MovieRequest movieRequest){ + + Object created = movieService.createMovie(movieRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @DeleteMapping("/admin/{mediaId}") + public ResponseEntity deleteMedia(@PathVariable Long mediaId) { + mediaService.deleteMedia(mediaId); + return ResponseEntity.noContent().build(); + } + @PutMapping("/admin/movie/{movieId}") + public ResponseEntity updateMovieById(@PathVariable Long movieId, @RequestBody MovieRequest movieRequest){ + Object updated = movieService.updateMovie(movieId, movieRequest); + return ResponseEntity.ok(updated); + } + + @PostMapping("/admin/series") + public ResponseEntity createNewSeries(@RequestBody SeriesCreationRequest seriesCreationRequest){ + + Object created = seriesService.createFullSeries(seriesCreationRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @PostMapping("/admin/series/season") + public ResponseEntity createNewSeason(@RequestBody SeriesCreationRequest seriesCreationRequest){ + + Object created = seriesService.createSeason(seriesCreationRequest); return ResponseEntity.status(HttpStatus.CREATED).body(created); } + @PostMapping("/admin/series/episode") + public ResponseEntity createNewEpisode(@RequestBody EpisodeRequest episodeRequest) { + Object created = seriesService.createEpisode(episodeRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + +// @PostMapping("/seasons") + + } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/entity/Episode.java b/src/main/java/com/example/streamflix/entity/Episode.java index 4ec6fa6..4d8f0c6 100644 --- a/src/main/java/com/example/streamflix/entity/Episode.java +++ b/src/main/java/com/example/streamflix/entity/Episode.java @@ -1,13 +1,14 @@ package com.example.streamflix.entity; import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import java.util.List; @Entity @Table(name = "episode") -public class Episode extends Media { +public class Episode { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/example/streamflix/entity/Media.java b/src/main/java/com/example/streamflix/entity/Media.java index b7c9085..c6da83c 100644 --- a/src/main/java/com/example/streamflix/entity/Media.java +++ b/src/main/java/com/example/streamflix/entity/Media.java @@ -1,5 +1,7 @@ package com.example.streamflix.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonManagedReference; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; @@ -11,6 +13,7 @@ @DiscriminatorColumn(name = "dtype", discriminatorType = DiscriminatorType.STRING) @Table(name = "media") @Schema(description = "Abstract Media entity representing common media properties") +@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) public abstract class Media { @Id diff --git a/src/main/java/com/example/streamflix/entity/Series.java b/src/main/java/com/example/streamflix/entity/Series.java index a2a05c6..8c8b363 100644 --- a/src/main/java/com/example/streamflix/entity/Series.java +++ b/src/main/java/com/example/streamflix/entity/Series.java @@ -27,9 +27,26 @@ public Series(AgeRating ageRating, String title, java.time.LocalDate releaseDate this.description = description; } - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + + public List getSeasons() { + return seasons; + } + + public void setSeasons(List seasons) { + this.seasons = seasons; + } - public List getSeasons() { return seasons; } - public void setSeasons(List seasons) { this.seasons = seasons; } + public void addSeason(Season season) { + if (this.seasons == null) { + this.seasons = new ArrayList<>(); + } + this.seasons.add(season); + season.setSeries(this); + } } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/EpisodeRequest.java b/src/main/java/com/example/streamflix/model/EpisodeRequest.java new file mode 100644 index 0000000..f6631ef --- /dev/null +++ b/src/main/java/com/example/streamflix/model/EpisodeRequest.java @@ -0,0 +1,71 @@ +package com.example.streamflix.model; + +public class EpisodeRequest +{ + private Long id; + private String title; + private int durationSeconds; + private String videoUrl; + private int episodeNumber; + private Long seasonId; + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public int getDurationSeconds() + { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) + { + this.durationSeconds = durationSeconds; + } + + public String getVideoUrl() + { + return videoUrl; + } + + public void setVideoUrl(String videoUrl) + { + this.videoUrl = videoUrl; + } + + public int getEpisodeNumber() + { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) + { + this.episodeNumber = episodeNumber; + } + + public Long getSeasonId() + { + return this.seasonId; + } + + public void setSeasonId(Long seasonId) + { + this.seasonId = seasonId; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/model/MediaCreationRequest.java b/src/main/java/com/example/streamflix/model/MediaCreationRequest.java new file mode 100644 index 0000000..b1e3433 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MediaCreationRequest.java @@ -0,0 +1,208 @@ +package com.example.streamflix.model; + +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Season; +import com.example.streamflix.entity.Series; + +import java.time.LocalDate; + +public class MediaCreationRequest +{ + private Long id; + + //Fields for both media + private String ageRating; + private String title; + private LocalDate releaseDate; + private String externalId; + private String mediaType; + + //Series specific + private String seriesDescription; + + //Season specific + private Series series; + private int seasonNumber; + + //Episode specific + private Season season; + private Episode episode; + private int episodeNumber; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + public Long getId() + { + return this.id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getAgeRating() + { + return this.ageRating; + } + + public void setAgeRating(String ageRating) + { + this.ageRating = ageRating; + } + + public String getTitle() + { + return this.title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public LocalDate getReleaseDate() + { + return this.releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) + { + this.releaseDate = releaseDate; + } + + public String getExternalId() + { + return this.externalId; + } + + public void setExternalId(String externalId) + { + this.externalId = externalId; + } + + public String getMediaType() + { + return this.mediaType; + } + + public void setMediaType(String mediaType) + { + this.mediaType = mediaType; + } + + public String getSeriesDescription() + { + return this.seriesDescription; + } + + public void setSeriesDescription(String seriesDescription) + { + this.seriesDescription = seriesDescription; + } + + public Series getSeries() + { + return this.series; + } + + public void setSeries(Series series) + { + this.series = series; + } + + public int getSeasonNumber() + { + return this.seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) + { + this.seasonNumber = seasonNumber; + } + + public Season getSeason() + { + return this.season; + } + + public void setSeason(Season season) + { + this.season = season; + } + + public Episode getEpisode() + { + return this.episode; + } + + public void setEpisode(Episode episode) + { + this.episode = episode; + } + + public int getEpisodeNumber() + { + return this.episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) + { + this.episodeNumber = episodeNumber; + } + + public int getDurationInSecond() + { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) + { + this.durationInSecond = durationInSecond; + } + + public String getPosterUrl() + { + return this.posterUrl; + } + + public void setPosterUrl(String posterUrl) + { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() + { + return this.backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) + { + this.backdropUrl = backdropUrl; + } + + public String getOverview() + { + return this.overview; + } + + public void setOverview(String overview) + { + this.overview = overview; + } + + public Double getExternalRating() + { + return this.externalRating; + } + + public void setExternalRating(Double externalRating) + { + this.externalRating = externalRating; + } +} diff --git a/src/main/java/com/example/streamflix/model/MovieMapper.java b/src/main/java/com/example/streamflix/model/MovieMapper.java new file mode 100644 index 0000000..af0b08a --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MovieMapper.java @@ -0,0 +1,5 @@ +package com.example.streamflix.model; + +public class MovieMapper +{ +} diff --git a/src/main/java/com/example/streamflix/model/MovieRequest.java b/src/main/java/com/example/streamflix/model/MovieRequest.java new file mode 100644 index 0000000..f619fa0 --- /dev/null +++ b/src/main/java/com/example/streamflix/model/MovieRequest.java @@ -0,0 +1,131 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; + +public class MovieRequest +{ + + private Long id; + private String ageRating; + private String title; + private LocalDate releaseDate; + private String externalId; + private String mediaType; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + public Long getId() + { + return this.id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getAgeRating() + { + return this.ageRating; + } + + public void setAgeRating(String ageRating) + { + this.ageRating = ageRating; + } + + public String getTitle() + { + return this.title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public LocalDate getReleaseDate() + { + return this.releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) + { + this.releaseDate = releaseDate; + } + + public String getExternalId() + { + return this.externalId; + } + + public void setExternalId(String externalId) + { + this.externalId = externalId; + } + + public String getMediaType() + { + return this.mediaType; + } + + public void setMediaType(String mediaType) + { + this.mediaType = mediaType; + } + + public int getDurationInSecond() + { + return this.durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) + { + this.durationInSecond = durationInSecond; + } + + public String getPosterUrl() + { + return this.posterUrl; + } + + public void setPosterUrl(String posterUrl) + { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() + { + return this.backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) + { + this.backdropUrl = backdropUrl; + } + + public String getOverview() + { + return this.overview; + } + + public void setOverview(String overview) + { + this.overview = overview; + } + + public Double getExternalRating() + { + return this.externalRating; + } + + public void setExternalRating(Double externalRating) + { + this.externalRating = externalRating; + } +} diff --git a/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java b/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java new file mode 100644 index 0000000..d87d2ac --- /dev/null +++ b/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java @@ -0,0 +1,212 @@ +package com.example.streamflix.model; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public class SeriesCreationRequest +{ + private Long id; + private String title; + private LocalDate releaseDate; + private String ageRating; + private String externalId; + private String mediaType; + private String description; + + private Long seriesId; + private Long seasonId; + private int seasonNumber; + + private EpisodeRequest episode; + private final List episodeList = new ArrayList<>(); + private int episodeNumber; + private int durationInSecond; + + // TMDB enriched data + private String posterUrl; + private String backdropUrl; + private String overview; + private Double externalRating; + + public Long getId() + { + return id; + } + + public void setId(Long id) + { + this.id = id; + } + + public String getTitle() + { + return title; + } + + public void setTitle(String title) + { + this.title = title; + } + + public LocalDate getReleaseDate() + { + return releaseDate; + } + + public void setReleaseDate(LocalDate releaseDate) + { + this.releaseDate = releaseDate; + } + + public String getAgeRating() + { + return ageRating; + } + + public void setAgeRating(String ageRating) + { + this.ageRating = ageRating; + } + + public String getExternalId() + { + return externalId; + } + + public void setExternalId(String externalId) + { + this.externalId = externalId; + } + + public String getMediaType() + { + return mediaType; + } + + public void setMediaType(String mediaType) + { + this.mediaType = mediaType; + } + + public String getDescription() + { + return this.description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public Long getSeriesId() + { + return seriesId; + } + + public void setSeriesId(Long seriesId) + { + this.seriesId = seriesId; + } + + public Long getSeasonId() + { + return seasonId; + } + + public void setSeasonId(Long seasonId) + { + this.seasonId = seasonId; + } + + public int getSeasonNumber() + { + return seasonNumber; + } + + public void setSeasonNumber(int seasonNumber) + { + this.seasonNumber = seasonNumber; + } + + public EpisodeRequest getEpisode() + { + return episode; + } + + public void setEpisode(EpisodeRequest episode) + { + this.episode = episode; + } + + public List getEpisodeList() + { + return episodeList; + } + + + public void addEpisode(EpisodeRequest episode) + { + this.episodeList.add(episode); + } + + public int getEpisodeNumber() + { + return episodeNumber; + } + + public void setEpisodeNumber(int episodeNumber) + { + this.episodeNumber = episodeNumber; + } + + public int getDurationInSecond() + { + return durationInSecond; + } + + public void setDurationInSecond(int durationInSecond) + { + this.durationInSecond = durationInSecond; + } + + public String getPosterUrl() + { + return posterUrl; + } + + public void setPosterUrl(String posterUrl) + { + this.posterUrl = posterUrl; + } + + public String getBackdropUrl() + { + return backdropUrl; + } + + public void setBackdropUrl(String backdropUrl) + { + this.backdropUrl = backdropUrl; + } + + public String getOverview() + { + return overview; + } + + public void setOverview(String overview) + { + this.overview = overview; + } + + public Double getExternalRating() + { + return externalRating; + } + + public void setExternalRating(Double externalRating) + { + this.externalRating = externalRating; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java index c6f2021..8af6169 100644 --- a/src/main/java/com/example/streamflix/repository/EpisodeRepository.java +++ b/src/main/java/com/example/streamflix/repository/EpisodeRepository.java @@ -11,4 +11,7 @@ public interface EpisodeRepository extends JpaRepository { List findBySeasonIdOrderByEpisodeNumber(Long seasonId); Optional findByTitle(String title); + boolean existsByTitle(String title); + boolean existsBySeasonAndTitle(String title, Long seasonID,int episodeNumber); + boolean existsBySeasonIdAndEpisodeNumber(Long seasonId, int seasonNumber); } diff --git a/src/main/java/com/example/streamflix/repository/MovieRepository.java b/src/main/java/com/example/streamflix/repository/MovieRepository.java index 58ce6f2..dc11c70 100644 --- a/src/main/java/com/example/streamflix/repository/MovieRepository.java +++ b/src/main/java/com/example/streamflix/repository/MovieRepository.java @@ -2,7 +2,10 @@ import com.example.streamflix.entity.Movie; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.Optional; +@Repository public interface MovieRepository extends JpaRepository { } diff --git a/src/main/java/com/example/streamflix/repository/SeasonRepository.java b/src/main/java/com/example/streamflix/repository/SeasonRepository.java index 3ef9e32..28c54db 100644 --- a/src/main/java/com/example/streamflix/repository/SeasonRepository.java +++ b/src/main/java/com/example/streamflix/repository/SeasonRepository.java @@ -1,9 +1,14 @@ package com.example.streamflix.repository; import com.example.streamflix.entity.Season; +import com.example.streamflix.entity.Series; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.List; +@Repository public interface SeasonRepository extends JpaRepository { List findBySeriesIdOrderBySeasonNumber(Long seriesId); + boolean existsBySeriesIdAndSeasonNumber(Long seriesId, int number); } diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java index 0757f09..473b052 100644 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -1,13 +1,17 @@ package com.example.streamflix.service; import com.example.streamflix.entity.*; +import com.example.streamflix.model.MediaCreationRequest; import com.example.streamflix.model.MediaDetailsDto; import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.SeriesCreationRequest; import com.example.streamflix.repository.*; import com.example.streamflix.util.SecurityUtils; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -19,6 +23,7 @@ public class MediaService private final AgeRatingRepository ageRatingRepository; private final EpisodeRepository episodeRepository; private final SeriesRepository seriesRepository; + private final SeasonRepository seasonRepository; private final TmdbService tmdbService; private final ContentAccessService contentAccessService; private final AgeRatingService ageRatingService; @@ -29,6 +34,7 @@ public MediaService(MediaRepository mediaRepository, AgeRatingRepository ageRatingRepository, EpisodeRepository episodeRepository, SeriesRepository seriesRepository, + SeasonRepository seasonRepository, TmdbService tmdbService, ContentAccessService contentAccessService, AgeRatingService ageRatingService, @@ -39,6 +45,7 @@ public MediaService(MediaRepository mediaRepository, this.ageRatingRepository = ageRatingRepository; this.episodeRepository = episodeRepository; this.seriesRepository = seriesRepository; + this.seasonRepository = seasonRepository; this.tmdbService = tmdbService; this.contentAccessService = contentAccessService; this.ageRatingService = ageRatingService; @@ -148,88 +155,64 @@ private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { } } - public Media createMedia(MediaDetailsDto mediaDetailRequest) { - if (mediaDetailRequest == null) { - throw new RuntimeException("Request is null"); - } - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Episode")) { - if(mediaDetailRequest.getSeriesId() == null){ - throw new RuntimeException("For episode there must be a series id"); - } - - return insertEpisode(mediaDetailRequest); - } - - Media mediaToCreate; - - if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Movie")) { - mediaToCreate = insertMovie(mediaDetailRequest); - } else if (mediaDetailRequest.getMediaType().equalsIgnoreCase("Series")) { - mediaToCreate = insertSeries(mediaDetailRequest); - } else { - throw new RuntimeException("Incorrect media type"); - } - - return mediaRepository.save(mediaToCreate); - - } - - private Movie insertMovie(MediaDetailsDto mediaDetailRequest) { - - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Movie already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Movie movie = new Movie(); - movie.setTitle(mediaDetailRequest.getTitle()); - movie.setAgeRating(ageRating); - movie.setReleaseDate(mediaDetailRequest.getReleaseDate()); - movie.setDurationSeconds(mediaDetailRequest.getDurationInSecond()); - movie.setVideoUrl(mediaDetailRequest.getBackdropUrl()); - - return movie; - } - - private Series insertSeries(MediaDetailsDto mediaDetailRequest) { - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Series already exists"); - } - - AgeRating ageRating = getAgeRating(mediaDetailRequest.getAgeRating()); - - Series series = new Series(); - series.setTitle(mediaDetailRequest.getTitle()); - series.setAgeRating(ageRating); - - return series; - } +// private Season createSeasonForNewSeries(SeriesCreationRequest request) +// { +// int seasonNumberToAdd = request.getSeasonNumber(); +// Series seriesToAddForSeason = request.getSeries(); +// +// if (seriesToAddForSeason.getId() == null) { +// throw new RuntimeException("Series (with id) must be provided"); +// } +// +// Series managedSeries = seriesRepository.findById(seriesToAddForSeason.getId()) +// .orElseThrow(() -> new RuntimeException("Series does not exist")); +// +// if(seasonRepository.existsBySeriesIdAndSeasonNumber(managedSeries.getId(),seasonNumberToAdd)){ +// throw new RuntimeException("Season already exists"); +// } +// +// Season seasonToAdd = new Season(); +// seasonToAdd.setSeasonNumber(seasonNumberToAdd); +// seasonToAdd.setSeries(managedSeries); +// +// return seasonRepository.save(seasonToAdd); +// } - private Episode insertEpisode(MediaDetailsDto mediaDetailRequest) { +// private Episode createNewEpisode(SeriesCreationRequest request) +// { +// if(request.getTitle() == null || request.getSeason() == null || request.getBackdropUrl() == null) { +// throw new RuntimeException("Some important data is null"); +// } +// +// Season managedSeason = seasonRepository.findById(request.getSeason().getId()) +// .orElseThrow(() -> new RuntimeException("Season does not exist")); +// +// if(episodeRepository.findByTitle(request.getTitle()).isPresent() +// && episodeRepository.existsBySeasonIdAndEpisodeNumber(request.getSeason().getId(), request.getEpisodeNumber())) { +// throw new RuntimeException("Title already exist or episodeNumberAlreadyOccupied"); +// } +// +// Episode episode = new Episode(); +// episode.setTitle(request.getTitle()); +// episode.setSeason(managedSeason); +// episode.setEpisodeNumber(request.getEpisodeNumber()); +// +// return episodeRepository.save(episode); +// } - if (mediaRepository.existsByTitle(mediaDetailRequest.getTitle())) { - throw new IllegalStateException("Episode already exists"); - } + @Transactional + public Media deleteMedia(Long mediaId) { - Episode episode = new Episode(); - episode.setTitle(mediaDetailRequest.getTitle()); -// episode.setSeason(mediaDetailRequest.); + Media media = mediaRepository.findById(mediaId) + .orElseThrow(() -> new NotFoundException("Media not found")); - return episodeRepository.save(episode); + mediaRepository.delete(media); + return media; } -// private Long getExistingAgeRatingId(String ageRatingLabel) -// { -// return ageRatingRepository.findByLabel(ageRatingLabel) -// .map(AgeRating::getId) -// .orElseThrow(() -> new NotFoundException("Age Rating not found: " + ageRatingLabel)); -// } - private AgeRating getAgeRating(String label) { return ageRatingRepository.findByLabel(label) .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); diff --git a/src/main/java/com/example/streamflix/service/MovieService.java b/src/main/java/com/example/streamflix/service/MovieService.java new file mode 100644 index 0000000..5ec5a57 --- /dev/null +++ b/src/main/java/com/example/streamflix/service/MovieService.java @@ -0,0 +1,134 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Movie; +import com.example.streamflix.entity.Series; +import com.example.streamflix.exception.NotFoundException; +import com.example.streamflix.model.MediaCreationRequest; +import com.example.streamflix.model.MovieRequest; +import com.example.streamflix.model.SeriesCreationRequest; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class MovieService +{ + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final SeasonRepository seasonRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + private final MovieRepository movieRepository; + + public MovieService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + SeasonRepository seasonRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils, MovieRepository movieRepository) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.seasonRepository = seasonRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + this.movieRepository = movieRepository; + } + + public Movie createMovie(MovieRequest movieRequest) { + + if (mediaRepository.existsByTitle(movieRequest.getTitle())) { + throw new IllegalStateException("Movie already exists"); + } + + AgeRating ageRating = (ageRatingRepository.findByLabel(movieRequest.getAgeRating())) + .orElseThrow(() -> new RuntimeException("ageRating is not found")); + + + Movie movie = new Movie(); + movie.setTitle(movieRequest.getTitle()); + movie.setAgeRating(ageRating); + movie.setReleaseDate(movieRequest.getReleaseDate()); + movie.setDurationSeconds(movieRequest.getDurationInSecond()); + movie.setVideoUrl(movieRequest.getBackdropUrl()); + + return mediaRepository.save(movie); + } + + + @Transactional + public Movie deleteMovieById(Long id) { + + Movie movieToDelete = movieRepository.findById(id) + .orElseThrow(()->new RuntimeException("Movie is not found")); + + movieRepository.delete(movieToDelete); + + return movieToDelete; + } + + @Transactional + public Movie updateMovie(Long movieId, MovieRequest movieRequest) + { + Movie movieToEdit = movieRepository.findById(movieId) + .orElseThrow(()-> new RuntimeException("No movie to edit has been found")); + + if(movieRequest.getTitle() != null) + { + movieToEdit.setTitle(movieRequest.getTitle()); + } + + if(movieRequest.getAgeRating() != null) + { + movieToEdit.setAgeRating(getAgeRating(movieRequest.getAgeRating())); + } + + if(movieRequest.getBackdropUrl() != null) + { + movieToEdit.setVideoUrl(movieRequest.getBackdropUrl()); + } + + if(movieRequest.getDurationInSecond() > 0) + { + movieToEdit.setDurationSeconds(movieRequest.getDurationInSecond()); + } + + return movieToEdit; + } + + private AgeRating getAgeRating(String label) { + return ageRatingRepository.findByLabel(label) + .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); + } + + public Movie getMovieById(Long id) + { + return movieRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Movie not found: " + id)); + } + + public List getAllMovies() + { + return movieRepository.findAll(); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SeriesService.java b/src/main/java/com/example/streamflix/service/SeriesService.java new file mode 100644 index 0000000..17e875f --- /dev/null +++ b/src/main/java/com/example/streamflix/service/SeriesService.java @@ -0,0 +1,182 @@ +package com.example.streamflix.service; + +import com.example.streamflix.entity.AgeRating; +import com.example.streamflix.entity.Episode; +import com.example.streamflix.entity.Season; +import com.example.streamflix.entity.Series; +import com.example.streamflix.model.EpisodeRequest; +import com.example.streamflix.model.SeriesCreationRequest; +import com.example.streamflix.repository.*; +import com.example.streamflix.util.SecurityUtils; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.*; + +@Service +public class SeriesService +{ + private final MediaRepository mediaRepository; + private final ProfileRepository profileRepository; + private final AgeRatingRepository ageRatingRepository; + private final EpisodeRepository episodeRepository; + private final SeriesRepository seriesRepository; + private final SeasonRepository seasonRepository; + private final TmdbService tmdbService; + private final ContentAccessService contentAccessService; + private final AgeRatingService ageRatingService; + private final SecurityUtils securityUtils; + + public SeriesService(MediaRepository mediaRepository, + ProfileRepository profileRepository, + AgeRatingRepository ageRatingRepository, + EpisodeRepository episodeRepository, + SeriesRepository seriesRepository, + SeasonRepository seasonRepository, + TmdbService tmdbService, + ContentAccessService contentAccessService, + AgeRatingService ageRatingService, + SecurityUtils securityUtils) + { + this.mediaRepository = mediaRepository; + this.profileRepository = profileRepository; + this.ageRatingRepository = ageRatingRepository; + this.episodeRepository = episodeRepository; + this.seriesRepository = seriesRepository; + this.seasonRepository = seasonRepository; + this.tmdbService = tmdbService; + this.contentAccessService = contentAccessService; + this.ageRatingService = ageRatingService; + this.securityUtils = securityUtils; + } + + + public Series createFullSeries(SeriesCreationRequest request) { + + Series series = createSeries(request); + Season season = createNewSeasonForSeries(series, request.getSeasonNumber()); + seasonRepository.save(season); + + for(EpisodeRequest epDto : request.getEpisodeList()) { + + epDto.setSeasonId(season.getId()); + createNewEpisode(epDto); + } + + + return seriesRepository.save(series); + } + + public Series createSeries(SeriesCreationRequest request) { + + if(request == null){ + throw new IllegalArgumentException("Series creation request cannot be null"); + } + + if (mediaRepository.existsByTitle(request.getTitle())) { + throw new IllegalStateException("Series already exists"); + } + + AgeRating ageRating = (ageRatingRepository.findByLabel(request.getAgeRating())) + .orElseThrow(() -> new RuntimeException("ageRating is not found")); + + + Series series = new Series(); + series.setTitle(request.getTitle()); + series.setDescription(request.getDescription()); + series.setAgeRating(ageRating); + series.setReleaseDate(request.getReleaseDate()); + + + series.setSeasons(new ArrayList<>()); + + return seriesRepository.save(series); + } + + private Season createNewSeasonForSeries(Series series, int seasonNumber) + { + + if (series.getId() == null) { + throw new RuntimeException("Series (with id) must be provided"); + } + + if(seasonNumber == 0) + { + seasonNumber = 1; + } + + Series managedSeries = seriesRepository.findById(series.getId()) + .orElseThrow(() -> new RuntimeException("Series does not exist")); + + if(seasonRepository.existsBySeriesIdAndSeasonNumber(managedSeries.getId(),seasonNumber)){ + throw new RuntimeException("Season already exists"); + } + + Season seasonToAdd = new Season(); + seasonToAdd.setSeasonNumber(seasonNumber); + seasonToAdd.setSeries(managedSeries); + + return seasonRepository.save(seasonToAdd); + } + + public Season createSeason(SeriesCreationRequest request) { + + Series series = seriesRepository.findById(request.getSeriesId()) + .orElseThrow(() -> new EntityNotFoundException("Series not found")); + + return createNewSeasonForSeries(series, request.getSeasonNumber()); + } + + private Episode createNewEpisode(EpisodeRequest request) + { + if(request.getTitle() == null || request.getSeasonId() == null || request.getVideoUrl() == null) { + throw new RuntimeException("Some important data is null"); + } + + Season managedSeason = seasonRepository.findById(request.getSeasonId()) + .orElseThrow(() -> new RuntimeException("Season does not exist")); + + if(episodeRepository.findByTitle(request.getTitle()).isPresent() + && episodeRepository.existsBySeasonIdAndEpisodeNumber(request.getSeasonId(), request.getEpisodeNumber())) { + throw new RuntimeException("Title already exist or episodeNumberAlreadyOccupied"); + } + + Episode episodeToadd = new Episode(); + episodeToadd.setTitle(request.getTitle()); + episodeToadd.setSeason(managedSeason); + episodeToadd.setEpisodeNumber(request.getEpisodeNumber()); + + return episodeRepository.save(episodeToadd); + } + + public Episode createEpisode(EpisodeRequest episodeRequest) + { + Season season = seasonRepository.findById(episodeRequest.getSeasonId()) + .orElseThrow(() -> new EntityNotFoundException("season not found")); + + return createNewEpisode(episodeRequest); + } + +// @Transactional +// public List createMultipleEpisodes(List requests) { +// +// Map seasonCache = new HashMap<>(); +// List episodesToSave = new ArrayList<>(); +// +// for (EpisodeRequest req : requests) { +// if (req == null) continue; +// +// Season season = seasonCache.computeIfAbsent(req.getSeasonId(), id -> +// seasonRepository.findById(id) +// .orElseThrow(() -> new EntityNotFoundException("Season not found: " + id)) +// ); +// +// Episode saved = createNewEpisode(req); +// episodesToSave.add(saved); +// } +// +// return episodesToSave; +// } + +} From ed3856294b7c73d95617231afbeff0a743b50297 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Mon, 12 Jan 2026 15:19:08 +0100 Subject: [PATCH 19/22] add Movie and Series controllers, updated the security on admin endpoints --- .../streamflix/config/SecurityConfig.java | 7 +-- .../controller/MediaController.java | 9 --- .../controller/MovieController.java | 57 +++++++++++++++++++ .../controller/SeriesController.java | 38 +++++++++++++ .../streamflix/service/MediaService.java | 47 --------------- .../streamflix/service/MovieService.java | 6 +- 6 files changed, 102 insertions(+), 62 deletions(-) create mode 100644 src/main/java/com/example/streamflix/controller/MovieController.java create mode 100644 src/main/java/com/example/streamflix/controller/SeriesController.java diff --git a/src/main/java/com/example/streamflix/config/SecurityConfig.java b/src/main/java/com/example/streamflix/config/SecurityConfig.java index d916d81..102eed4 100644 --- a/src/main/java/com/example/streamflix/config/SecurityConfig.java +++ b/src/main/java/com/example/streamflix/config/SecurityConfig.java @@ -31,10 +31,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password").permitAll() -// .requestMatchers("/api/v1/media/createNewMedia").permitAll() -// .requestMatchers("/api/v1/media/createNewMedia").hasAnyRole("Junior", "Mid-level", "Senior") - .requestMatchers(org.springframework.http.HttpMethod.POST, "/api/v1/media/admin/**") - .hasAnyRole("Junior", "Mid-level", "Senior") + .requestMatchers("/api/v1/media/admin/**","/api/v1/media/movie/admin/**") + .hasAnyRole("Junior", "Mid-level", "Senior") + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() .requestMatchers("/api/v1/analytics/admin/**").authenticated() .requestMatchers("/api/v1/analytics/**").permitAll() diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java index 788e016..f7b277e 100644 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -84,12 +84,6 @@ public ResponseEntity validateStream( @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), @ApiResponse(responseCode = "400", description = "Media upload failed") }) -// @PostMapping("/createNewMedia") -// public ResponseEntity createNewMedia(@RequestBody MediaDetailsDto mediaDetailsRequest){ -// -// Object created = mediaService.createMedia(mediaDetailsRequest); -// return ResponseEntity.status(HttpStatus.CREATED).body(created); -// } @PostMapping("/admin/movie") public ResponseEntity createNewMovie(@RequestBody MovieRequest movieRequest){ @@ -130,7 +124,4 @@ public ResponseEntity createNewEpisode(@RequestBody EpisodeRequest episodeReq } -// @PostMapping("/seasons") - - } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MovieController.java b/src/main/java/com/example/streamflix/controller/MovieController.java new file mode 100644 index 0000000..c9150f4 --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/MovieController.java @@ -0,0 +1,57 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Movie; +import com.example.streamflix.model.MovieRequest; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.MovieService; +import com.example.streamflix.service.SeriesService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.Column; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +@RequestMapping("/api/v1/media/movie") +@Tag(name = "Movie", description = "Operations related to media content and streaming") +public class MovieController { + + private final MediaService mediaService; + private final StreamingService streamingService; + private final MovieService movieService; + private final SeriesService seriesService; + + public MovieController(MediaService mediaService, + StreamingService streamingService, + MovieService movieService, + SeriesService seriesService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + this.movieService = movieService; + this.seriesService = seriesService; + } + + @GetMapping("") + public List getAllMovies(){ + return movieService.getAllMovies(); + } + + @GetMapping("{movieId}") + public ResponseEntity getMovieById(@PathVariable Long movieId){ + return movieService.getMovieById(movieId); + } + + @DeleteMapping("/admin/{movieId}") + public Movie deleteMovieById(@PathVariable Long movieId){ + return movieService.deleteMovieById(movieId); + } + + @PutMapping("/admin/{movieId}") + public ResponseEntity updateMovieById(@PathVariable Long movieId, @RequestBody MovieRequest movieRequest){ + Object updated = movieService.updateMovie(movieId, movieRequest); + return ResponseEntity.ok(updated); + } +} diff --git a/src/main/java/com/example/streamflix/controller/SeriesController.java b/src/main/java/com/example/streamflix/controller/SeriesController.java new file mode 100644 index 0000000..2c119ec --- /dev/null +++ b/src/main/java/com/example/streamflix/controller/SeriesController.java @@ -0,0 +1,38 @@ +package com.example.streamflix.controller; + +import com.example.streamflix.entity.Movie; +import com.example.streamflix.model.MovieRequest; +import com.example.streamflix.service.MediaService; +import com.example.streamflix.service.MovieService; +import com.example.streamflix.service.SeriesService; +import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.Column; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +@RequestMapping("/api/v1/media/Series") +@Tag(name = "Series", description = "Operations related to media content and streaming") +public class SeriesController { + + private final MediaService mediaService; + private final StreamingService streamingService; + private final MovieService movieService; + private final SeriesService seriesService; + + public SeriesController(MediaService mediaService, + StreamingService streamingService, + MovieService movieService, + SeriesService seriesService) { + this.mediaService = mediaService; + this.streamingService = streamingService; + this.movieService = movieService; + this.seriesService = seriesService; + } + + +} diff --git a/src/main/java/com/example/streamflix/service/MediaService.java b/src/main/java/com/example/streamflix/service/MediaService.java index 473b052..b48980f 100644 --- a/src/main/java/com/example/streamflix/service/MediaService.java +++ b/src/main/java/com/example/streamflix/service/MediaService.java @@ -155,53 +155,6 @@ private void enrichWithTmdbData(MediaDetailsDto dto, String externalId) { } } - - -// private Season createSeasonForNewSeries(SeriesCreationRequest request) -// { -// int seasonNumberToAdd = request.getSeasonNumber(); -// Series seriesToAddForSeason = request.getSeries(); -// -// if (seriesToAddForSeason.getId() == null) { -// throw new RuntimeException("Series (with id) must be provided"); -// } -// -// Series managedSeries = seriesRepository.findById(seriesToAddForSeason.getId()) -// .orElseThrow(() -> new RuntimeException("Series does not exist")); -// -// if(seasonRepository.existsBySeriesIdAndSeasonNumber(managedSeries.getId(),seasonNumberToAdd)){ -// throw new RuntimeException("Season already exists"); -// } -// -// Season seasonToAdd = new Season(); -// seasonToAdd.setSeasonNumber(seasonNumberToAdd); -// seasonToAdd.setSeries(managedSeries); -// -// return seasonRepository.save(seasonToAdd); -// } - -// private Episode createNewEpisode(SeriesCreationRequest request) -// { -// if(request.getTitle() == null || request.getSeason() == null || request.getBackdropUrl() == null) { -// throw new RuntimeException("Some important data is null"); -// } -// -// Season managedSeason = seasonRepository.findById(request.getSeason().getId()) -// .orElseThrow(() -> new RuntimeException("Season does not exist")); -// -// if(episodeRepository.findByTitle(request.getTitle()).isPresent() -// && episodeRepository.existsBySeasonIdAndEpisodeNumber(request.getSeason().getId(), request.getEpisodeNumber())) { -// throw new RuntimeException("Title already exist or episodeNumberAlreadyOccupied"); -// } -// -// Episode episode = new Episode(); -// episode.setTitle(request.getTitle()); -// episode.setSeason(managedSeason); -// episode.setEpisodeNumber(request.getEpisodeNumber()); -// -// return episodeRepository.save(episode); -// } - @Transactional public Media deleteMedia(Long mediaId) { diff --git a/src/main/java/com/example/streamflix/service/MovieService.java b/src/main/java/com/example/streamflix/service/MovieService.java index 5ec5a57..17d79d2 100644 --- a/src/main/java/com/example/streamflix/service/MovieService.java +++ b/src/main/java/com/example/streamflix/service/MovieService.java @@ -10,6 +10,7 @@ import com.example.streamflix.repository.*; import com.example.streamflix.util.SecurityUtils; import jakarta.transaction.Transactional; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.util.List; @@ -119,10 +120,11 @@ private AgeRating getAgeRating(String label) { .orElseThrow(() -> new NotFoundException("Age Rating not found: " + label)); } - public Movie getMovieById(Long id) + public ResponseEntity getMovieById(Long id) { return movieRepository.findById(id) - .orElseThrow(() -> new NotFoundException("Movie not found: " + id)); + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); } public List getAllMovies() From ff6369ff839efe273eb3206dca115bcf76a4a826 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Mon, 12 Jan 2026 15:51:54 +0100 Subject: [PATCH 20/22] Little fixes after merging --- .../com/example/streamflix/controller/MediaController.java | 1 + .../java/com/example/streamflix/service/SeriesService.java | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java index 6c4e1f2..466a655 100644 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/com/example/streamflix/service/SeriesService.java b/src/main/java/com/example/streamflix/service/SeriesService.java index 17e875f..b633945 100644 --- a/src/main/java/com/example/streamflix/service/SeriesService.java +++ b/src/main/java/com/example/streamflix/service/SeriesService.java @@ -51,16 +51,16 @@ public SeriesService(MediaRepository mediaRepository, this.securityUtils = securityUtils; } - + @Transactional public Series createFullSeries(SeriesCreationRequest request) { Series series = createSeries(request); Season season = createNewSeasonForSeries(series, request.getSeasonNumber()); - seasonRepository.save(season); + Season savedSeason = seasonRepository.save(season); for(EpisodeRequest epDto : request.getEpisodeList()) { - epDto.setSeasonId(season.getId()); + epDto.setSeasonId(savedSeason.getId()); createNewEpisode(epDto); } From ad5d67d5154d4332ca5b4f0a65129684a1f36889 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Mon, 12 Jan 2026 16:42:03 +0100 Subject: [PATCH 21/22] Fixed creat full series logic --- .../model/SeriesCreationRequest.java | 35 +------------------ .../streamflix/service/SeriesService.java | 22 ++++++------ 2 files changed, 11 insertions(+), 46 deletions(-) diff --git a/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java b/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java index d87d2ac..f75cbdb 100644 --- a/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java +++ b/src/main/java/com/example/streamflix/model/SeriesCreationRequest.java @@ -18,10 +18,7 @@ public class SeriesCreationRequest private Long seasonId; private int seasonNumber; - private EpisodeRequest episode; - private final List episodeList = new ArrayList<>(); - private int episodeNumber; - private int durationInSecond; + private List episodeList = new ArrayList<>(); // TMDB enriched data private String posterUrl; @@ -129,16 +126,6 @@ public void setSeasonNumber(int seasonNumber) this.seasonNumber = seasonNumber; } - public EpisodeRequest getEpisode() - { - return episode; - } - - public void setEpisode(EpisodeRequest episode) - { - this.episode = episode; - } - public List getEpisodeList() { return episodeList; @@ -150,26 +137,6 @@ public void addEpisode(EpisodeRequest episode) this.episodeList.add(episode); } - public int getEpisodeNumber() - { - return episodeNumber; - } - - public void setEpisodeNumber(int episodeNumber) - { - this.episodeNumber = episodeNumber; - } - - public int getDurationInSecond() - { - return durationInSecond; - } - - public void setDurationInSecond(int durationInSecond) - { - this.durationInSecond = durationInSecond; - } - public String getPosterUrl() { return posterUrl; diff --git a/src/main/java/com/example/streamflix/service/SeriesService.java b/src/main/java/com/example/streamflix/service/SeriesService.java index b633945..6674fe0 100644 --- a/src/main/java/com/example/streamflix/service/SeriesService.java +++ b/src/main/java/com/example/streamflix/service/SeriesService.java @@ -56,15 +56,13 @@ public Series createFullSeries(SeriesCreationRequest request) { Series series = createSeries(request); Season season = createNewSeasonForSeries(series, request.getSeasonNumber()); - Season savedSeason = seasonRepository.save(season); - for(EpisodeRequest epDto : request.getEpisodeList()) { - - epDto.setSeasonId(savedSeason.getId()); - createNewEpisode(epDto); + if (request.getEpisodeList() != null) { + for (EpisodeRequest epDto : request.getEpisodeList()) { + createNewEpisode(epDto, season); + } } - return seriesRepository.save(series); } @@ -128,15 +126,13 @@ public Season createSeason(SeriesCreationRequest request) { return createNewSeasonForSeries(series, request.getSeasonNumber()); } - private Episode createNewEpisode(EpisodeRequest request) + private Episode createNewEpisode(EpisodeRequest request, Season managedSeason) { - if(request.getTitle() == null || request.getSeasonId() == null || request.getVideoUrl() == null) { + + if(request.getTitle() == null || managedSeason.getId() == null || request.getVideoUrl() == null) { throw new RuntimeException("Some important data is null"); } - Season managedSeason = seasonRepository.findById(request.getSeasonId()) - .orElseThrow(() -> new RuntimeException("Season does not exist")); - if(episodeRepository.findByTitle(request.getTitle()).isPresent() && episodeRepository.existsBySeasonIdAndEpisodeNumber(request.getSeasonId(), request.getEpisodeNumber())) { throw new RuntimeException("Title already exist or episodeNumberAlreadyOccupied"); @@ -146,6 +142,8 @@ private Episode createNewEpisode(EpisodeRequest request) episodeToadd.setTitle(request.getTitle()); episodeToadd.setSeason(managedSeason); episodeToadd.setEpisodeNumber(request.getEpisodeNumber()); + episodeToadd.setVideoUrl(request.getVideoUrl()); + episodeToadd.setDurationSeconds(request.getDurationSeconds()); return episodeRepository.save(episodeToadd); } @@ -155,7 +153,7 @@ public Episode createEpisode(EpisodeRequest episodeRequest) Season season = seasonRepository.findById(episodeRequest.getSeasonId()) .orElseThrow(() -> new EntityNotFoundException("season not found")); - return createNewEpisode(episodeRequest); + return createNewEpisode(episodeRequest, season); } // @Transactional From 5a296188019dbfe7dfda69c80650239c7378d615 Mon Sep 17 00:00:00 2001 From: NickGrahovskis Date: Tue, 13 Jan 2026 22:05:52 +0100 Subject: [PATCH 22/22] Fixed security based on employee account id, added new method and created documentation --- init-db/03_schema.sql | 13 +++- .../controller/MediaController.java | 46 -------------- .../controller/MovieController.java | 53 ++++++++++++++++ .../controller/SeriesController.java | 61 ++++++++++++++++++- .../example/streamflix/entity/Account.java | 14 ----- .../example/streamflix/entity/Employee.java | 14 +++++ .../repository/AccountRepository.java | 3 + .../repository/EmployeeRepository.java | 2 + .../security/CustomUserDetails.java | 19 ++---- .../service/AccountUserDetailsService.java | 38 +++++++++++- .../streamflix/service/SeriesService.java | 47 +++++--------- 11 files changed, 197 insertions(+), 113 deletions(-) diff --git a/init-db/03_schema.sql b/init-db/03_schema.sql index fde572c..e334b5a 100644 --- a/init-db/03_schema.sql +++ b/init-db/03_schema.sql @@ -50,8 +50,7 @@ CREATE TABLE accounts ( is_verified BOOLEAN DEFAULT FALSE, failed_login_attempts INT DEFAULT 0, is_blocked BOOLEAN DEFAULT FALSE, - discount_used BOOLEAN DEFAULT FALSE, - role_id int references role(id) + discount_used BOOLEAN DEFAULT FALSE ); -- Verification and password recovery tokens @@ -191,7 +190,8 @@ CREATE TABLE employee ( id SERIAL PRIMARY KEY, role_id INT NOT NULL REFERENCES role(id), name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL + email VARCHAR(255) UNIQUE NOT NULL, + account_id int references accounts(id) ); -- Insert initial lookup data @@ -290,3 +290,10 @@ INSERT INTO media_content_warning (media_id, content_warning_id) VALUES (4, 1), (4, 5), (4, 6), -- Pulp Fiction: Violence, Drug Abuse, Coarse Language (6, 1), (6, 2), -- Matrix: Violence, Fear (7, 1), (7, 5), (7, 6); -- Breaking Bad: Violence, Drug Abuse, Coarse Language + + +INSERT INTO employee (role_id, name, email) +VALUES + (1, 'Alice Junior', 'alice.junior@streamflix.com'), + (2, 'Bob Mid', 'bob.mid@streamflix.com'), + (3, 'Carol Senior', 'carol.senior@streamflix.com'); \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MediaController.java b/src/main/java/com/example/streamflix/controller/MediaController.java index 466a655..1d5d3d3 100644 --- a/src/main/java/com/example/streamflix/controller/MediaController.java +++ b/src/main/java/com/example/streamflix/controller/MediaController.java @@ -86,50 +86,4 @@ public ResponseEntity validateStream( streamingService.validateStream(profileId, mediaId, quality) ); } - - @Operation(summary = "create new media", description = "create media based on type") - @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Media has been saved successfully"), - @ApiResponse(responseCode = "400", description = "Media upload failed") - }) - - @PostMapping("/admin/movie") - public ResponseEntity createNewMovie(@RequestBody MovieRequest movieRequest){ - - Object created = movieService.createMovie(movieRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @DeleteMapping("/admin/{mediaId}") - public ResponseEntity deleteMedia(@PathVariable Long mediaId) { - mediaService.deleteMedia(mediaId); - return ResponseEntity.noContent().build(); - } - @PutMapping("/admin/movie/{movieId}") - public ResponseEntity updateMovieById(@PathVariable Long movieId, @RequestBody MovieRequest movieRequest){ - Object updated = movieService.updateMovie(movieId, movieRequest); - return ResponseEntity.ok(updated); - } - - @PostMapping("/admin/series") - public ResponseEntity createNewSeries(@RequestBody SeriesCreationRequest seriesCreationRequest){ - - Object created = seriesService.createFullSeries(seriesCreationRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @PostMapping("/admin/series/season") - public ResponseEntity createNewSeason(@RequestBody SeriesCreationRequest seriesCreationRequest){ - - Object created = seriesService.createSeason(seriesCreationRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - @PostMapping("/admin/series/episode") - public ResponseEntity createNewEpisode(@RequestBody EpisodeRequest episodeRequest) { - Object created = seriesService.createEpisode(episodeRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(created); - } - - } \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/controller/MovieController.java b/src/main/java/com/example/streamflix/controller/MovieController.java index c9150f4..69eabaa 100644 --- a/src/main/java/com/example/streamflix/controller/MovieController.java +++ b/src/main/java/com/example/streamflix/controller/MovieController.java @@ -6,8 +6,12 @@ import com.example.streamflix.service.MovieService; import com.example.streamflix.service.SeriesService; import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.persistence.Column; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -34,24 +38,73 @@ public MovieController(MediaService mediaService, this.seriesService = seriesService; } + + @Operation(summary = "Get all movies", + description = "Retrieves a complete list of all movies available in the database." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list"), + @ApiResponse(responseCode = "204", description = "No movies found in the database") + }) @GetMapping("") public List getAllMovies(){ return movieService.getAllMovies(); } + @Operation( + summary = "Get movie by ID", + description = "Returns a single movie object based on its unique identifier." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Movie found"), + @ApiResponse(responseCode = "404", description = "Movie not found with the provided ID") + }) @GetMapping("{movieId}") public ResponseEntity getMovieById(@PathVariable Long movieId){ return movieService.getMovieById(movieId); } + @Operation( + summary = "Delete movie", + description = "Admin only: Removes a movie record from the system." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Movie successfully deleted"), + @ApiResponse(responseCode = "404", description = "Movie not found") + }) @DeleteMapping("/admin/{movieId}") public Movie deleteMovieById(@PathVariable Long movieId){ return movieService.deleteMovieById(movieId); } + @Operation( + summary = "Update movie details", + description = "Admin only: Updates an existing movie's information." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Movie updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid input data"), + @ApiResponse(responseCode = "404", description = "Movie not found") + }) @PutMapping("/admin/{movieId}") public ResponseEntity updateMovieById(@PathVariable Long movieId, @RequestBody MovieRequest movieRequest){ Object updated = movieService.updateMovie(movieId, movieRequest); return ResponseEntity.ok(updated); } + + @Operation( + summary = "Create a new movie", + description = "Admin only: Adds a new movie record to the catalog." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Movie created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid request body") + }) + @PostMapping("/admin") + public ResponseEntity createNewMovie(@RequestBody MovieRequest movieRequest){ + + Object created = movieService.createMovie(movieRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + } diff --git a/src/main/java/com/example/streamflix/controller/SeriesController.java b/src/main/java/com/example/streamflix/controller/SeriesController.java index 2c119ec..01a605a 100644 --- a/src/main/java/com/example/streamflix/controller/SeriesController.java +++ b/src/main/java/com/example/streamflix/controller/SeriesController.java @@ -1,13 +1,20 @@ package com.example.streamflix.controller; import com.example.streamflix.entity.Movie; +import com.example.streamflix.model.EpisodeRequest; import com.example.streamflix.model.MovieRequest; +import com.example.streamflix.model.SeriesCreationRequest; import com.example.streamflix.service.MediaService; import com.example.streamflix.service.MovieService; import com.example.streamflix.service.SeriesService; import com.example.streamflix.service.StreamingService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.persistence.Column; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @@ -15,8 +22,8 @@ import java.util.*; @RestController -@RequestMapping("/api/v1/media/Series") -@Tag(name = "Series", description = "Operations related to media content and streaming") +@RequestMapping("/api/v1/media/series") +@Tag(name = "Series", description = "Operations related to series") public class SeriesController { private final MediaService mediaService; @@ -34,5 +41,55 @@ public SeriesController(MediaService mediaService, this.seriesService = seriesService; } + @Operation(summary = "Get all series", description = "Returns a list of all series available in the system") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully retrieved list"), + @ApiResponse(responseCode = "204", description = "No series found") + }) + @GetMapping + public ResponseEntity> getAllSeries() { + List seriesList = seriesService.getSeriesList(); + if (seriesList.isEmpty()) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.ok(seriesList); + } + + @Operation(summary = "Get series by ID", description = "Retrieves a single series along with its seasons and episodes") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Found the series"), + @ApiResponse(responseCode = "404", description = "Series not found") + }) + @GetMapping("/{id}") + public ResponseEntity getSeriesById( + @Parameter(description = "ID of the series to be obtained", required = true) + @PathVariable Long id) { + + return seriesService.getSeriesById(id); + } + + @Operation(summary = "Create a new series", description = "Admin endpoint to initialize a new series") + @PostMapping("/admin/series") + public ResponseEntity createNewSeries(@RequestBody SeriesCreationRequest seriesCreationRequest){ + + Object created = seriesService.createFullSeries(seriesCreationRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @Operation(summary = "Create a new season", description = "Add a season to an existing series") + @PostMapping("/admin/series/season") + public ResponseEntity createNewSeason(@RequestBody SeriesCreationRequest seriesCreationRequest){ + + Object created = seriesService.createSeason(seriesCreationRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + + @Operation(summary = "Create a new episode", description = "Add an episode to an existing season") + @PostMapping("/admin/series/episode") + public ResponseEntity createNewEpisode(@RequestBody EpisodeRequest episodeRequest) { + Object created = seriesService.createEpisode(episodeRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + } diff --git a/src/main/java/com/example/streamflix/entity/Account.java b/src/main/java/com/example/streamflix/entity/Account.java index 6b0bc25..800f360 100644 --- a/src/main/java/com/example/streamflix/entity/Account.java +++ b/src/main/java/com/example/streamflix/entity/Account.java @@ -43,12 +43,6 @@ public class Account { @Schema(description = "Indicates if the discount has been used", example = "false") private boolean discountUsed = false; - @JsonIgnore - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "role_id") - @Schema(description = "Account role", example = "") - private Role role; - public Account() { } @@ -112,12 +106,4 @@ public boolean isDiscountUsed() { public void setDiscountUsed(boolean discountUsed) { this.discountUsed = discountUsed; } - - public Role getRole() { - return this.role; - } - - public void setRole(Role role) { - this.role = role; - } } diff --git a/src/main/java/com/example/streamflix/entity/Employee.java b/src/main/java/com/example/streamflix/entity/Employee.java index 095d9ec..78afa17 100644 --- a/src/main/java/com/example/streamflix/entity/Employee.java +++ b/src/main/java/com/example/streamflix/entity/Employee.java @@ -20,6 +20,10 @@ public class Employee { @Column(unique = true, nullable = false) private String email; + @ManyToOne + @JoinColumn(name = "account_id") + private Account account; + public Employee() { } @@ -60,4 +64,14 @@ public String getEmail() { public void setEmail(String email) { this.email = email; } + + public Account getAccount() + { + return this.account; + } + + public void setAccount(Account account) + { + this.account = account; + } } diff --git a/src/main/java/com/example/streamflix/repository/AccountRepository.java b/src/main/java/com/example/streamflix/repository/AccountRepository.java index 562787a..559f730 100644 --- a/src/main/java/com/example/streamflix/repository/AccountRepository.java +++ b/src/main/java/com/example/streamflix/repository/AccountRepository.java @@ -2,8 +2,11 @@ import com.example.streamflix.entity.Account; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + import java.util.Optional; +@Repository public interface AccountRepository extends JpaRepository { Optional findByEmail(String email); } diff --git a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java index e87053b..b9c6dd6 100644 --- a/src/main/java/com/example/streamflix/repository/EmployeeRepository.java +++ b/src/main/java/com/example/streamflix/repository/EmployeeRepository.java @@ -1,5 +1,6 @@ package com.example.streamflix.repository; +import com.example.streamflix.entity.Account; import com.example.streamflix.entity.Employee; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -9,4 +10,5 @@ @Repository public interface EmployeeRepository extends JpaRepository { Optional findByEmail(String email); + Optional findByAccount(Account account); } diff --git a/src/main/java/com/example/streamflix/security/CustomUserDetails.java b/src/main/java/com/example/streamflix/security/CustomUserDetails.java index 55922d9..73d5d26 100644 --- a/src/main/java/com/example/streamflix/security/CustomUserDetails.java +++ b/src/main/java/com/example/streamflix/security/CustomUserDetails.java @@ -6,15 +6,16 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -import java.util.Collections; import java.util.List; public class CustomUserDetails implements UserDetails { private final Account account; + private final String roleName; - public CustomUserDetails(Account account) { + public CustomUserDetails(Account account, String roleName) { this.account = account; + this.roleName = roleName; } public Account getAccount() { @@ -23,21 +24,11 @@ public Account getAccount() { @Override public Collection getAuthorities() { - if (account.getRole() == null || account.getRole().getId() == null) { + if (roleName == null) { return List.of(); } - String name = ""; - if (account.getRole().getId() == 1) name = "Junior"; - else if (account.getRole().getId() == 2) name = "Mid-level"; - else if (account.getRole().getId() == 3) name = "Senior"; - else name = null; - - if(name == null) { - return List.of(); - } - - return List.of(new SimpleGrantedAuthority("ROLE_" + name)); + return List.of(new SimpleGrantedAuthority("ROLE_" + roleName)); } @Override diff --git a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java index e1aff8c..a45f7a7 100644 --- a/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java +++ b/src/main/java/com/example/streamflix/service/AccountUserDetailsService.java @@ -1,27 +1,59 @@ package com.example.streamflix.service; import com.example.streamflix.entity.Account; +import com.example.streamflix.entity.Employee; import com.example.streamflix.repository.AccountRepository; +import com.example.streamflix.repository.EmployeeRepository; // Add this import com.example.streamflix.security.CustomUserDetails; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service public class AccountUserDetailsService implements UserDetailsService { private final AccountRepository accountRepository; + private final EmployeeRepository employeeRepository; - public AccountUserDetailsService(AccountRepository accountRepository) { + public AccountUserDetailsService(AccountRepository accountRepository, + EmployeeRepository employeeRepository) { this.accountRepository = accountRepository; + this.employeeRepository = employeeRepository; } @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Account account = accountRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); - return new CustomUserDetails(account); + Optional employeeOpt = employeeRepository.findByAccount(account); + String roleName; + + if (employeeOpt.isPresent()) { + + Employee foundEmployee = employeeOpt.get(); + Long roleId; + + if (foundEmployee.getRole() == null) { + roleId = 0L; + } else { + roleId = foundEmployee.getRole().getId(); + } + + roleName = switch (roleId.intValue()) { + case 1 -> "Junior"; + case 2 -> "Mid-level"; + case 3 -> "Senior"; + default -> "User"; + }; + } else { + roleName = "User"; + } + + return new CustomUserDetails(account, roleName); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/streamflix/service/SeriesService.java b/src/main/java/com/example/streamflix/service/SeriesService.java index 6674fe0..33d89e7 100644 --- a/src/main/java/com/example/streamflix/service/SeriesService.java +++ b/src/main/java/com/example/streamflix/service/SeriesService.java @@ -10,6 +10,7 @@ import com.example.streamflix.util.SecurityUtils; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.util.*; @@ -68,7 +69,7 @@ public Series createFullSeries(SeriesCreationRequest request) { public Series createSeries(SeriesCreationRequest request) { - if(request == null){ + if(request == null) { throw new IllegalArgumentException("Series creation request cannot be null"); } @@ -79,28 +80,24 @@ public Series createSeries(SeriesCreationRequest request) { AgeRating ageRating = (ageRatingRepository.findByLabel(request.getAgeRating())) .orElseThrow(() -> new RuntimeException("ageRating is not found")); - Series series = new Series(); series.setTitle(request.getTitle()); series.setDescription(request.getDescription()); series.setAgeRating(ageRating); series.setReleaseDate(request.getReleaseDate()); - series.setSeasons(new ArrayList<>()); return seriesRepository.save(series); } - private Season createNewSeasonForSeries(Series series, int seasonNumber) - { + private Season createNewSeasonForSeries(Series series, int seasonNumber) { if (series.getId() == null) { throw new RuntimeException("Series (with id) must be provided"); } - if(seasonNumber == 0) - { + if(seasonNumber == 0) { seasonNumber = 1; } @@ -126,8 +123,7 @@ public Season createSeason(SeriesCreationRequest request) { return createNewSeasonForSeries(series, request.getSeasonNumber()); } - private Episode createNewEpisode(EpisodeRequest request, Season managedSeason) - { + private Episode createNewEpisode(EpisodeRequest request, Season managedSeason) { if(request.getTitle() == null || managedSeason.getId() == null || request.getVideoUrl() == null) { throw new RuntimeException("Some important data is null"); @@ -148,33 +144,22 @@ private Episode createNewEpisode(EpisodeRequest request, Season managedSeason) return episodeRepository.save(episodeToadd); } - public Episode createEpisode(EpisodeRequest episodeRequest) - { + public Episode createEpisode(EpisodeRequest episodeRequest) { Season season = seasonRepository.findById(episodeRequest.getSeasonId()) .orElseThrow(() -> new EntityNotFoundException("season not found")); return createNewEpisode(episodeRequest, season); } -// @Transactional -// public List createMultipleEpisodes(List requests) { -// -// Map seasonCache = new HashMap<>(); -// List episodesToSave = new ArrayList<>(); -// -// for (EpisodeRequest req : requests) { -// if (req == null) continue; -// -// Season season = seasonCache.computeIfAbsent(req.getSeasonId(), id -> -// seasonRepository.findById(id) -// .orElseThrow(() -> new EntityNotFoundException("Season not found: " + id)) -// ); -// -// Episode saved = createNewEpisode(req); -// episodesToSave.add(saved); -// } -// -// return episodesToSave; -// } + public List getSeriesList(){ + + return seriesRepository.findAll(); + } + + public ResponseEntity getSeriesById(Long seriesId) { + return seriesRepository.findById(seriesId) + .map(ResponseEntity::ok) + .orElseThrow(()-> new EntityNotFoundException("series not found")); + } }