Merge pull request #71 from mateuszskoczek/dev

Dev
This commit is contained in:
2024-09-23 20:30:51 +02:00
committed by GitHub
Unverified
109 changed files with 4281 additions and 387 deletions

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Accounts;
public abstract class AccountProfilePicture
{
#region PROPERTIES
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public AccountProfilePicture() {}
#endregion
}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Accounts;
public class AccountProfilePictureResponse : AccountProfilePicture
{
#region PROPERTIES
[JsonPropertyName("id")]
public required Guid Id { get; set; }
[JsonPropertyName("upload_date")]
public required DateTime UploadDate { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public AccountProfilePictureResponse() {}
[SetsRequiredMembers]
public AccountProfilePictureResponse(Database.Model.Account.AccountProfilePicture accountProfilePicture)
{
Id = accountProfilePicture.Id;
Image = accountProfilePicture.Image;
MimeType = accountProfilePicture.MimeType;
UploadDate = accountProfilePicture.UploadDate;
}
#endregion
}

View File

@@ -26,6 +26,9 @@ public class RegisterResponse
#region CONSTRUCTORS
[JsonConstructor]
public RegisterResponse() {}
[SetsRequiredMembers]
public RegisterResponse(Account account)
{

View File

@@ -16,6 +16,9 @@ public class GenreResponse : Genre
#region CONSTRUCTORS
[JsonConstructor]
public GenreResponse() {}
[SetsRequiredMembers]
public GenreResponse(Database.Model.Common.Genre genre)
{

View File

@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public abstract class Media
{
[JsonPropertyName("title")]
public required string Title { get; set; }
[JsonPropertyName("original_title")]
public string? OriginalTitle { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("release_date")]
public DateOnly? ReleaseDate { get; set; }
[JsonPropertyName("length")]
public short? Length { get; set; }
}

View File

@@ -24,6 +24,7 @@ public class MediaPhotoRequest : MediaPhoto
item.MediaId = MediaId;
item.Image = Image;
item.MimeType = MimeType;
item.UploadDate = DateTime.Now;
}
public void UpdateMediaPhotoImageBackground(MediaPhotoImageBackground item)

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public abstract class MediaPoster
{
#region PROPERTIES
[JsonPropertyName("image")]
public required byte[] Image { get; set; }
[JsonPropertyName("mime_type")]
public required string MimeType { get; set; }
#endregion
#region PUBLIC METHODS
public override string ToString() => $"data:{MimeType};base64,{Convert.ToBase64String(Image)}";
#endregion
}

View File

@@ -0,0 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Media;
public class MediaPosterRequest : MediaPoster
{
#region CONSTRUCTORS
public MediaPosterRequest() {}
[SetsRequiredMembers]
public MediaPosterRequest(MediaPosterResponse response)
{
Image = response.Image;
MimeType = response.MimeType;
}
#endregion
public MediaPosterImage CreateMediaPosterImage() => new MediaPosterImage
{
Image = Image,
MimeType = MimeType,
};
public void UpdateMediaPosterImage(MediaPosterImage item)
{
item.Image = Image;
item.MimeType = MimeType;
item.UploadDate = DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,36 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Media;
public class MediaPosterResponse : MediaPoster
{
#region PROPERTIES
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("upload_date")]
public DateTime UploadDate { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public MediaPosterResponse() {}
[SetsRequiredMembers]
public MediaPosterResponse(MediaPosterImage mediaPhotoImage)
{
Id = mediaPhotoImage.Id;
Image = mediaPhotoImage.Image;
MimeType = mediaPhotoImage.MimeType;
UploadDate = mediaPhotoImage.UploadDate;
}
#endregion
}

View File

@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public class MediaRatingRequest
{
#region PROPERTIES
[JsonPropertyName("rating")]
public required short Rating { get; set; }
#endregion
#region CONSTRUCTORS
[SetsRequiredMembers]
public MediaRatingRequest(short rating)
{
Rating = rating;
}
#endregion
}

View File

@@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public class MediaRatingResponse
{
#region PROPERTIES
[JsonPropertyName("rating_average")]
public required double RatingAverage { get; set; }
[JsonPropertyName("rating_count")]
public required long RatingCount { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public MediaRatingResponse() {}
[SetsRequiredMembers]
public MediaRatingResponse(double ratingAverage, long ratingCount)
{
RatingAverage = ratingAverage;
RatingCount = ratingCount;
}
#endregion
}

View File

@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Media;
public class MediaResponse : Media
{
#region PROPERTIES
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("type")]
public MediaType Type { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public MediaResponse() {}
[SetsRequiredMembers]
public MediaResponse(Database.Model.Media.Media media, MediaType mediaType)
{
Id = media.Id;
Title = media.Title;
OriginalTitle = media.OriginalTitle;
Description = media.Description;
ReleaseDate = media.ReleaseDate;
Length = media.Length;
Type = mediaType;
}
#endregion
}

View File

@@ -0,0 +1,7 @@
namespace WatchIt.Common.Model.Media;
public enum MediaType
{
Movie,
Series
}

View File

@@ -2,23 +2,8 @@
namespace WatchIt.Common.Model.Movies;
public class Movie
public class Movie : Media.Media
{
[JsonPropertyName("title")]
public required string Title { get; set; }
[JsonPropertyName("original_title")]
public string? OriginalTitle { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("release_date")]
public DateOnly? ReleaseDate { get; set; }
[JsonPropertyName("length")]
public short? Length { get; set; }
[JsonPropertyName("budget")]
public decimal? Budget { get; set; }
}

View File

@@ -1,9 +1,29 @@
using WatchIt.Database.Model.Media;
using System.Diagnostics.CodeAnalysis;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Movies;
public class MovieRequest : Movie
{
#region CONSTRUCTORS
[SetsRequiredMembers]
public MovieRequest(MovieResponse initData)
{
Title = initData.Title;
OriginalTitle = initData.OriginalTitle;
Description = initData.Description;
ReleaseDate = initData.ReleaseDate;
Length = initData.Length;
Budget = initData.Budget;
}
public MovieRequest() {}
#endregion
#region PUBLIC METHODS
public Database.Model.Media.Media CreateMedia() => new Database.Model.Media.Media

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Movies;
@@ -7,6 +8,7 @@ public class MovieResponse : Movie
{
#region PROPERTIES
[JsonPropertyName("id")]
public long Id { get; set; }
#endregion
@@ -15,6 +17,9 @@ public class MovieResponse : Movie
#region CONSTRUCTORS
[JsonConstructor]
public MovieResponse() {}
[SetsRequiredMembers]
public MovieResponse(MediaMovie mediaMovie)
{

View File

@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace WatchIt.Common.Model.Series;
public class Series : Media.Media
{
[JsonPropertyName("has_ended")]
public bool HasEnded { get; set; }
}

View File

@@ -0,0 +1,62 @@
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Query;
namespace WatchIt.Common.Model.Series;
public class SeriesQueryParameters : QueryParameters<SeriesResponse>
{
#region PROPERTIES
[FromQuery(Name = "title")]
public string? Title { get; set; }
[FromQuery(Name = "original_title")]
public string? OriginalTitle { get; set; }
[FromQuery(Name = "description")]
public string? Description { get; set; }
[FromQuery(Name = "release_date")]
public DateOnly? ReleaseDate { get; set; }
[FromQuery(Name = "release_date_from")]
public DateOnly? ReleaseDateFrom { get; set; }
[FromQuery(Name = "release_date_to")]
public DateOnly? ReleaseDateTo { get; set; }
[FromQuery(Name = "length")]
public short? Length { get; set; }
[FromQuery(Name = "length_from")]
public short? LengthFrom { get; set; }
[FromQuery(Name = "length_to")]
public short? LengthTo { get; set; }
[FromQuery(Name = "has_ended")]
public bool? HasEnded { get; set; }
#endregion
#region PUBLIC METHODS
public override bool IsMeetingConditions(SeriesResponse item) =>
(
TestString(item.Title, Title)
&&
TestString(item.OriginalTitle, OriginalTitle)
&&
TestString(item.Description, Description)
&&
TestComparable(item.ReleaseDate, ReleaseDate, ReleaseDateFrom, ReleaseDateTo)
&&
TestComparable(item.Length, Length, LengthFrom, LengthTo)
&&
TestBoolean(item.HasEnded, HasEnded)
);
#endregion
}

View File

@@ -0,0 +1,59 @@
using System.Diagnostics.CodeAnalysis;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Series;
public class SeriesRequest : Series
{
#region CONSTRUCTORS
[SetsRequiredMembers]
public SeriesRequest(SeriesResponse initData)
{
Title = initData.Title;
OriginalTitle = initData.OriginalTitle;
Description = initData.Description;
ReleaseDate = initData.ReleaseDate;
Length = initData.Length;
HasEnded = initData.HasEnded;
}
public SeriesRequest() {}
#endregion
#region PUBLIC METHODS
public Database.Model.Media.Media CreateMedia() => new Database.Model.Media.Media
{
Title = Title,
OriginalTitle = OriginalTitle,
Description = Description,
ReleaseDate = ReleaseDate,
Length = Length,
};
public MediaSeries CreateMediaSeries(long id) => new MediaSeries
{
Id = id,
HasEnded = HasEnded,
};
public void UpdateMedia(Database.Model.Media.Media media)
{
media.Title = Title;
media.OriginalTitle = OriginalTitle;
media.Description = Description;
media.ReleaseDate = ReleaseDate;
media.Length = Length;
}
public void UpdateMediaSeries(MediaSeries mediaSeries)
{
mediaSeries.HasEnded = HasEnded;
}
#endregion
}

View File

@@ -0,0 +1,36 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using WatchIt.Database.Model.Media;
namespace WatchIt.Common.Model.Series;
public class SeriesResponse : Series
{
#region PROPERTIES
[JsonPropertyName("id")]
public long Id { get; set; }
#endregion
#region CONSTRUCTORS
[JsonConstructor]
public SeriesResponse() {}
[SetsRequiredMembers]
public SeriesResponse(MediaSeries mediaSeries)
{
Id = mediaSeries.Media.Id;
Title = mediaSeries.Media.Title;
OriginalTitle = mediaSeries.Media.OriginalTitle;
Description = mediaSeries.Media.Description;
ReleaseDate = mediaSeries.Media.ReleaseDate;
Length = mediaSeries.Media.Length;
HasEnded = mediaSeries.HasEnded;
}
#endregion
}

View File

@@ -7,7 +7,7 @@ public class HttpResponse
{
#region FIELDS
private HttpResponseMessage _message;
private readonly HttpResponseMessage _message;
private Action? _2XXAction;
private Action? _400Action;
@@ -32,37 +32,37 @@ public class HttpResponse
#region PUBLIC METHODS
public HttpResponse RegisterActionFor2XXSuccess(Action action)
public HttpResponse RegisterActionFor2XXSuccess(Action? action)
{
_2XXAction = action;
return this;
}
public HttpResponse RegisterActionFor2XXSuccess<T>(Action<T> action)
public HttpResponse RegisterActionFor2XXSuccess<T>(Action<T>? action)
{
_2XXAction = () => Invoke(action);
return this;
}
public HttpResponse RegisterActionFor400BadRequest(Action<IDictionary<string, string[]>> action)
public HttpResponse RegisterActionFor400BadRequest(Action<IDictionary<string, string[]>>? action)
{
_400Action = () => Invoke(action);
return this;
}
public HttpResponse RegisterActionFor401Unauthorized(Action action)
public HttpResponse RegisterActionFor401Unauthorized(Action? action)
{
_401Action = action;
return this;
}
public HttpResponse RegisterActionFor403Forbidden(Action action)
public HttpResponse RegisterActionFor403Forbidden(Action? action)
{
_403Action = action;
return this;
}
public HttpResponse RegisterActionFor404NotFound(Action action)
public HttpResponse RegisterActionFor404NotFound(Action? action)
{
_404Action = action;
return this;
@@ -86,21 +86,21 @@ public class HttpResponse
#region PRIVATE METHODS
private async void Invoke<T>(Action<T> action)
private async void Invoke<T>(Action<T>? action)
{
Stream streamData = await _message.Content.ReadAsStreamAsync();
T? data = await JsonSerializer.DeserializeAsync<T>(streamData);
action.Invoke(data!);
action?.Invoke(data!);
}
private async void Invoke(Action<IDictionary<string, string[]>> action)
private async void Invoke(Action<IDictionary<string, string[]>>? action)
{
Stream streamData = await _message.Content.ReadAsStreamAsync();
ValidationProblemDetails? data = await JsonSerializer.DeserializeAsync<ValidationProblemDetails>(streamData, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
});
action.Invoke(data!.Errors);
action?.Invoke(data!.Errors);
}
#endregion

View File

@@ -7,7 +7,7 @@ public class MediaPosterImage
public Guid Id { get; set; }
public required byte[] Image { get; set; }
public required string MimeType { get; set; }
public required DateTime UploadDate { get; set; }
public DateTime UploadDate { get; set; }
#endregion

View File

@@ -30,4 +30,15 @@ public class AccountsController(IAccountsControllerService accountsControllerSer
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> AuthenticateRefresh() => await accountsControllerService.AuthenticateRefresh();
[HttpDelete("logout")]
[Authorize(AuthenticationSchemes = "refresh")]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
public async Task<ActionResult> Logout() => await accountsControllerService.Logout();
[HttpGet("{id}/profile-picture")]
[AllowAnonymous]
[ProducesResponseType(typeof(AccountProfilePictureResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetAccountProfilePicture([FromRoute(Name = "id")]long id) => await accountsControllerService.GetAccountProfilePicture(id);
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Genres;
@@ -11,33 +12,125 @@ namespace WatchIt.WebAPI.Controllers;
[Route("media")]
public class MediaController(IMediaControllerService mediaControllerService)
{
#region MAIN
[HttpGet("{id}")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMedia([FromRoute] long id) => await mediaControllerService.GetMedia(id);
#endregion
#region GENRES
[HttpGet("{id}/genres")]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<GenreResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetGenres([FromRoute]long id) => await mediaControllerService.GetGenres(id);
public async Task<ActionResult> GetMediaGenres([FromRoute]long id) => await mediaControllerService.GetMediaGenres(id);
[HttpPost("{id}/genres/{genre_id}")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.PostGenre(id, genreId);
public async Task<ActionResult> PostMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.PostMediaGenre(id, genreId);
[HttpDelete("{id}/genres/{genre_id}")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeleteGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.DeleteGenre(id, genreId);
public async Task<ActionResult> DeleteMediaGenre([FromRoute]long id, [FromRoute(Name = "genre_id")]short genreId) => await mediaControllerService.DeleteMediaGenre(id, genreId);
#endregion
#region RATING
[HttpGet("{id}/rating")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaRatingResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaRating([FromRoute] long id) => await mediaControllerService.GetMediaRating(id);
[HttpGet("{id}/rating/{user_id}")]
[AllowAnonymous]
[ProducesResponseType(typeof(short), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaRatingByUser([FromRoute] long id, [FromRoute(Name = "user_id")]long userId) => await mediaControllerService.GetMediaRatingByUser(id, userId);
[HttpPut("{id}/rating")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PutMediaRating([FromRoute] long id, [FromBody] MediaRatingRequest data) => await mediaControllerService.PutMediaRating(id, data);
[HttpDelete("{id}/rating")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> DeleteMediaRating([FromRoute] long id) => await mediaControllerService.DeleteMediaRating(id);
#endregion
#region VIEW COUNT
[HttpPost("{id}/view")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> PostMediaView([FromRoute] long id) => await mediaControllerService.PostMediaView(id);
#endregion
#region POSTER
[HttpGet("{id}/poster")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaPosterResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaPoster([FromRoute] long id) => await mediaControllerService.GetMediaPoster(id);
[HttpPut("{id}/poster")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(MediaPosterResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> PutMediaPoster([FromRoute]long id, [FromBody]MediaPosterRequest body) => await mediaControllerService.PutMediaPoster(id, body);
[HttpDelete("{id}/poster")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteMediaPoster([FromRoute]long id) => await mediaControllerService.DeleteMediaPoster(id);
#endregion
#region PHOTOS
[HttpGet("{id}/photos/random_background")]
[AllowAnonymous]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetMediaRandomBackgroundPhoto([FromRoute]long id) => await mediaControllerService.GetMediaRandomBackgroundPhoto(id);
public async Task<ActionResult> GetMediaPhotoRandomBackground([FromRoute]long id) => await mediaControllerService.GetMediaRandomBackgroundPhoto(id);
[HttpGet("photos/{photo_id}")]
[AllowAnonymous]
@@ -54,10 +147,10 @@ public class MediaController(IMediaControllerService mediaControllerService)
[AllowAnonymous]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetRandomBackgroundPhoto() => await mediaControllerService.GetRandomBackgroundPhoto();
public async Task<ActionResult> GetPhotoRandomBackground() => await mediaControllerService.GetRandomBackgroundPhoto();
[HttpPost("photos")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(MediaPhotoResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
@@ -65,7 +158,7 @@ public class MediaController(IMediaControllerService mediaControllerService)
public async Task<ActionResult> PostPhoto([FromBody]MediaPhotoRequest body) => await mediaControllerService.PostPhoto(body);
[HttpPut("photos/{photo_id}")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
@@ -74,10 +167,12 @@ public class MediaController(IMediaControllerService mediaControllerService)
public async Task<ActionResult> PutPhoto([FromRoute(Name = "photo_id")]Guid photoId, [FromBody]MediaPhotoRequest body) => await mediaControllerService.PutPhoto(photoId, body);
[HttpDelete("photos/{photo_id}")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DeletePhoto([FromRoute(Name = "photo_id")]Guid photoId) => await mediaControllerService.DeletePhoto(photoId);
#endregion
}

View File

@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using System.Net;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Genres;
@@ -9,38 +11,76 @@ namespace WatchIt.WebAPI.Controllers;
[ApiController]
[Route("movies")]
public class MoviesController(IMoviesControllerService moviesControllerService) : ControllerBase
public class MoviesController : ControllerBase
{
#region SERVICES
private readonly IMoviesControllerService _moviesControllerService;
#endregion
#region CONSTRUCTORS
public MoviesController(IMoviesControllerService moviesControllerService)
{
_moviesControllerService = moviesControllerService;
}
#endregion
#region METHODS
#region Main
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<MovieResponse>), StatusCodes.Status200OK)]
public async Task<ActionResult> GetAll(MovieQueryParameters query) => await moviesControllerService.GetAll(query);
public async Task<ActionResult> GetAllMovies(MovieQueryParameters query) => await _moviesControllerService.GetAllMovies(query);
[HttpGet("{id}")]
[AllowAnonymous]
[ProducesResponseType(typeof(MovieResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Get([FromRoute]long id) => await moviesControllerService.Get(id);
public async Task<ActionResult> GetMovie([FromRoute] long id) => await _moviesControllerService.GetMovie(id);
[HttpPost]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(MovieResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> Post([FromBody]MovieRequest body) => await moviesControllerService.Post(body);
public async Task<ActionResult> PostMovie([FromBody] MovieRequest body) => await _moviesControllerService.PostMovie(body);
[HttpPut("{id}")]
[Authorize]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> Put([FromRoute]long id, [FromBody]MovieRequest body) => await moviesControllerService.Put(id, body);
public async Task<ActionResult> PutMovie([FromRoute] long id, [FromBody]MovieRequest body) => await _moviesControllerService.PutMovie(id, body);
[HttpDelete("{id}")]
[Authorize]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> Delete([FromRoute] long id) => await moviesControllerService.Delete(id);
public async Task<ActionResult> DeleteMovie([FromRoute] long id) => await _moviesControllerService.DeleteMovie(id);
#endregion
#region View count
[HttpGet("view")]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<MovieResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetMoviesViewRank([FromQuery] int first = 5, [FromQuery] int days = 7) => await _moviesControllerService.GetMoviesViewRank(first, days);
#endregion
#endregion
}

View File

@@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WatchIt.Common.Model.Series;
using WatchIt.Database;
using WatchIt.WebAPI.Services.Controllers.Series;
namespace WatchIt.WebAPI.Controllers;
[ApiController]
[Route("series")]
public class SeriesController : ControllerBase
{
#region SERVICES
private readonly ISeriesControllerService _seriesControllerService;
#endregion
#region CONSTRUCTORS
public SeriesController(ISeriesControllerService seriesControllerService)
{
_seriesControllerService = seriesControllerService;
}
#endregion
#region PUBLIC METHODS
#region Main
[HttpGet]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<SeriesResponse>), StatusCodes.Status200OK)]
public async Task<ActionResult> GetAllSeries(SeriesQueryParameters query) => await _seriesControllerService.GetAllSeries(query);
[HttpGet("{id}")]
[AllowAnonymous]
[ProducesResponseType(typeof(SeriesResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> GetSeries([FromRoute] long id) => await _seriesControllerService.GetSeries(id);
[HttpPost]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(SeriesResponse), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> PostSeries([FromBody] SeriesRequest body) => await _seriesControllerService.PostSeries(body);
[HttpPut("{id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> PutSeries([FromRoute] long id, [FromBody]SeriesRequest body) => await _seriesControllerService.PutSeries(id, body);
[HttpDelete("{id}")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ProducesResponseType(typeof(void), StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status403Forbidden)]
public async Task<ActionResult> DeleteSeries([FromRoute] long id) => await _seriesControllerService.DeleteSeries(id);
#endregion
#region View count
[HttpGet("view")]
[AllowAnonymous]
[ProducesResponseType(typeof(IEnumerable<SeriesResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetSeriesViewRank([FromQuery] int first = 5, [FromQuery] int days = 7) => await _seriesControllerService.GetSeriesViewRank(first, days);
#endregion
#endregion
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
@@ -16,6 +17,7 @@
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Genres\WatchIt.WebAPI.Services.Controllers.Genres.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Media\WatchIt.WebAPI.Services.Controllers.Media.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Movies\WatchIt.WebAPI.Services.Controllers.Movies.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Controllers\WatchIt.WebAPI.Services.Controllers.Series\WatchIt.WebAPI.Services.Controllers.Series.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,6 +11,7 @@ using WatchIt.WebAPI.Services.Controllers.Common;
using WatchIt.WebAPI.Services.Utility.Tokens;
using WatchIt.WebAPI.Services.Utility.Tokens.Exceptions;
using WatchIt.WebAPI.Services.Utility.User;
using AccountProfilePicture = WatchIt.Common.Model.Accounts.AccountProfilePicture;
namespace WatchIt.WebAPI.Services.Controllers.Accounts;
@@ -73,16 +74,10 @@ public class AccountsControllerService(
return RequestResult.Unauthorized();
}
AuthenticateResponse response;
string refreshToken;
try
{
Task<string> refreshTokenTask = tokensService.ExtendRefreshTokenAsync(token.Account, token.Id);
Task<string> accessTokenTask = tokensService.CreateAccessTokenAsync(token.Account);
response = new AuthenticateResponse
{
AccessToken = await accessTokenTask,
RefreshToken = await refreshTokenTask,
};
refreshToken = await tokensService.ExtendRefreshTokenAsync(token.Account, token.Id);
}
catch (TokenNotFoundException)
{
@@ -90,11 +85,48 @@ public class AccountsControllerService(
}
catch (TokenNotExtendableException)
{
return RequestResult.Forbidden();
refreshToken = userService.GetRawToken().Replace("Bearer ", string.Empty);
}
string accessToken = await tokensService.CreateAccessTokenAsync(token.Account);
logger.LogInformation($"Account with ID {token.AccountId} was authenticated by token refreshing");
return RequestResult.Ok(response);
return RequestResult.Ok(new AuthenticateResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
});
}
public async Task<RequestResult> Logout()
{
Guid jti = userService.GetJti();
AccountRefreshToken? token = await database.AccountRefreshTokens.FirstOrDefaultAsync(x => x.Id == jti);
if (token is not null)
{
database.AccountRefreshTokens.Attach(token);
database.AccountRefreshTokens.Remove(token);
await database.SaveChangesAsync();
}
return RequestResult.NoContent();
}
public async Task<RequestResult> GetAccountProfilePicture(long id)
{
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
if (account is null)
{
return RequestResult.BadRequest()
.AddValidationError("id", "Account with this id does not exists");
}
if (account.ProfilePicture is null)
{
return RequestResult.NotFound();
}
AccountProfilePictureResponse picture = new AccountProfilePictureResponse(account.ProfilePicture);
return RequestResult.Ok(picture);
}
#endregion

View File

@@ -8,4 +8,6 @@ public interface IAccountsControllerService
Task<RequestResult> Register(RegisterRequest data);
Task<RequestResult> Authenticate(AuthenticateRequest data);
Task<RequestResult> AuthenticateRefresh();
Task<RequestResult> Logout();
Task<RequestResult> GetAccountProfilePicture(long id);
}

View File

@@ -5,9 +5,23 @@ namespace WatchIt.WebAPI.Services.Controllers.Media;
public interface IMediaControllerService
{
Task<RequestResult> GetGenres(long mediaId);
Task<RequestResult> PostGenre(long mediaId, short genreId);
Task<RequestResult> DeleteGenre(long mediaId, short genreId);
Task<RequestResult> GetMedia(long mediaId);
Task<RequestResult> GetMediaGenres(long mediaId);
Task<RequestResult> PostMediaGenre(long mediaId, short genreId);
Task<RequestResult> DeleteMediaGenre(long mediaId, short genreId);
Task<RequestResult> GetMediaRating(long mediaId);
Task<RequestResult> GetMediaRatingByUser(long mediaId, long userId);
Task<RequestResult> PutMediaRating(long mediaId, MediaRatingRequest data);
Task<RequestResult> DeleteMediaRating(long mediaId);
Task<RequestResult> PostMediaView(long mediaId);
Task<RequestResult> GetMediaPoster(long mediaId);
Task<RequestResult> PutMediaPoster(long mediaId, MediaPosterRequest data);
Task<RequestResult> DeleteMediaPoster(long mediaId);
Task<RequestResult> GetPhoto(Guid id);
Task<RequestResult> GetPhotos(MediaPhotoQueryParameters query);
Task<RequestResult> GetRandomBackgroundPhoto();

View File

@@ -4,6 +4,8 @@ using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media;
using WatchIt.Database;
using WatchIt.Database.Model.Media;
using WatchIt.Database.Model.Rating;
using WatchIt.Database.Model.ViewCount;
using WatchIt.WebAPI.Services.Controllers.Common;
using WatchIt.WebAPI.Services.Utility.User;
@@ -13,19 +15,40 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
{
#region PUBLIC METHODS
public async Task<RequestResult> GetGenres(long mediaId)
#region Main
public async Task<RequestResult> GetMedia(long mediaId)
{
MediaMovie? item = await database.MediaMovies.FirstOrDefaultAsync(x => x.Id == mediaId);
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
IEnumerable<GenreResponse> genres = item.Media.MediaGenres.Select(x => new GenreResponse(x.Genre));
MediaMovie? movie = await database.MediaMovies.FirstOrDefaultAsync(x => x.Id == mediaId);
MediaResponse mediaResponse = new MediaResponse(item, movie is not null ? MediaType.Movie : MediaType.Series);
return RequestResult.Ok(mediaResponse);
}
#endregion
#region Genres
public async Task<RequestResult> GetMediaGenres(long mediaId)
{
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
IEnumerable<GenreResponse> genres = item.MediaGenres.Select(x => new GenreResponse(x.Genre));
return RequestResult.Ok(genres);
}
public async Task<RequestResult> PostGenre(long mediaId, short genreId)
public async Task<RequestResult> PostMediaGenre(long mediaId, short genreId)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
@@ -50,7 +73,7 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
return RequestResult.Ok();
}
public async Task<RequestResult> DeleteGenre(long mediaId, short genreId)
public async Task<RequestResult> DeleteMediaGenre(long mediaId, short genreId)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
@@ -71,6 +94,207 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
return RequestResult.Ok();
}
#endregion
#region Rating
public async Task<RequestResult> GetMediaRating(long mediaId)
{
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
double ratingAverage = item.RatingMedia.Any() ? item.RatingMedia.Average(x => x.Rating) : 0;
long ratingCount = item.RatingMedia.Count();
MediaRatingResponse ratingResponse = new MediaRatingResponse(ratingAverage, ratingCount);
return RequestResult.Ok(ratingResponse);
}
public async Task<RequestResult> GetMediaRatingByUser(long mediaId, long userId)
{
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
short? rating = item.RatingMedia.FirstOrDefault(x => x.AccountId == userId)?.Rating;
if (!rating.HasValue)
{
return RequestResult.NotFound();
}
return RequestResult.Ok(rating.Value);
}
public async Task<RequestResult> PutMediaRating(long mediaId, MediaRatingRequest data)
{
short ratingValue = data.Rating;
if (ratingValue < 1 || ratingValue > 10)
{
return RequestResult.BadRequest();
}
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
long userId = userService.GetUserId();
RatingMedia? rating = item.RatingMedia.FirstOrDefault(x => x.AccountId == userId);
if (rating is not null)
{
rating.Rating = ratingValue;
}
else
{
rating = new RatingMedia
{
AccountId = userId,
MediaId = mediaId,
Rating = ratingValue
};
await database.RatingsMedia.AddAsync(rating);
}
await database.SaveChangesAsync();
return RequestResult.Ok();
}
public async Task<RequestResult> DeleteMediaRating(long mediaId)
{
long userId = userService.GetUserId();
RatingMedia? item = await database.RatingsMedia.FirstOrDefaultAsync(x => x.MediaId == mediaId && x.AccountId == userId);
if (item is null)
{
return RequestResult.Ok();
}
database.RatingsMedia.Attach(item);
database.RatingsMedia.Remove(item);
await database.SaveChangesAsync();
return RequestResult.Ok();
}
#endregion
#region View count
public async Task<RequestResult> PostMediaView(long mediaId)
{
Database.Model.Media.Media? item = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (item is null)
{
return RequestResult.NotFound();
}
DateOnly dateNow = DateOnly.FromDateTime(DateTime.Now);
ViewCountMedia? viewCount = await database.ViewCountsMedia.FirstOrDefaultAsync(x => x.MediaId == mediaId && x.Date == dateNow);
if (viewCount is null)
{
viewCount = new ViewCountMedia
{
MediaId = mediaId,
Date = dateNow,
ViewCount = 1
};
await database.ViewCountsMedia.AddAsync(viewCount);
}
else
{
viewCount.ViewCount++;
}
await database.SaveChangesAsync();
return RequestResult.Ok();
}
#endregion
#region Poster
public async Task<RequestResult> GetMediaPoster(long mediaId)
{
Database.Model.Media.Media? media = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (media is null)
{
return RequestResult.BadRequest();
}
MediaPosterImage? poster = media.MediaPosterImage;
if (poster is null)
{
return RequestResult.NotFound();
}
MediaPosterResponse data = new MediaPosterResponse(poster);
return RequestResult.Ok(data);
}
public async Task<RequestResult> PutMediaPoster(long mediaId, MediaPosterRequest data)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
Database.Model.Media.Media? media = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (media is null)
{
return RequestResult.BadRequest();
}
if (media.MediaPosterImage is null)
{
MediaPosterImage image = data.CreateMediaPosterImage();
await database.MediaPosterImages.AddAsync(image);
await database.SaveChangesAsync();
media.MediaPosterImageId = image.Id;
}
else
{
data.UpdateMediaPosterImage(media.MediaPosterImage);
}
await database.SaveChangesAsync();
MediaPosterResponse returnData = new MediaPosterResponse(media.MediaPosterImage);
return RequestResult.Ok(returnData);
}
public async Task<RequestResult> DeleteMediaPoster(long mediaId)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
Database.Model.Media.Media? media = await database.Media.FirstOrDefaultAsync(x => x.Id == mediaId);
if (media?.MediaPosterImage != null)
{
database.MediaPosterImages.Attach(media.MediaPosterImage);
database.MediaPosterImages.Remove(media.MediaPosterImage);
await database.SaveChangesAsync();
}
return RequestResult.NoContent();
}
#endregion
#region Photos
public async Task<RequestResult> GetPhoto(Guid id)
{
MediaPhotoImage? item = await database.MediaPhotoImages.FirstOrDefaultAsync(x => x.Id == id);
@@ -199,4 +423,6 @@ public class MediaControllerService(DatabaseContext database, IUserService userS
}
#endregion
#endregion
}

View File

@@ -5,9 +5,11 @@ namespace WatchIt.WebAPI.Services.Controllers.Movies;
public interface IMoviesControllerService
{
Task<RequestResult> GetAll(MovieQueryParameters query);
Task<RequestResult> Get(long id);
Task<RequestResult> Post(MovieRequest data);
Task<RequestResult> Put(long id, MovieRequest data);
Task<RequestResult> Delete(long id);
Task<RequestResult> GetAllMovies(MovieQueryParameters query);
Task<RequestResult> GetMovie(long id);
Task<RequestResult> PostMovie(MovieRequest data);
Task<RequestResult> PutMovie(long id, MovieRequest data);
Task<RequestResult> DeleteMovie(long id);
Task<RequestResult> GetMoviesViewRank(int first, int days);
}

View File

@@ -7,20 +7,45 @@ using WatchIt.WebAPI.Services.Utility.User;
namespace WatchIt.WebAPI.Services.Controllers.Movies;
public class MoviesControllerService(DatabaseContext database, IUserService userService) : IMoviesControllerService
public class MoviesControllerService : IMoviesControllerService
{
#region SERVICES
private readonly DatabaseContext _database;
private readonly IUserService _userService;
#endregion
#region CONSTRUCTORS
public MoviesControllerService(DatabaseContext database, IUserService userService)
{
_database = database;
_userService = userService;
}
#endregion
#region PUBLIC METHODS
public async Task<RequestResult> GetAll(MovieQueryParameters query)
#region Main
public async Task<RequestResult> GetAllMovies(MovieQueryParameters query)
{
IEnumerable<MovieResponse> data = await database.MediaMovies.Select(x => new MovieResponse(x)).ToListAsync();
IEnumerable<MovieResponse> data = await _database.MediaMovies.Select(x => new MovieResponse(x)).ToListAsync();
data = query.PrepareData(data);
return RequestResult.Ok(data);
}
public async Task<RequestResult> Get(long id)
public async Task<RequestResult> GetMovie(long id)
{
MediaMovie? item = await database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
MediaMovie? item = await _database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
@@ -30,33 +55,33 @@ public class MoviesControllerService(DatabaseContext database, IUserService user
return RequestResult.Ok(data);
}
public async Task<RequestResult> Post(MovieRequest data)
public async Task<RequestResult> PostMovie(MovieRequest data)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
Media mediaItem = data.CreateMedia();
await database.Media.AddAsync(mediaItem);
await database.SaveChangesAsync();
await _database.Media.AddAsync(mediaItem);
await _database.SaveChangesAsync();
MediaMovie mediaMovieItem = data.CreateMediaMovie(mediaItem.Id);
await database.MediaMovies.AddAsync(mediaMovieItem);
await database.SaveChangesAsync();
await _database.MediaMovies.AddAsync(mediaMovieItem);
await _database.SaveChangesAsync();
return RequestResult.Created($"movies/{mediaItem.Id}", new MovieResponse(mediaMovieItem));
}
public async Task<RequestResult> Put(long id, MovieRequest data)
public async Task<RequestResult> PutMovie(long id, MovieRequest data)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaMovie? item = await database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
MediaMovie? item = await _database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
@@ -64,49 +89,74 @@ public class MoviesControllerService(DatabaseContext database, IUserService user
data.UpdateMediaMovie(item);
data.UpdateMedia(item.Media);
await database.SaveChangesAsync();
await _database.SaveChangesAsync();
return RequestResult.Ok();
}
public async Task<RequestResult> Delete(long id)
public async Task<RequestResult> DeleteMovie(long id)
{
UserValidator validator = userService.GetValidator().MustBeAdmin();
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaMovie? item = await database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
MediaMovie? item = await _database.MediaMovies.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
}
database.MediaMovies.Attach(item);
database.MediaMovies.Remove(item);
database.MediaPosterImages.Attach(item.Media.MediaPosterImage!);
database.MediaPosterImages.Remove(item.Media.MediaPosterImage!);
database.MediaPhotoImages.AttachRange(item.Media.MediaPhotoImages);
database.MediaPhotoImages.RemoveRange(item.Media.MediaPhotoImages);
database.MediaGenres.AttachRange(item.Media.MediaGenres);
database.MediaGenres.RemoveRange(item.Media.MediaGenres);
database.MediaProductionCountries.AttachRange(item.Media.MediaProductionCountries);
database.MediaProductionCountries.RemoveRange(item.Media.MediaProductionCountries);
database.PersonActorRoles.AttachRange(item.Media.PersonActorRoles);
database.PersonActorRoles.RemoveRange(item.Media.PersonActorRoles);
database.PersonCreatorRoles.AttachRange(item.Media.PersonCreatorRoles);
database.PersonCreatorRoles.RemoveRange(item.Media.PersonCreatorRoles);
database.RatingsMedia.AttachRange(item.Media.RatingMedia);
database.RatingsMedia.RemoveRange(item.Media.RatingMedia);
database.ViewCountsMedia.AttachRange(item.Media.ViewCountsMedia);
database.ViewCountsMedia.RemoveRange(item.Media.ViewCountsMedia);
database.Media.Attach(item.Media);
database.Media.Remove(item.Media);
await database.SaveChangesAsync();
_database.MediaMovies.Attach(item);
_database.MediaMovies.Remove(item);
_database.MediaPosterImages.Attach(item.Media.MediaPosterImage!);
_database.MediaPosterImages.Remove(item.Media.MediaPosterImage!);
_database.MediaPhotoImages.AttachRange(item.Media.MediaPhotoImages);
_database.MediaPhotoImages.RemoveRange(item.Media.MediaPhotoImages);
_database.MediaGenres.AttachRange(item.Media.MediaGenres);
_database.MediaGenres.RemoveRange(item.Media.MediaGenres);
_database.MediaProductionCountries.AttachRange(item.Media.MediaProductionCountries);
_database.MediaProductionCountries.RemoveRange(item.Media.MediaProductionCountries);
_database.PersonActorRoles.AttachRange(item.Media.PersonActorRoles);
_database.PersonActorRoles.RemoveRange(item.Media.PersonActorRoles);
_database.PersonCreatorRoles.AttachRange(item.Media.PersonCreatorRoles);
_database.PersonCreatorRoles.RemoveRange(item.Media.PersonCreatorRoles);
_database.RatingsMedia.AttachRange(item.Media.RatingMedia);
_database.RatingsMedia.RemoveRange(item.Media.RatingMedia);
_database.ViewCountsMedia.AttachRange(item.Media.ViewCountsMedia);
_database.ViewCountsMedia.RemoveRange(item.Media.ViewCountsMedia);
_database.Media.Attach(item.Media);
_database.Media.Remove(item.Media);
await _database.SaveChangesAsync();
return RequestResult.Ok();
return RequestResult.NoContent();
}
#endregion
#region View count
public async Task<RequestResult> GetMoviesViewRank(int first, int days)
{
if (first < 1 || days < 1)
{
return RequestResult.BadRequest();
}
DateOnly startDate = DateOnly.FromDateTime(DateTime.Now).AddDays(-days);
IEnumerable<MediaMovie> rawData = await _database.MediaMovies.OrderByDescending(x => x.Media.ViewCountsMedia.Where(y => y.Date >= startDate)
.Sum(y => y.ViewCount))
.ThenBy(x => x.Id)
.Take(first)
.ToListAsync();
IEnumerable<MovieResponse> data = rawData.Select(x => new MovieResponse(x));
return RequestResult.Ok(data);
}
#endregion
#endregion
}

View File

@@ -0,0 +1,15 @@
using WatchIt.Common.Model.Series;
using WatchIt.WebAPI.Services.Controllers.Common;
namespace WatchIt.WebAPI.Services.Controllers.Series;
public interface ISeriesControllerService
{
Task<RequestResult> GetAllSeries(SeriesQueryParameters query);
Task<RequestResult> GetSeries(long id);
Task<RequestResult> PostSeries(SeriesRequest data);
Task<RequestResult> PutSeries(long id, SeriesRequest data);
Task<RequestResult> DeleteSeries(long id);
Task<RequestResult> GetSeriesViewRank(int first, int days);
}

View File

@@ -0,0 +1,162 @@
using Microsoft.EntityFrameworkCore;
using WatchIt.Common.Model.Series;
using WatchIt.Database;
using WatchIt.Database.Model.Media;
using WatchIt.WebAPI.Services.Controllers.Common;
using WatchIt.WebAPI.Services.Utility.User;
namespace WatchIt.WebAPI.Services.Controllers.Series;
public class SeriesControllerService : ISeriesControllerService
{
#region SERVICES
private readonly DatabaseContext _database;
private readonly IUserService _userService;
#endregion
#region CONSTRUCTORS
public SeriesControllerService(DatabaseContext database, IUserService userService)
{
_database = database;
_userService = userService;
}
#endregion
#region PUBLIC METHODS
#region Main
public async Task<RequestResult> GetAllSeries(SeriesQueryParameters query)
{
IEnumerable<SeriesResponse> data = await _database.MediaSeries.Select(x => new SeriesResponse(x)).ToListAsync();
data = query.PrepareData(data);
return RequestResult.Ok(data);
}
public async Task<RequestResult> GetSeries(long id)
{
MediaSeries? item = await _database.MediaSeries.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
}
SeriesResponse data = new SeriesResponse(item);
return RequestResult.Ok(data);
}
public async Task<RequestResult> PostSeries(SeriesRequest data)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
Media mediaItem = data.CreateMedia();
await _database.Media.AddAsync(mediaItem);
await _database.SaveChangesAsync();
MediaSeries mediaSeriesItem = data.CreateMediaSeries(mediaItem.Id);
await _database.MediaSeries.AddAsync(mediaSeriesItem);
await _database.SaveChangesAsync();
return RequestResult.Created($"series/{mediaItem.Id}", new SeriesResponse(mediaSeriesItem));
}
public async Task<RequestResult> PutSeries(long id, SeriesRequest data)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaSeries? item = await _database.MediaSeries.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
}
data.UpdateMediaSeries(item);
data.UpdateMedia(item.Media);
await _database.SaveChangesAsync();
return RequestResult.Ok();
}
public async Task<RequestResult> DeleteSeries(long id)
{
UserValidator validator = _userService.GetValidator().MustBeAdmin();
if (!validator.IsValid)
{
return RequestResult.Forbidden();
}
MediaSeries? item = await _database.MediaSeries.FirstOrDefaultAsync(x => x.Id == id);
if (item is null)
{
return RequestResult.NotFound();
}
_database.MediaSeries.Attach(item);
_database.MediaSeries.Remove(item);
_database.MediaPosterImages.Attach(item.Media.MediaPosterImage!);
_database.MediaPosterImages.Remove(item.Media.MediaPosterImage!);
_database.MediaPhotoImages.AttachRange(item.Media.MediaPhotoImages);
_database.MediaPhotoImages.RemoveRange(item.Media.MediaPhotoImages);
_database.MediaGenres.AttachRange(item.Media.MediaGenres);
_database.MediaGenres.RemoveRange(item.Media.MediaGenres);
_database.MediaProductionCountries.AttachRange(item.Media.MediaProductionCountries);
_database.MediaProductionCountries.RemoveRange(item.Media.MediaProductionCountries);
_database.PersonActorRoles.AttachRange(item.Media.PersonActorRoles);
_database.PersonActorRoles.RemoveRange(item.Media.PersonActorRoles);
_database.PersonCreatorRoles.AttachRange(item.Media.PersonCreatorRoles);
_database.PersonCreatorRoles.RemoveRange(item.Media.PersonCreatorRoles);
_database.RatingsMedia.AttachRange(item.Media.RatingMedia);
_database.RatingsMedia.RemoveRange(item.Media.RatingMedia);
_database.ViewCountsMedia.AttachRange(item.Media.ViewCountsMedia);
_database.ViewCountsMedia.RemoveRange(item.Media.ViewCountsMedia);
_database.Media.Attach(item.Media);
_database.Media.Remove(item.Media);
await _database.SaveChangesAsync();
return RequestResult.NoContent();
}
#endregion
#region View count
public async Task<RequestResult> GetSeriesViewRank(int first, int days)
{
if (first < 1 || days < 1)
{
return RequestResult.BadRequest();
}
DateOnly startDate = DateOnly.FromDateTime(DateTime.Now).AddDays(-days);
IEnumerable<MediaSeries> rawData = await _database.MediaSeries.OrderByDescending(x => x.Media.ViewCountsMedia.Where(y => y.Date >= startDate)
.Sum(y => y.ViewCount))
.ThenBy(x => x.Id)
.Take(first)
.ToListAsync();
IEnumerable<SeriesResponse> data = rawData.Select(x => new SeriesResponse(x));
return RequestResult.Ok(data);
}
#endregion
#endregion
}

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Model\WatchIt.Common.Model.csproj" />
<ProjectReference Include="..\..\..\..\WatchIt.Database\WatchIt.Database\WatchIt.Database.csproj" />
<ProjectReference Include="..\..\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.User\WatchIt.WebAPI.Services.Utility.User.csproj" />
<ProjectReference Include="..\WatchIt.WebAPI.Services.Controllers.Common\WatchIt.WebAPI.Services.Controllers.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -85,21 +85,24 @@ public class TokensService(DatabaseContext database, IConfigurationService confi
return TokenToString(tokenDescriptor);
}
protected SecurityTokenDescriptor CreateBaseSecurityTokenDescriptor(Account account, Guid id, DateTime expirationTime) => new SecurityTokenDescriptor
protected SecurityTokenDescriptor CreateBaseSecurityTokenDescriptor(Account account, Guid id, DateTime expirationTime)
{
Subject = new ClaimsIdentity(new List<Claim>
return new SecurityTokenDescriptor
{
new Claim(JwtRegisteredClaimNames.Jti, id.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, account.Email),
new Claim(JwtRegisteredClaimNames.UniqueName, account.Username),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.Ticks.ToString()),
new Claim("admin", account.IsAdmin.ToString()),
}),
Expires = expirationTime,
Issuer = configurationService.Data.Authentication.Issuer,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurationService.Data.Authentication.Key)), SecurityAlgorithms.HmacSha512)
};
Subject = new ClaimsIdentity(new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Jti, id.ToString()),
new Claim(JwtRegisteredClaimNames.Sub, account.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, account.Email),
new Claim(JwtRegisteredClaimNames.UniqueName, account.Username),
new Claim(JwtRegisteredClaimNames.Exp, expirationTime.Ticks.ToString()),
new Claim("admin", account.IsAdmin.ToString()),
}),
Expires = expirationTime,
Issuer = configurationService.Data.Authentication.Issuer,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configurationService.Data.Authentication.Key)), SecurityAlgorithms.HmacSha512)
};
}
protected string TokenToString(SecurityTokenDescriptor tokenDescriptor)
{

View File

@@ -0,0 +1,12 @@
using System.Security.Claims;
namespace WatchIt.WebAPI.Services.Utility.User;
public interface IUserService
{
ClaimsPrincipal GetRawUser();
string? GetRawToken();
UserValidator GetValidator();
Guid GetJti();
long GetUserId();
}

View File

@@ -18,6 +18,15 @@ public class UserService(DatabaseContext database, IHttpContextAccessor accessor
return accessor.HttpContext.User;
}
public string? GetRawToken()
{
if (accessor.HttpContext is null)
{
throw new NullReferenceException();
}
return accessor.HttpContext.Request.Headers.Authorization;
}
public UserValidator GetValidator()
{
ClaimsPrincipal rawUser = GetRawUser();
@@ -32,5 +41,13 @@ public class UserService(DatabaseContext database, IHttpContextAccessor accessor
return guid;
}
public long GetUserId()
{
ClaimsPrincipal user = GetRawUser();
Claim subClaim = user.FindFirst(JwtRegisteredClaimNames.Sub)!;
long id = long.Parse(subClaim.Value);
return id;
}
#endregion
}

View File

@@ -0,0 +1,57 @@
using System.Security.Claims;
using WatchIt.Database;
namespace WatchIt.WebAPI.Services.Utility.User;
public class UserValidator
{
#region FIELDS
protected readonly DatabaseContext _database;
protected readonly ClaimsPrincipal _claimsPrincipal;
protected readonly List<string> _validationErrors;
#endregion
#region PROPERTIES
public bool IsValid { get; protected set; }
public IEnumerable<string> ValidationErrors => _validationErrors;
#endregion
#region CONSTRUCTORS
internal UserValidator(DatabaseContext database, ClaimsPrincipal claimsPrincipal)
{
_database = database;
_claimsPrincipal = claimsPrincipal;
_validationErrors = new List<string>();
IsValid = true;
}
#endregion
#region PUBLIC METHODS
public UserValidator MustBeAdmin()
{
Claim adminClaim = _claimsPrincipal.FindFirst(x => x.Type == "admin")!;
if (adminClaim.Value == bool.FalseString)
{
IsValid = false;
_validationErrors.Add("User is not admin");
}
return this;
}
#endregion
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
using WatchIt.Common.Model.Media;
using WatchIt.Database;
namespace WatchIt.WebAPI.Validators.Media;
public class MediaPosterRequestValidator : AbstractValidator<MediaPosterRequest>
{
public MediaPosterRequestValidator()
{
RuleFor(x => x.Image).NotEmpty();
RuleFor(x => x.MimeType).Matches(@"\w+/.+").WithMessage("Incorrect mimetype");
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
using WatchIt.Common.Model.Movies;
namespace WatchIt.WebAPI.Validators.Movies;
public class MovieRequestValidator : AbstractValidator<MovieRequest>
{
public MovieRequestValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(250);
RuleFor(x => x.OriginalTitle).MaximumLength(250);
RuleFor(x => x.Description).MaximumLength(1000);
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
using WatchIt.Common.Model.Series;
namespace WatchIt.WebAPI.Validators.Movies;
public class SeriesRequestValidator : AbstractValidator<SeriesRequest>
{
public SeriesRequestValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(250);
RuleFor(x => x.OriginalTitle).MaximumLength(250);
RuleFor(x => x.Description).MaximumLength(1000);
}
}

View File

@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WatchIt.Database;
@@ -5,13 +6,36 @@ using WatchIt.Database.Model.Account;
namespace WatchIt.WebAPI.WorkerServices;
public class DeleteExpiredRefreshTokensService(ILogger<DeleteExpiredRefreshTokensService> logger, DatabaseContext database) : BackgroundService
public class DeleteExpiredRefreshTokensService : BackgroundService
{
#region SERVICES
private readonly ILogger<DeleteExpiredRefreshTokensService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
#endregion
#region CONSTRUCTORS
public DeleteExpiredRefreshTokensService(ILogger<DeleteExpiredRefreshTokensService> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
#endregion
#region PUBLIC METHODS
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
Task delayTask = Task.Delay(300000, stoppingToken);
Task actionTask = Action();
@@ -22,9 +46,16 @@ public class DeleteExpiredRefreshTokensService(ILogger<DeleteExpiredRefreshToken
protected async Task Action()
{
IEnumerable<AccountRefreshToken> tokens = database.AccountRefreshTokens.Where(x => x.ExpirationDate < DateTime.UtcNow);
database.AccountRefreshTokens.AttachRange(tokens);
database.AccountRefreshTokens.RemoveRange(tokens);
await database.SaveChangesAsync();
using (IServiceScope scope = _serviceScopeFactory.CreateScope())
{
DatabaseContext database = scope.ServiceProvider.GetService<DatabaseContext>();
IEnumerable<AccountRefreshToken> tokens = database.AccountRefreshTokens.Where(x => x.ExpirationDate < DateTime.UtcNow);
database.AccountRefreshTokens.AttachRange(tokens);
database.AccountRefreshTokens.RemoveRange(tokens);
await database.SaveChangesAsync();
}
}
#endregion
}

View File

@@ -1,3 +1,4 @@
using System.IdentityModel.Tokens.Jwt;
using System.Reflection;
using System.Text;
using FluentValidation;
@@ -5,12 +6,14 @@ using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using WatchIt.Database;
using WatchIt.WebAPI.Services.Controllers.Accounts;
using WatchIt.WebAPI.Services.Controllers.Genres;
using WatchIt.WebAPI.Services.Controllers.Media;
using WatchIt.WebAPI.Services.Controllers.Movies;
using WatchIt.WebAPI.Services.Controllers.Series;
using WatchIt.WebAPI.Services.Utility.Configuration;
using WatchIt.WebAPI.Services.Utility.Tokens;
using WatchIt.WebAPI.Services.Utility.User;
@@ -26,12 +29,12 @@ public static class Program
public static void Main(string[] args)
{
WebApplication app = WebApplication.CreateBuilder(args)
.SetupAuthentication()
.SetupDatabase()
.SetupWorkerServices()
.SetupServices()
.SetupApplication()
.Build();
.SetupAuthentication()
.SetupDatabase()
.SetupWorkerServices()
.SetupServices()
.SetupApplication()
.Build();
if (app.Environment.IsDevelopment())
{
@@ -41,6 +44,7 @@ public static class Program
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
@@ -56,6 +60,9 @@ public static class Program
private static WebApplicationBuilder SetupAuthentication(this WebApplicationBuilder builder)
{
JsonWebTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
AuthenticationBuilder authenticationBuilder = builder.Services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@@ -121,7 +128,7 @@ public static class Program
private static WebApplicationBuilder SetupDatabase(this WebApplicationBuilder builder)
{
builder.Services.AddDbContext<DatabaseContext>(x => x.UseLazyLoadingProxies().UseNpgsql(builder.Configuration.GetConnectionString("Default")), ServiceLifetime.Singleton);
builder.Services.AddDbContext<DatabaseContext>(x => x.UseLazyLoadingProxies().UseNpgsql(builder.Configuration.GetConnectionString("Default")), ServiceLifetime.Transient);
return builder;
}
@@ -134,15 +141,16 @@ public static class Program
private static WebApplicationBuilder SetupServices(this WebApplicationBuilder builder)
{
// Utility
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
builder.Services.AddSingleton<ITokensService, TokensService>();
builder.Services.AddSingleton<IUserService, UserService>();
builder.Services.AddTransient<IConfigurationService, ConfigurationService>();
builder.Services.AddTransient<ITokensService, TokensService>();
builder.Services.AddTransient<IUserService, UserService>();
// Controller
builder.Services.AddSingleton<IAccountsControllerService, AccountsControllerService>();
builder.Services.AddSingleton<IGenresControllerService, GenresControllerService>();
builder.Services.AddSingleton<IMoviesControllerService, MoviesControllerService>();
builder.Services.AddSingleton<IMediaControllerService, MediaControllerService>();
builder.Services.AddTransient<IAccountsControllerService, AccountsControllerService>();
builder.Services.AddTransient<IGenresControllerService, GenresControllerService>();
builder.Services.AddTransient<IMoviesControllerService, MoviesControllerService>();
builder.Services.AddTransient<IMediaControllerService, MediaControllerService>();
builder.Services.AddTransient<ISeriesControllerService, SeriesControllerService>();
return builder;
}

View File

@@ -0,0 +1,85 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Components.Authorization;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
namespace WatchIt.Website.Services.Utility.Authentication;
public class AuthenticationService : IAuthenticationService
{
#region SERVICES
private readonly AuthenticationStateProvider _authenticationStateProvider;
private readonly HttpClient _httpClient;
private readonly ITokensService _tokensService;
private readonly IAccountsWebAPIService _accountsWebAPIService;
#endregion
#region CONSTRUCTORS
public AuthenticationService(AuthenticationStateProvider authenticationStateProvider, HttpClient httpClient, ITokensService tokensService, IAccountsWebAPIService accountsWebAPIService)
{
_authenticationStateProvider = authenticationStateProvider;
_httpClient = httpClient;
_tokensService = tokensService;
_accountsWebAPIService = accountsWebAPIService;
}
#endregion
#region PUBLIC METHODS
public async Task<User?> GetUserAsync()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
if (!GetAuthenticationStatusAsync(state))
{
return null;
}
return new User
{
Id = int.Parse(state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.Sub)!.Value),
Username = state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.UniqueName)!.Value,
Email = state.User.FindFirst(x => x.Type == JwtRegisteredClaimNames.Email)!.Value,
IsAdmin = bool.Parse(state.User.FindFirst(x => x.Type == "admin")!.Value),
};
}
public async Task<bool> GetAuthenticationStatusAsync()
{
AuthenticationState state = await _authenticationStateProvider.GetAuthenticationStateAsync();
return GetAuthenticationStatusAsync(state);
}
public async Task LogoutAsync()
{
string? refreshToken = await _tokensService.GetRefreshToken();
if (refreshToken is not null)
{
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("refresh", refreshToken.Replace("\"", ""));
await _accountsWebAPIService.Logout();
_httpClient.DefaultRequestHeaders.Authorization = null;
}
}
#endregion
#region PRIVATE METHODS
private bool GetAuthenticationStatusAsync(AuthenticationState state)
{
return state.User.HasClaim(x => x.Type == JwtRegisteredClaimNames.Iss && x.Value == "WatchIt");
}
#endregion
}

View File

@@ -0,0 +1,8 @@
namespace WatchIt.Website.Services.Utility.Authentication;
public interface IAuthenticationService
{
Task<User?> GetUserAsync();
Task<bool> GetAuthenticationStatusAsync();
Task LogoutAsync();
}

View File

@@ -0,0 +1,165 @@
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using WatchIt.Common.Model.Accounts;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
namespace WatchIt.Website.Services.Utility.Authentication;
public class JWTAuthenticationStateProvider : AuthenticationStateProvider
{
#region SERVICES
private readonly HttpClient _httpClient;
private readonly ILogger<JWTAuthenticationStateProvider> _logger;
private readonly ITokensService _tokensService;
private readonly IAccountsWebAPIService _accountsService;
#endregion
#region CONSTRUCTORS
public JWTAuthenticationStateProvider(HttpClient httpClient, ILogger<JWTAuthenticationStateProvider> logger, ITokensService tokensService, IAccountsWebAPIService accountsService)
{
_httpClient = httpClient;
_logger = logger;
_tokensService = tokensService;
_accountsService = accountsService;
}
#endregion
#region PUBLIC METHODS
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
AuthenticationState state = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
Task<string?> accessTokenTask = _tokensService.GetAccessToken();
Task<string?> refreshTokenTask = _tokensService.GetRefreshToken();
await Task.WhenAll(accessTokenTask, refreshTokenTask);
string? accessToken = await accessTokenTask;
string? refreshToken = await refreshTokenTask;
bool refreshed = false;
if (string.IsNullOrWhiteSpace(accessToken))
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
return state;
}
string? accessTokenNew = await Refresh(refreshToken);
if (string.IsNullOrWhiteSpace(accessToken))
{
return state;
}
accessToken = accessTokenNew;
refreshed = true;
}
IEnumerable<Claim> claims = GetClaimsFromToken(accessToken);
Claim? expClaim = claims.FirstOrDefault(c => c.Type == "exp");
if (expClaim is not null && ConvertFromUnixTimestamp(int.Parse(expClaim.Value)) > DateTime.UtcNow)
{
if (refreshed)
{
return state;
}
}
else
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
return state;
}
string? accessTokenNew = await Refresh(refreshToken);
if (accessTokenNew is null)
{
return state;
}
accessToken = accessTokenNew;
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Replace("\"", ""));
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims)));
}
#endregion
#region PRIVATE METHODS
private async Task<string?> Refresh(string refreshToken)
{
AuthenticateResponse? response = null;
void SetResponse(AuthenticateResponse data)
{
response = data;
}
await _accountsService.AuthenticateRefresh(SetResponse);
if (response is not null)
{
await _tokensService.SaveAuthenticationData(response);
}
else
{
await _tokensService.RemoveAuthenticationData();
}
return response?.AccessToken;
}
private static IEnumerable<Claim> GetClaimsFromToken(string token)
{
string payload = token.Split('.')[1];
switch (payload.Length % 4)
{
case 2: payload += "=="; break;
case 3: payload += "="; break;
}
byte[] jsonBytes = Convert.FromBase64String(payload);
Dictionary<string, object>? keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
if (keyValuePairs is null)
{
throw new Exception("Incorrect token");
}
return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()));
}
public static DateTime ConvertFromUnixTimestamp(int timestamp)
{
DateTime date = new DateTime(1970, 1, 1, 0, 0, 0, 0);
date = date.AddSeconds(timestamp);
return date;
}
#endregion
}

View File

@@ -0,0 +1,13 @@
namespace WatchIt.Website.Services.Utility.Authentication;
public class User
{
#region PROPERTIES
public required long Id { get; init; }
public required string Username { get; init; }
public required string Email { get; init; }
public required bool IsAdmin { get; init; }
#endregion
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Accounts\WatchIt.Website.Services.WebAPI.Accounts.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.Utility.Tokens\WatchIt.Website.Services.Utility.Tokens.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,4 +6,6 @@ public class Accounts
public string Register { get; set; }
public string Authenticate { get; set; }
public string AuthenticateRefresh { get; set; }
public string Logout { get; set; }
public string GetProfilePicture { get; set; }
}

View File

@@ -4,5 +4,6 @@ public class ConfigurationData
{
public Logging Logging { get; set; }
public string AllowedHosts { get; set; }
public StorageKeys StorageKeys { get; set; }
public Endpoints Endpoints { get; set; }
}

View File

@@ -5,6 +5,7 @@ public class Endpoints
public string Base { get; set; }
public Accounts Accounts { get; set; }
public Genres Genres { get; set; }
public Movies Movies { get; set; }
public Media Media { get; set; }
public Movies Movies { get; set; }
public Series Series { get; set; }
}

View File

@@ -3,10 +3,19 @@
public class Media
{
public string Base { get; set; }
public string Get { get; set; }
public string GetGenres { get; set; }
public string PostGenre { get; set; }
public string DeleteGenre { get; set; }
public string GetMediaRating { get; set; }
public string GetMediaRatingByUser { get; set; }
public string PutMediaRating { get; set; }
public string DeleteMediaRating { get; set; }
public string PostMediaView { get; set; }
public string GetPhotoMediaRandomBackground { get; set; }
public string GetPoster { get; set; }
public string PutPoster { get; set; }
public string DeletePoster { get; set; }
public string GetPhoto { get; set; }
public string GetPhotos { get; set; }
public string GetPhotoRandomBackground { get; set; }

View File

@@ -3,12 +3,10 @@
public class Movies
{
public string Base { get; set; }
public string GetAll { get; set; }
public string Get { get; set; }
public string Post { get; set; }
public string Put { get; set; }
public string Delete { get; set; }
public string GetGenres { get; set; }
public string PostGenre { get; set; }
public string DeleteGenre { get; set; }
public string GetAllMovies { get; set; }
public string GetMovie { get; set; }
public string PostMovie { get; set; }
public string PutMovie { get; set; }
public string DeleteMovie { get; set; }
public string GetMoviesViewRank { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace WatchIt.Website.Services.Utility.Configuration.Model;
public class Series
{
public string Base { get; set; }
public string GetAllSeries { get; set; }
public string GetSeries { get; set; }
public string PostSeries { get; set; }
public string PutSeries { get; set; }
public string DeleteSeries { get; set; }
public string GetSeriesViewRank { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace WatchIt.Website.Services.Utility.Configuration.Model;
public class StorageKeys
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}

View File

@@ -0,0 +1,15 @@
using WatchIt.Common.Model.Accounts;
namespace WatchIt.Website.Services.Utility.Tokens;
public interface ITokensService
{
Task<string?> GetAccessToken();
Task<string?> GetRefreshToken();
Task SaveAuthenticationData(AuthenticateResponse authenticateResponse);
Task SaveAccessToken(string accessToken);
Task SaveRefreshToken(string refreshToken);
Task RemoveAuthenticationData();
Task RemoveAccessToken();
Task RemoveRefreshToken();
}

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using WatchIt.Common.Model.Accounts;
using WatchIt.Website.Services.Utility.Configuration;
namespace WatchIt.Website.Services.Utility.Tokens;
public class TokensService : ITokensService
{
#region SERVICES
private readonly ProtectedLocalStorage _localStorageService;
private readonly IConfigurationService _configurationService;
#endregion
#region CONSTRUCTORS
public TokensService(ProtectedLocalStorage localStorageService, IConfigurationService configurationService)
{
_localStorageService = localStorageService;
_configurationService = configurationService;
}
#endregion
#region PUBLIC METHODS
public async Task<string?> GetAccessToken() => await GetValueAsync<string>(GetAccessTokenStorageKey());
public async Task<string?> GetRefreshToken() => await GetValueAsync<string>(GetRefreshTokenStorageKey());
public async Task SaveAuthenticationData(AuthenticateResponse authenticateResponse) => await Task.WhenAll(SaveAccessToken(authenticateResponse.AccessToken), SaveRefreshToken(authenticateResponse.RefreshToken));
public async Task SaveAccessToken(string accessToken) => await _localStorageService.SetAsync(GetAccessTokenStorageKey(), accessToken);
public async Task SaveRefreshToken(string refreshToken) => await _localStorageService.SetAsync(GetRefreshTokenStorageKey(), refreshToken);
public async Task RemoveAuthenticationData() => await Task.WhenAll(RemoveAccessToken(), RemoveRefreshToken());
public async Task RemoveAccessToken() => await _localStorageService.DeleteAsync(GetAccessTokenStorageKey());
public async Task RemoveRefreshToken() => await _localStorageService.DeleteAsync(GetRefreshTokenStorageKey());
#endregion
#region PRIVATE METHODS
private string GetAccessTokenStorageKey() => _configurationService.Data.StorageKeys.AccessToken;
private string GetRefreshTokenStorageKey() => _configurationService.Data.StorageKeys.RefreshToken;
private async Task<T?> GetValueAsync<T>(string key)
{
ProtectedBrowserStorageResult<T> result = await _localStorageService.GetAsync<T>(key);
return result.Success ? result.Value : default;
}
#endregion
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WatchIt.Website.Services.Utility.Token</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Model\WatchIt.Common.Model.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -2,15 +2,16 @@
using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.Utility.Configuration.Model;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Common;
namespace WatchIt.Website.Services.WebAPI.Accounts;
public class AccountsWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : BaseWebAPIService(configurationService), IAccountsWebAPIService
public class AccountsWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService, ITokensService tokensService) : BaseWebAPIService(configurationService), IAccountsWebAPIService
{
#region PUBLIC METHODS
public async Task Register(RegisterRequest data, Action<RegisterResponse> createdAction, Action<IDictionary<string, string[]>> badRequestAction)
public async Task Register(RegisterRequest data, Action<RegisterResponse>? createdAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.Register);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url)
@@ -24,7 +25,7 @@ public class AccountsWebAPIService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task Authenticate(AuthenticateRequest data, Action<AuthenticateResponse> successAction, Action<IDictionary<string, string[]>> badRequestAction, Action unauthorizedAction)
public async Task Authenticate(AuthenticateRequest data, Action<AuthenticateResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.Authenticate);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url)
@@ -39,10 +40,13 @@ public class AccountsWebAPIService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task AuthenticateRefresh(Action<AuthenticateResponse> successAction, Action unauthorizedAction, Action forbiddenAction)
public async Task AuthenticateRefresh(Action<AuthenticateResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.AuthenticateRefresh);
string? token = await tokensService.GetRefreshToken();
HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
request.Headers.Add("Authorization", $"Bearer {token}");
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
@@ -51,6 +55,31 @@ public class AccountsWebAPIService(IHttpClientService httpClientService, IConfig
.ExecuteAction();
}
public async Task Logout(Action? successAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.Logout);
string? token = await tokensService.GetRefreshToken();
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
request.Headers.Add("Authorization", $"Bearer {token}");
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.ExecuteAction();
}
public async Task GetAccountProfilePicture(long id, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Accounts.GetProfilePicture, id);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion

View File

@@ -4,7 +4,9 @@ namespace WatchIt.Website.Services.WebAPI.Accounts;
public interface IAccountsWebAPIService
{
Task Register(RegisterRequest data, Action<RegisterResponse> createdAction, Action<IDictionary<string, string[]>> badRequestAction);
Task Authenticate(AuthenticateRequest data, Action<AuthenticateResponse> successAction, Action<IDictionary<string, string[]>> badRequestAction, Action unauthorizedAction);
Task AuthenticateRefresh(Action<AuthenticateResponse> successAction, Action unauthorizedAction, Action forbiddenAction);
Task Register(RegisterRequest data, Action<RegisterResponse>? createdAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null);
Task Authenticate(AuthenticateRequest data, Action<AuthenticateResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null);
Task AuthenticateRefresh(Action<AuthenticateResponse>? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task Logout(Action? successAction = null);
Task GetAccountProfilePicture(long id, Action<AccountProfilePictureResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null);
}

View File

@@ -0,0 +1,7 @@
namespace WatchIt.Website.Services.WebAPI.Common;
public enum AuthorizationType
{
Access,
Refresh
}

View File

@@ -1,13 +1,33 @@
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.Utility.Configuration.Model;
using WatchIt.Website.Services.Utility.Tokens;
namespace WatchIt.Website.Services.WebAPI.Common;
public abstract class BaseWebAPIService(IConfigurationService configurationService)
public abstract class BaseWebAPIService
{
#region SERVICES
protected readonly IConfigurationService _configurationService;
#endregion
#region FIELDS
protected Endpoints EndpointsConfiguration => configurationService.Data.Endpoints;
protected Endpoints EndpointsConfiguration => _configurationService.Data.Endpoints;
#endregion
#region CONSTRUCTORS
protected BaseWebAPIService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
#endregion

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.Configuration\WatchIt.WebAPI.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\..\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\..\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Tokens\WatchIt.Website.Services.Utility.Tokens.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,7 +5,21 @@ namespace WatchIt.Website.Services.WebAPI.Media;
public interface IMediaWebAPIService
{
Task GetGenres(long mediaId, Action<IEnumerable<GenreResponse>> successAction, Action notFoundAction);
Task PostGenre(long mediaId, long genreId, Action successAction, Action unauthorizedAction, Action forbiddenAction, Action notFoundAction);
Task GetPhotoRandomBackground(Action<MediaPhotoResponse> successAction, Action notFoundAction);
Task GetMedia(long mediaId, Action<MediaResponse>? successAction = null, Action? notFoundAction = null);
Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null);
Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null);
Task GetMediaRating(long mediaId, Action<MediaRatingResponse>? successAction = null, Action? notFoundAction = null);
Task GetMediaRatingByUser(long mediaId, long userId, Action<short>? successAction = null, Action? notFoundAction = null);
Task PutMediaRating(long mediaId, MediaRatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null);
Task DeleteMediaRating(long mediaId, Action? successAction = null, Action? unauthorizedAction = null);
Task PostMediaView(long mediaId, Action? successAction = null, Action? notFoundAction = null);
Task GetPhotoMediaRandomBackground(long mediaId, Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null);
Task GetPhotoRandomBackground(Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null);
Task GetPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null);
Task PutPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task DeletePoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
}

View File

@@ -11,7 +11,25 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
{
#region PUBLIC METHODS
public async Task GetGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null)
#region Main
public async Task GetMedia(long mediaId, Action<MediaResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.Get, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion
#region Genres
public async Task GetMediaGenres(long mediaId, Action<IEnumerable<GenreResponse>>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetGenres, mediaId);
@@ -23,7 +41,7 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
.ExecuteAction();
}
public async Task PostGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
public async Task PostMediaGenre(long mediaId, long genreId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.PostGenre, mediaId, genreId);
@@ -37,6 +55,95 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
.ExecuteAction();
}
#endregion
#region Rating
public async Task GetMediaRating(long mediaId, Action<MediaRatingResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetMediaRating, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetMediaRatingByUser(long mediaId, long userId, Action<short>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetMediaRatingByUser, mediaId, userId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task PutMediaRating(long mediaId, MediaRatingRequest body, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.PutMediaRating, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{
Body = body
};
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task DeleteMediaRating(long mediaId, Action? successAction = null, Action? unauthorizedAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.DeleteMediaRating, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.ExecuteAction();
}
#endregion
#region View count
public async Task PostMediaView(long mediaId, Action? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.PostMediaView, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
#endregion
public async Task GetPhotoMediaRandomBackground(long mediaId, Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetPhotoMediaRandomBackground, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetPhotoRandomBackground(Action<MediaPhotoResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetPhotoRandomBackground);
@@ -45,8 +152,51 @@ public class MediaWebAPIService(IHttpClientService httpClientService, IConfigura
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task GetPoster(long mediaId, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.GetPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task PutPoster(long mediaId, MediaPosterRequest data, Action<MediaPosterResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.PutPoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url)
{
Body = data
};
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
public async Task DeletePoster(long mediaId, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Media.DeletePoster, mediaId);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
#endregion

View File

@@ -0,0 +1,14 @@
using WatchIt.Common.Model.Movies;
namespace WatchIt.Website.Services.WebAPI.Movies;
public interface IMoviesWebAPIService
{
Task GetAllMovies(MovieQueryParameters? query = null, Action<IEnumerable<MovieResponse>>? successAction = null);
Task PostMovie(MovieRequest data, Action<MovieResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task GetMovie(long id, Action<MovieResponse>? successAction = null, Action? notFoundAction = null);
Task PutMovie(long id, MovieRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task DeleteMovie(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task GetMoviesViewRank(int? first = null, int? days = null, Action<IEnumerable<MovieResponse>>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null);
}

View File

@@ -0,0 +1,152 @@
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Primitives;
using WatchIt.Common.Model.Movies;
using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.WebAPI.Common;
namespace WatchIt.Website.Services.WebAPI.Movies;
public class MoviesWebAPIService : BaseWebAPIService, IMoviesWebAPIService
{
#region SERVICES
private IHttpClientService _httpClientService;
private IConfigurationService _configurationService;
#endregion
#region CONSTRUCTORS
public MoviesWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : base(configurationService)
{
_httpClientService = httpClientService;
_configurationService = configurationService;
}
#endregion
#region PUBLIC METHODS
#region Main
public async Task GetAllMovies(MovieQueryParameters? query = null, Action<IEnumerable<MovieResponse>>? successAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.GetAllMovies);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
request.Query = query;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.ExecuteAction();
}
public async Task GetMovie(long id, Action<MovieResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.GetMovie, id);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task PostMovie(MovieRequest data, Action<MovieResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.PostMovie);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
request.Body = data;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
public async Task PutMovie(long id, MovieRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.PutMovie, id);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url);
request.Body = data;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
public async Task DeleteMovie(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.DeleteMovie, id);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
#endregion
#region View count
public async Task GetMoviesViewRank(int? first = null, int? days = null, Action<IEnumerable<MovieResponse>>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null)
{
string url = GetUrl(EndpointsConfiguration.Movies.GetMoviesViewRank);
if (first.HasValue || days.HasValue)
{
StringBuilder urlBuilder = new StringBuilder(url);
urlBuilder.Append('?');
bool firstParameter = true;
if (first.HasValue)
{
urlBuilder.Append($"first={first.Value}");
firstParameter = false;
}
if (days.HasValue)
{
if (!firstParameter)
{
urlBuilder.Append('&');
}
urlBuilder.Append($"days={days.Value}");
}
url = urlBuilder.ToString();
}
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.ExecuteAction();
}
#endregion
#endregion
#region PRIVATE METHODS
protected override string GetServiceBase() => EndpointsConfiguration.Movies.Base;
#endregion
}

View File

@@ -6,4 +6,18 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components">
<HintPath>..\..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.2\Microsoft.AspNetCore.Components.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Model\WatchIt.Common.Model.csproj" />
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Services\WatchIt.Common.Services.HttpClient\WatchIt.Common.Services.HttpClient.csproj" />
<ProjectReference Include="..\..\..\..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.Configuration\WatchIt.WebAPI.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\..\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.WebAPI.Common\WatchIt.Website.Services.WebAPI.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
using WatchIt.Common.Model.Series;
namespace WatchIt.Website.Services.WebAPI.Series;
public interface ISeriesWebAPIService
{
Task GetAllSeries(SeriesQueryParameters? query = null, Action<IEnumerable<SeriesResponse>>? successAction = null);
Task GetSeries(long id, Action<SeriesResponse>? successAction = null, Action? notFoundAction = null);
Task PostSeries(SeriesRequest data, Action<SeriesResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task PutSeries(long id, SeriesRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task DeleteSeries(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null);
Task GetSeriesViewRank(int? first = null, int? days = null, Action<IEnumerable<SeriesResponse>>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null);
}

View File

@@ -0,0 +1,150 @@
using System.Text;
using WatchIt.Common.Model.Series;
using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.WebAPI.Common;
namespace WatchIt.Website.Services.WebAPI.Series;
public class SeriesWebAPIService : BaseWebAPIService, ISeriesWebAPIService
{
#region SERVICES
private IHttpClientService _httpClientService;
private IConfigurationService _configurationService;
#endregion
#region CONSTRUCTORS
public SeriesWebAPIService(IHttpClientService httpClientService, IConfigurationService configurationService) : base(configurationService)
{
_httpClientService = httpClientService;
_configurationService = configurationService;
}
#endregion
#region PUBLIC METHODS
#region Main
public async Task GetAllSeries(SeriesQueryParameters? query = null, Action<IEnumerable<SeriesResponse>>? successAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.GetAllSeries);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
request.Query = query;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.ExecuteAction();
}
public async Task GetSeries(long id, Action<SeriesResponse>? successAction = null, Action? notFoundAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.GetSeries, id);
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor404NotFound(notFoundAction)
.ExecuteAction();
}
public async Task PostSeries(SeriesRequest data, Action<SeriesResponse>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.PostSeries);
HttpRequest request = new HttpRequest(HttpMethodType.Post, url);
request.Body = data;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
public async Task PutSeries(long id, SeriesRequest data, Action? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.PutSeries, id);
HttpRequest request = new HttpRequest(HttpMethodType.Put, url);
request.Body = data;
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
public async Task DeleteSeries(long id, Action? successAction = null, Action? unauthorizedAction = null, Action? forbiddenAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.DeleteSeries, id);
HttpRequest request = new HttpRequest(HttpMethodType.Delete, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor401Unauthorized(unauthorizedAction)
.RegisterActionFor403Forbidden(forbiddenAction)
.ExecuteAction();
}
#endregion
#region View count
public async Task GetSeriesViewRank(int? first = null, int? days = null, Action<IEnumerable<SeriesResponse>>? successAction = null, Action<IDictionary<string, string[]>>? badRequestAction = null)
{
string url = GetUrl(EndpointsConfiguration.Series.GetSeriesViewRank);
if (first.HasValue || days.HasValue)
{
StringBuilder urlBuilder = new StringBuilder(url);
urlBuilder.Append('?');
bool firstParameter = true;
if (first.HasValue)
{
urlBuilder.Append($"first={first.Value}");
firstParameter = false;
}
if (days.HasValue)
{
if (!firstParameter)
{
urlBuilder.Append('&');
}
urlBuilder.Append($"days={days.Value}");
}
url = urlBuilder.ToString();
}
HttpRequest request = new HttpRequest(HttpMethodType.Get, url);
HttpResponse response = await _httpClientService.SendRequestAsync(request);
response.RegisterActionFor2XXSuccess(successAction)
.RegisterActionFor400BadRequest(badRequestAction)
.ExecuteAction();
}
#endregion
#endregion
#region PRIVATE METHODS
protected override string GetServiceBase() => EndpointsConfiguration.Series.Base;
#endregion
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\WatchIt.Common\WatchIt.Common.Services\WatchIt.Common.Services.HttpClient\WatchIt.Common.Services.HttpClient.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services.WebAPI.Common\WatchIt.Website.Services.WebAPI.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,20 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="app.css?version=0.2"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.2"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- CSS -->
<link rel="stylesheet" href="app.css?version=0.1.0.16"/>
<link rel="stylesheet" href="WatchIt.Website.styles.css?version=0.1.0.26"/>
<!-- BOOTSTRAP -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- FONTS -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Belanosima:wght@400;600;700&family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<HeadOutlet @rendermode="InteractiveServer"/>
</head>
<body>
<Routes @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>
<Routes @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<div class="row">
<div class="col">
<div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid">
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<div class="text-danger icon-size">⚠&#xFE0E;</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<h3 class="text-danger">An error occured while loading a page</h3>
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="row">
<div class="col">
<div class="d-flex justify-content-center">
<p>@ErrorMessage</p>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace WatchIt.Website.Components;
public partial class ErrorComponent : ComponentBase
{
#region PARAMETERS
[Parameter] public string? ErrorMessage { get; set; }
#endregion
}

View File

@@ -0,0 +1,5 @@
/* CLASSES */
.icon-size {
font-size: 80px;
}

View File

@@ -0,0 +1,12 @@
<div class="row">
<div class="col">
<div class="d-flex flex-column m-5">
<div class="d-flex justify-content-center">
<div id="spinner" class="spinner-border text-dark"></div>
</div>
<div class="d-flex justify-content-center">
<p id="text" class="text-dark">Loading...</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
/* IDS */
#spinner {
width: 5rem;
height: 5rem;
}
#text {
font-size: 25px;
border-width: 0.4rem;
}

View File

@@ -1,48 +1,75 @@
@using System.Diagnostics
@using System.Text
@using WatchIt.Common.Model.Media
@using WatchIt.Website.Services.WebAPI.Media
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@if (loaded)
@if (_loaded)
{
<div class="container-xl">
<div class="row align-items-center m-2 rounded-3 header panel">
<div class="col-sm-4">
<div class="row align-items-center m-1 my-2 mb-3 rounded-3 header panel panel-header z-3">
<div class="col-2">
<a class="logo" href="/">
WatchIt
</a>
</div>
<div class="col-sm-4">
<div class="col">
<p>Menu</p>
</div>
<div class="col-sm-4">
<div class="col-auto">
<div class="d-flex flex-row-reverse">
@if (signedIn)
@if (_user is null)
{
<p>test</p>
<a class="main-button" href="/auth">Sign in</a>
}
else
{
<a class="main-button" href="/auth">Sign in or up</a>
<div class="dropdown z-3">
<a class="dropdown-toggle align-items-center text-decoration-none d-flex" id="dropdownUser" aria-expanded="false" @onclick="() => _userMenuIsActive = !_userMenuIsActive">
<img class="rounded-circle" alt="avatar" height="30" src="@(_userProfilePicture)"/>
<div class="text-decoration-none mx-2 text-white">@(_user.Username)</div>
</a>
<ul class="dropdown-menu dropdown-menu-right text-small z-3" id="user-menu" aria-labelledby="dropdownUser">
<li>
@if (_user.IsAdmin)
{
<a class="dropdown-item" href="/admin">Administrator panel</a>
}
<div class="dropdown-menu-separator"></div>
<a class="dropdown-item text-danger" @onclick="UserMenuLogOut">Log out</a>
</li>
</ul>
</div>
}
</div>
</div>
</div>
<div class="row body-content">
<div class="col-sm-12">
<div class="row">
<div class="col z-0 p-1">
@Body
</div>
</div>
</div>
<style>
html {
height: 100%;
}
body {
background: url('@background') no-repeat center center fixed;
background-image: url('@_background');
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
}
.logo, .main-button {
background-image: linear-gradient(45deg, @firstGradientColor, @secondGradientColor);
background-image: linear-gradient(45deg, @_firstGradientColor, @_secondGradientColor);
}
#user-menu {
display: @(_userMenuIsActive ? "block" : "none");
position: fixed;
}
</style>
}
@@ -55,6 +82,10 @@
#region SERVICES
[Inject] public ILogger<MainLayout> Logger { get; set; } = default!;
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public ITokensService TokensService { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
#endregion
@@ -63,12 +94,15 @@
#region FIELDS
private bool loaded = false;
private bool _loaded = false;
private string background = "assets/background_temp.jpg";
private string firstGradientColor = "#c6721c";
private string secondGradientColor = "#85200c";
private bool signedIn = false;
private string _background = "assets/background_temp.jpg";
private string _firstGradientColor = "#c6721c";
private string _secondGradientColor = "#85200c";
private User? _user = null;
private string _userProfilePicture = "assets/user_placeholder.png";
private bool _userMenuIsActive = false;
#endregion
@@ -76,7 +110,29 @@
#region METHODS
protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> bgTasks = new List<Task>();
bgTasks.Add(GetBackground());
await GetAuthenticatedUser();
if (_user is not null)
{
bgTasks.Add(GetProfilePicture());
}
await Task.WhenAll(bgTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task GetBackground()
{
Action<MediaPhotoResponse> backgroundSuccess = (data) =>
{
@@ -86,13 +142,33 @@
string secondColor = BitConverter.ToString(data.Background.SecondGradientColor)
.Replace("-", string.Empty);
background = $"data:{data.MimeType};base64,{imageBase64}";
firstGradientColor = $"#{firstColor}";
secondGradientColor = $"#{secondColor}";
_background = $"data:{data.MimeType};base64,{imageBase64}";
_firstGradientColor = $"#{firstColor}";
_secondGradientColor = $"#{secondColor}";
};
await MediaWebAPIService.GetPhotoRandomBackground(backgroundSuccess, null);
await MediaWebAPIService.GetPhotoRandomBackground(backgroundSuccess);
}
loaded = true;
private async Task GetAuthenticatedUser()
{
_user = await AuthenticationService.GetUserAsync();
}
private async Task GetProfilePicture()
{
Action<AccountProfilePictureResponse> successAction = (data) =>
{
string imageBase64 = Convert.ToBase64String(data.Image);
_userProfilePicture = $"data:{data.MimeType};base64,{imageBase64}";
};
await AccountsWebAPIService.GetAccountProfilePicture(_user.Id, successAction);
}
private async Task UserMenuLogOut()
{
await AuthenticationService.LogoutAsync();
await TokensService.RemoveAuthenticationData();
NavigationManager.Refresh(true);
}
#endregion

View File

@@ -0,0 +1,36 @@
@page "/admin"
<PageTitle>WatchIt administrator panel</PageTitle>
<div class="container-fluid">
@if (_loaded)
{
if (_authenticated)
{
<div class="row">
<div class="col rounded-3 panel panel-regular m-1">
<h2>Add new data</h2>
</div>
</div>
<div class="row">
<a class="col rounded-3 panel panel-regular m-1" href="/media/new/movies">
<p class="text-center text-decorations-none">New movie</p>
</a>
<div class="col rounded-3 panel panel-regular m-1">
<p class="text-center">New TV series</p>
</div>
<div class="col rounded-3 panel panel-regular m-1">
<p class="text-center">New TV series</p>
</div>
</div>
}
else
{
<ErrorComponent ErrorMessage="You do not have permission to view this site"/>
}
}
else
{
<LoadingComponent/>
}
</div>

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Website.Services.Utility.Authentication;
namespace WatchIt.Website.Pages;
public partial class AdminPage
{
#region SERVICE
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
#endregion
#region FIELDS
private bool _loaded = false;
private bool _authenticated = false;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
User? user = await AuthenticationService.GetUserAsync();
if (user is not null && user.IsAdmin)
{
_authenticated = true;
}
_loaded = true;
StateHasChanged();
}
}
#endregion
}

View File

@@ -0,0 +1 @@

View File

@@ -1,73 +0,0 @@
@page "/auth"
@layout EmptyLayout
<PageTitle>WatchIt - @(_authType == AuthType.SignIn ? "Sign in" : "Sign up")</PageTitle>
<div class="h-100 d-flex align-items-center justify-content-center">
<div class="d-inline-flex flex-column justify-content-center panel rounded-3">
<a class="logo" href="/">
WatchIt
</a>
@if (_authType == AuthType.SignIn)
{
}
</div>
</div>
<style>
body {
background: url('@(_background)') no-repeat center center fixed;
}
.logo {
background-image: linear-gradient(45deg, @_firstGradientColor, @_secondGradientColor);
}
</style>
@code {
#region ENUMS
private enum AuthType
{
SignIn,
SignUp
}
#endregion
#region FIELDS
private AuthType _authType = AuthType.SignIn;
private string _background = "assets/background_temp.jpg";
private string _firstGradientColor = "#c6721c";
private string _secondGradientColor = "#85200c";
#endregion
#region METHODS
protected override Task OnInitializedAsync()
{
return base.OnInitializedAsync();
}
#endregion
}

View File

@@ -1,8 +0,0 @@
body {
background-size: cover;
}
.logo {
font-size: 60px;
margin: 10px;
}

View File

@@ -0,0 +1,117 @@
@page "/auth"
@layout EmptyLayout
<PageTitle>WatchIt - @(_authType == AuthType.SignIn ? "Sign in" : "Sign up")</PageTitle>
@if (_loaded)
{
<div class="h-100 d-flex align-items-center justify-content-center">
<div class="d-inline-flex flex-column justify-content-center panel panel-header rounded-3">
<a class="logo" href="/">
WatchIt
</a>
<div>
@if (_authType == AuthType.SignIn)
{
<form method="post" @onsubmit="Login" @formname="login">
<AntiforgeryToken/>
<div>
<label>
Username or email:
<InputText @bind-Value="_loginModel!.UsernameOrEmail"/>
</label>
</div>
<div>
<label>
Password:
<InputText type="password" @bind-Value="_loginModel!.Password"/>
</label>
</div>
<div>
<label>
<InputCheckbox @bind-Value="_loginModel!.RememberMe"></InputCheckbox>
Remember me
</label>
</div>
<div>
<button type="submit">Sign in</button>
</div>
</form>
}
else
{
<form method="post" @onsubmit="Register" @formname="register">
<AntiforgeryToken/>
<div>
<label>
Username:
<InputText @bind-Value="_registerModel!.Username"/>
</label>
</div>
<div>
<label>
Email:
<InputText @bind-Value="_registerModel!.Email"/>
</label>
</div>
<div>
<label>
Password:
<InputText type="password" @bind-Value="_registerModel!.Password"/>
</label>
</div>
<div>
<label>
Confirm password:
<InputText type="password" @bind-Value="_passwordConfirmation"/>
</label>
</div>
<div>
<button type="submit">Sign up</button>
</div>
</form>
}
</div>
@if (_errors is not null)
{
<div class="text-danger">
@foreach (string error in _errors)
{
@error
<br/>
}
</div>
}
<div>
<label>
<input type="radio" checked="@(() => _authType == AuthType.SignIn)" name="auth" @onchange="@(() => _authType = AuthType.SignIn)" />
Sign in
</label>
<label>
<input type="radio" checked="@(() => _authType == AuthType.SignUp)" name="auth" @onchange="@(() => _authType = AuthType.SignUp)" />
Sign up
</label>
</div>
</div>
</div>
<style>
body {
height: 100%;
background-image: url('@_background');
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.logo {
background-image: linear-gradient(45deg, @_firstGradientColor, @_secondGradientColor);
}
</style>
}

View File

@@ -0,0 +1,140 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Accounts;
using WatchIt.Common.Model.Media;
using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
using WatchIt.Website.Services.WebAPI.Media;
namespace WatchIt.Website.Pages;
public partial class AuthPage
{
#region SERVICES
[Inject] public ILogger<AuthPage> Logger { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public ITokensService TokensService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IAccountsWebAPIService AccountsWebAPIService { get; set; } = default!;
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
#endregion
#region ENUMS
private enum AuthType
{
SignIn,
SignUp
}
#endregion
#region FIELDS
private bool _loaded = false;
private AuthType _authType = AuthType.SignIn;
private string _background = "assets/background_temp.jpg";
private string _firstGradientColor = "#c6721c";
private string _secondGradientColor = "#85200c";
private AuthenticateRequest _loginModel = new AuthenticateRequest
{
UsernameOrEmail = null,
Password = null
};
private RegisterRequest _registerModel = new RegisterRequest
{
Username = null,
Email = null,
Password = null
};
private string _passwordConfirmation;
private IEnumerable<string> _errors;
#endregion
#region METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (await AuthenticationService.GetAuthenticationStatusAsync())
{
NavigationManager.NavigateTo("/");
}
Action<MediaPhotoResponse> backgroundSuccess = (data) =>
{
string imageBase64 = Convert.ToBase64String(data.Image);
string firstColor = BitConverter.ToString(data.Background.FirstGradientColor)
.Replace("-", string.Empty);
string secondColor = BitConverter.ToString(data.Background.SecondGradientColor)
.Replace("-", string.Empty);
_background = $"data:{data.MimeType};base64,{imageBase64}";
_firstGradientColor = $"#{firstColor}";
_secondGradientColor = $"#{secondColor}";
};
await MediaWebAPIService.GetPhotoRandomBackground(backgroundSuccess);
_loaded = true;
StateHasChanged();
}
}
private async Task Login()
{
await AccountsWebAPIService.Authenticate(_loginModel, LoginSuccess, LoginBadRequest, LoginUnauthorized);
async void LoginSuccess(AuthenticateResponse data)
{
await TokensService.SaveAuthenticationData(data);
NavigationManager.NavigateTo("/");
}
void LoginBadRequest(IDictionary<string, string[]> data)
{
_errors = data.SelectMany(x => x.Value).Select(x => $"• {x}");
}
void LoginUnauthorized()
{
_errors = [ "Incorrect account data" ];
}
}
private async Task Register()
{
if (_registerModel.Password != _passwordConfirmation)
{
_errors = [ "Password fields don't match" ];
return;
}
await AccountsWebAPIService.Register(_registerModel, RegisterSuccess, RegisterBadRequest);
void RegisterSuccess(RegisterResponse data)
{
_authType = AuthType.SignIn;
}
void RegisterBadRequest(IDictionary<string, string[]> data)
{
_errors = data.SelectMany(x => x.Value).Select(x => $"• {x}");
}
}
#endregion
}

View File

@@ -0,0 +1,14 @@
/* TAGS */
html {
height: 100%;
}
/* CLASSES */
.logo {
font-size: 60px;
margin: 10px;
}

View File

@@ -1,50 +0,0 @@
@page "/"
<PageTitle>WatchIt</PageTitle>
<div class="container-fluid">
<div class="row">
<div class="col-sm-12">
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
<h1>Hello, world!</h1>
Welcome to your new app.
</div>
</div>
</div>

View File

@@ -0,0 +1,97 @@
@page "/"
@using WatchIt.Common.Model.Movies
<PageTitle>WatchIt</PageTitle>
<div class="container-fluid">
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
<div class="row">
<div class="col">
<div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0">
<div class="row">
<div class="col">
<h4><strong>Top 5 movies this week by popularity</strong></h4>
</div>
</div>
<div class="row mt-3">
@for (int i = 0; i < 5; i++)
{
<div class="col">
@if (_topMovies.Count > i)
{
<a class="text-reset text-decoration-none" href="/media/@_topMovies.ToArray()[i].Key.Id">
<div class="d-flex flex-column align-items-center gap-2 h-100">
<img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topMovies.ToArray()[i].Value is not null ? _topMovies.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/>
<div class="container-fluid p-0">
<div class="row">
<div class="col-auto">
<div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div>
</div>
<div class="col">
<div class="text-end ms-auto">@_topMovies.ToArray()[i].Key.Title@(_topMovies.ToArray()[i].Key.ReleaseDate.HasValue ? $" ({_topMovies.ToArray()[i].Key.ReleaseDate.Value.Year})" : string.Empty)</div>
</div>
</div>
</div>
</div>
</a>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col">
<div class="rounded-3 panel panel-regular p-4">
<div class="container-fluid p-0">
<div class="row">
<div class="col">
<h4><strong>Top 5 TV series this week by popularity</strong></h4>
</div>
</div>
<div class="row mt-3">
@for (int i = 0; i < 5; i++)
{
<div class="col">
@if (_topSeries.Count > i)
{
<a class="text-reset text-decoration-none" href="/media/@_topSeries.ToArray()[i].Key.Id">
<div class="d-flex flex-column align-items-center gap-2 h-100">
<img class="rounded-2 shadow object-fit-cover poster-aspect-ratio" src="@(_topSeries.ToArray()[i].Value is not null ? _topSeries.ToArray()[i].Value.ToString() : "assets/poster.png")" alt="poster" width="100%"/>
<div class="container-fluid p-0">
<div class="row">
<div class="col-auto">
<div class="text-center border border-2 border-light rounded-circle place-circle"><strong>@(i + 1)</strong></div>
</div>
<div class="col">
<div class="text-end ms-auto">@_topSeries.ToArray()[i].Key.Title@(_topSeries.ToArray()[i].Key.ReleaseDate.HasValue ? $" ({_topSeries.ToArray()[i].Key.ReleaseDate.Value.Year})" : string.Empty)</div>
</div>
</div>
</div>
</div>
</a>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
}
else
{
<ErrorComponent ErrorMessage="@_error"/>
}
}
else
{
<LoadingComponent/>
}
</div>

View File

@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Series;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website.Pages;
public partial class HomePage
{
#region SERVICES
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] public ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
#endregion
#region FIELDS
private bool _loaded;
private string? _error;
private IDictionary<MovieResponse, MediaPosterResponse?> _topMovies = new Dictionary<MovieResponse, MediaPosterResponse?>();
private IDictionary<SeriesResponse, MediaPosterResponse?> _topSeries = new Dictionary<SeriesResponse, MediaPosterResponse?>();
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> step1Tasks = new List<Task>();
List<Task> endTasks = new List<Task>();
// STEP 0
step1Tasks.AddRange(
[
MoviesWebAPIService.GetMoviesViewRank(successAction: data => _topMovies = data.ToDictionary(x => x, _ => default(MediaPosterResponse?))),
SeriesWebAPIService.GetSeriesViewRank(successAction: data => _topSeries = data.ToDictionary(x => x, _ => default(MediaPosterResponse?))),
]);
// STEP 1
await Task.WhenAll(step1Tasks);
endTasks.AddRange(
[
Parallel.ForEachAsync(_topMovies, async (x, _) => await MediaWebAPIService.GetPoster(x.Key.Id, y => _topMovies[x.Key] = y)),
Parallel.ForEachAsync(_topSeries, async (x, _) => await MediaWebAPIService.GetPoster(x.Key.Id, y => _topSeries[x.Key] = y))
]);
// END
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
#endregion
}

View File

@@ -0,0 +1,16 @@
/* CLASSES */
.poster-aspect-ratio {
aspect-ratio: 3/5;
}
.border-2 {
border-width: 2px;
}
.place-circle {
width: 30px;
height: 30px;
vertical-align: middle;
line-height: 25px;
}

View File

@@ -0,0 +1,230 @@
@using WatchIt.Common.Model.Movies
@using WatchIt.Common.Model.Series
@page "/media/{id:long}/edit"
@page "/media/new/{type}"
<PageTitle>
WatchIt -
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error) && _user?.IsAdmin == true)
{
if (_media is not null)
{
@($"Edit \"")@(_media.Title)@("\"")
}
else
{
if (_movieRequest is null)
{
@("New TV series")
}
else
{
@("New movie")
}
}
}
else
{
@("Error")
}
}
else
{
@("Loading")
}
</PageTitle>
<div class="container-fluid">
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
if (_user?.IsAdmin == true)
{
<div class="row">
<div class="col">
<div class="rounded-3 panel panel-regular p-2">
<h2 class="m-0 mx-2 mb-1 p-0">@(_media is not null ? "Edit" : "Create new") @(_movieRequest is not null ? "movie" : "series")@(_media is not null ? $" \"{_media.Title}\"" : string.Empty)</h2>
</div>
</div>
</div>
<div class="row mt-3 gx-3">
<div class="col-auto">
<div class="rounded-3 panel panel-regular p-4 h-100">
<div class="container-fluid p-0">
<div class="row">
<div class="col">
<img class="rounded-2 shadow object-fit-cover" src="@(_mediaPosterRequest is not null ? _mediaPosterRequest.ToString() : "assets/poster.png")" alt="poster" width="300" height="500"/>
</div>
</div>
<div class="row mt-4">
<div class="col">
<InputFile class="form-control" OnChange="LoadPoster" disabled="@(_media is null)" autocomplete="off" style="width: 300px;"/>
</div>
</div>
@if (_mediaPosterChanged || _mediaPosterSaved is not null)
{
<div class="row mt-4 gx-1" style="width: 300px;">
@if (_mediaPosterChanged)
{
<div class="col">
<button type="button" class="btn btn-secondary btn-block btn-stretch-x" @onclick="SavePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off">
@if (!_mediaPosterSaving)
{
<span class="sr-only">Save poster</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span>
}
</button>
</div>
<div class="col">
<button type="button" class="btn btn-danger btn-block btn-stretch-x" @onclick="CancelPoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off">Drop changes</button>
</div>
}
else if (_mediaPosterSaved is not null)
{
<div class="col">
<button type="button" class="btn btn-danger btn-block btn-stretch-x" @onclick="DeletePoster" disabled=@(!Id.HasValue || _mediaPosterSaving || _mediaPosterDeleting) autocomplete="off">
@if (!_mediaPosterSaving)
{
<span class="sr-only">Delete poster</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Deleting...</span>
}
</button>
</div>
}
</div>
}
</div>
</div>
</div>
<div class="col">
<div class="rounded-3 panel panel-regular p-4 h-100">
<EditForm Model="_mediaRequest">
<AntiforgeryToken/>
<div class="container-fluid p-0">
<div class="row form-group mb-1">
<label for="title" class="col-2 col-form-label">Title*</label>
<div class="col-10">
<InputText id="title" class="form-control" @bind-Value="_mediaRequest!.Title"/>
</div>
</div>
<div class="row form-group my-1">
<label for="og-title" class="col-2 col-form-label">Original title</label>
<div class="col-10">
<InputText id="og-title" class="form-control" @bind-Value="_mediaRequest!.OriginalTitle"/>
</div>
</div>
<div class="row form-group my-1">
<label for="desc" class="col-2 col-form-label">Description</label>
<div class="col-10">
<InputTextArea id="desc" class="form-control" @bind-Value="_mediaRequest!.Description"/>
</div>
</div>
<div class="row form-group my-1">
<label for="rel-date" class="col-2 col-form-label">Release date</label>
<div class="col-4">
<InputDate TValue="DateOnly?" id="rel-date" class="form-control" @bind-Value="_mediaRequest!.ReleaseDate"/>
</div>
<label for="length" class="col-2 col-form-label">Length</label>
<div class="col-4">
<InputNumber TValue="short?" id="length" class="form-control" @bind-Value="_mediaRequest!.Length"/>
</div>
</div>
@if (_mediaRequest is MovieRequest)
{
<div class="row form-group mt-1">
<label for="budget" class="col-2 col-form-label">Budget</label>
<div class="col-10">
<InputNumber TValue="decimal?" id="budget" class="form-control" @bind-Value="((MovieRequest)_mediaRequest)!.Budget"/>
</div>
</div>
}
else
{
<div class="row form-group mt-1">
<label class="col-2 col-form-label">Has ended</label>
<div class="col-10 col-form-label">
<div class="d-flex gap-3">
<InputRadioGroup TValue="bool" @bind-Value="((SeriesRequest)_mediaRequest)!.HasEnded">
<div class="d-flex gap-2">
<InputRadio TValue="bool" Value="true"/>
Yes
</div>
<div class="d-flex gap-2">
<InputRadio TValue="bool" Value="false"/>
No
</div>
</InputRadioGroup>
</div>
</div>
</div>
}
<div class="row mt-4">
<div class="col">
<div class="d-flex justify-content-end align-items-center gap-3">
@if (!string.IsNullOrWhiteSpace(_basicDataError))
{
<div class="text-danger">@_basicDataError</div>
}
<button type="button" class="btn btn-secondary" @onclick="SaveBasicData">
@if (!_basicDataSaving)
{
<span class="sr-only">Save</span>
}
else
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="sr-only">Saving...</span>
}
</button>
</div>
</div>
</div>
</div>
</EditForm>
</div>
</div>
</div>
}
else
{
<ErrorComponent ErrorMessage="You do not have permission to view this site"/>
}
}
else
{
<ErrorComponent ErrorMessage="@_error"/>
}
}
else
{
<LoadingComponent/>
}
</div>
@if (_background is not null)
{
<style>
body {
background-image: url('@($"data:{_background.MimeType};base64,{Convert.ToBase64String(_background.Image)}")') !important;
}
.logo, .main-button {
background-image: linear-gradient(45deg, @($"#{BitConverter.ToString(_background.Background.FirstGradientColor).Replace("-", string.Empty)}"), @($"#{BitConverter.ToString(_background.Background.SecondGradientColor).Replace("-", string.Empty)}")) !important;
}
</style>
}

View File

@@ -0,0 +1,254 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Series;
using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website.Pages;
public partial class MediaEditPage : ComponentBase
{
#region SERVICES
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] public ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public long? Id { get; set; }
[Parameter] public string? Type { get; set; }
#endregion
#region FIELDS
private bool _loaded = false;
private string? _error;
private User? _user;
private MediaPhotoResponse? _background;
private MediaResponse? _media;
private MovieRequest? _movieRequest;
private SeriesRequest? _seriesRequest;
private Media? _mediaRequest => _movieRequest is not null ? _movieRequest : _seriesRequest;
private bool _basicDataSaving;
private string? _basicDataError;
private MediaPosterResponse? _mediaPosterSaved;
private MediaPosterRequest? _mediaPosterRequest;
private bool _mediaPosterChanged;
private bool _mediaPosterSaving;
private bool _mediaPosterDeleting;
#endregion
#region PRIVATE METHODS
#region Main
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> step1Tasks = new List<Task>();
List<Task> step2Tasks = new List<Task>();
List<Task> endTasks = new List<Task>();
// STEP 0
step1Tasks.AddRange(
[
Task.Run(async () => _user = await AuthenticationService.GetUserAsync())
]);
// STEP 1
await Task.WhenAll(step1Tasks);
if (_user is not null && _user.IsAdmin)
{
step2Tasks.AddRange(
[
InitializeMedia()
]);
}
// STEP 2
await Task.WhenAll(step2Tasks);
if (_user is not null && _user.IsAdmin && _media is not null)
{
endTasks.AddRange(
[
MediaWebAPIService.GetPhotoMediaRandomBackground(Id.Value, data => _background = data),
MediaWebAPIService.GetPoster(Id.Value, data =>
{
_mediaPosterSaved = data;
_mediaPosterRequest = new MediaPosterRequest(data);
})
]);
}
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task InitializeMedia()
{
if (Id.HasValue)
{
await MediaWebAPIService.GetMedia(Id.Value, data => _media = data, () => NavigationManager.NavigateTo("/media/new/movie"));
if (_media.Type == MediaType.Movie)
{
await MoviesWebAPIService.GetMovie(Id.Value, data => _movieRequest = new MovieRequest(data));
}
else
{
await SeriesWebAPIService.GetSeries(Id.Value, data => _seriesRequest = new SeriesRequest(data));
}
}
else
{
if (!string.IsNullOrWhiteSpace(Type) && Type == "series")
{
_seriesRequest = new SeriesRequest
{
Title = string.Empty
};
}
else
{
_movieRequest = new MovieRequest
{
Title = string.Empty
};
}
}
}
#endregion
#region Poster
private async Task LoadPoster(InputFileChangeEventArgs args)
{
if (args.File.ContentType.StartsWith("image"))
{
Stream stream = args.File.OpenReadStream(5242880);
byte[] array;
using (MemoryStream ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
array = ms.ToArray();
}
_mediaPosterRequest = new MediaPosterRequest()
{
Image = array,
MimeType = args.File.ContentType
};
_mediaPosterChanged = true;
}
}
private async Task SavePoster()
{
void Success(MediaPosterResponse data)
{
_mediaPosterSaved = data;
_mediaPosterRequest = new MediaPosterRequest(data);
_mediaPosterChanged = false;
_mediaPosterSaving = false;
}
_mediaPosterSaving = true;
await MediaWebAPIService.PutPoster(Id.Value, _mediaPosterRequest, Success);
}
private void CancelPoster()
{
_mediaPosterRequest = _mediaPosterSaved is not null ? new MediaPosterRequest(_mediaPosterSaved) : null;
_mediaPosterChanged = false;
}
private async Task DeletePoster()
{
void Success()
{
_mediaPosterSaved = null;
_mediaPosterRequest = null;
_mediaPosterChanged = false;
_mediaPosterDeleting = false;
}
_mediaPosterDeleting = true;
await MediaWebAPIService.DeletePoster(Id.Value, Success);
}
#endregion
#region Basic data
private async Task SaveBasicData()
{
void SuccessPost(long id)
{
_basicDataSaving = false;
NavigationManager.NavigateTo($"/media/{id}/edit", true);
}
void BadRequest(IDictionary<string, string[]> errors)
{
_basicDataError = errors.SelectMany(x => x.Value).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(_basicDataError))
{
_basicDataSaving = false;
}
}
_basicDataSaving = true;
if (_media is null)
{
if (_movieRequest is not null)
{
await MoviesWebAPIService.PostMovie(_movieRequest, data => SuccessPost(data.Id), BadRequest);
}
else
{
await SeriesWebAPIService.PostSeries(_seriesRequest, data => SuccessPost(data.Id), BadRequest);
}
}
else
{
if (_movieRequest is not null)
{
await MoviesWebAPIService.PutMovie(Id.Value, _movieRequest, () => _basicDataSaving = false, BadRequest);
}
else
{
await SeriesWebAPIService.PutSeries(Id.Value, _seriesRequest, () => _basicDataSaving = false, BadRequest);
}
}
}
#endregion
#endregion
}

View File

@@ -0,0 +1,218 @@
@using System.Text
@using Microsoft.IdentityModel.Tokens
@using WatchIt.Common.Model.Genres
@page "/media/{id:long}"
@layout MainLayout
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
<PageTitle>@_media.Title@(_media.ReleaseDate is not null ? $" ({_media.ReleaseDate.Value.Year})" : string.Empty) - WatchIt</PageTitle>
}
else
{
<PageTitle>Error - WatchIt</PageTitle>
}
}
else
{
<PageTitle>Loading... - WatchIt</PageTitle>
}
<div class="container-fluid">
@if (_loaded)
{
if (string.IsNullOrWhiteSpace(_error))
{
<div class="row mt-9">
<div class="col-auto">
<img class="rounded-2 shadow object-fit-cover" src="@(_poster is not null ? _poster.ToString() : "assets/poster.png")" alt="poster" width="200" height="333"/>
</div>
<div class="col">
<div class="d-flex h-100">
<div class="container-fluid px-0 align-self-end">
<div class="row">
<div class="col">
<h1 class="align-self-end title-shadow">
<strong>@_media.Title</strong>
</h1>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_media.Description))
{
<div class="row">
<div class="col">
<div class="description-shadow">
@_media.Description
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="row mt-3 gx-3">
<div class="col">
<div class="rounded-3 panel panel-regular p-4 h-100">
<div class="container-fluid px-0">
<div class="row">
<div class="col">
<div class="d-flex flex-wrap gap-3">
<div class="metadata-pill">
<strong>@(_media.Type == MediaType.Movie ? "Movie" : "TV Series")</strong>
</div>
@if (!string.IsNullOrWhiteSpace(_media.OriginalTitle))
{
<div class="metadata-pill">
<strong>Original title:</strong> @_media.OriginalTitle
</div>
}
@if (_media.ReleaseDate is not null)
{
<div class="metadata-pill">
<strong>Release date:</strong> @_media.ReleaseDate.ToString()
</div>
}
@if (_media.Length is not null)
{
<div class="metadata-pill">
<strong>Length:</strong> @TimeSpan.FromMinutes(_media.Length.Value).ToString(@"hh\:mm")
</div>
}
@if (_movie?.Budget is not null)
{
<div class="metadata-pill">
<strong>Budget:</strong> @(Math.Round(_movie.Budget.Value))$
</div>
}
@if (_series?.HasEnded == true)
{
<div class="metadata-pill">
Ended
</div>
}
</div>
</div>
</div>
<div class="row mt-4">
<div class="col">
<h4><strong>Genres:</strong></h4>
</div>
</div>
<div class="row">
<div class="col">
<div class="d-flex flex-wrap gap-3">
@if (_genres.IsNullOrEmpty())
{
<div>
No genres assigned.
</div>
}
else
{
foreach (GenreResponse genre in _genres)
{
<a class="text-decoration-none text-light" href="/genre/@genre.Id">
<div class="metadata-pill">
@genre.Name
</div>
</a>
}
}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-auto">
<div class="rounded-3 panel panel-yellow p-4 h-100">
<div class="container-fluid px-0">
<div class="row">
<div class="col">
<h4 class="text-dark">
<strong>Global rating:</strong> @(_globalRating.RatingCount == 0 ? "no ratings" : $"{Math.Round(_globalRating.RatingAverage, 1)}/10")
</h4>
</div>
</div>
<div class="row">
<div class="col">
<hr class="rating-separator"/>
</div>
</div>
<div class="row">
<div class="col">
<h4 class="text-dark">
<strong>Your rating:</strong>
</h4>
</div>
</div>
<div class="row">
<div class="col">
@if (_user is not null)
{
<div class="d-flex rating">
<input id="r1" type="radio" name="rate" checked="@(_userRating == 1 )" onclick="@(async () => await AddRating(1 ))"/>
<label class="text-dark" for="r1">@(_userRating >= 1 ? "\u2605" : "\u2606")</label>
<input id="r2" type="radio" name="rate" checked="@(_userRating == 2 )" onclick="@(async () => await AddRating(2 ))"/>
<label class="text-dark" for="r2">@(_userRating >= 2 ? "\u2605" : "\u2606")</label>
<input id="r3" type="radio" name="rate" checked="@(_userRating == 3 )" onclick="@(async () => await AddRating(3 ))"/>
<label class="text-dark" for="r3">@(_userRating >= 3 ? "\u2605" : "\u2606")</label>
<input id="r4" type="radio" name="rate" checked="@(_userRating == 4 )" onclick="@(async () => await AddRating(4 ))"/>
<label class="text-dark" for="r4">@(_userRating >= 4 ? "\u2605" : "\u2606")</label>
<input id="r5" type="radio" name="rate" checked="@(_userRating == 5 )" onclick="@(async () => await AddRating(5 ))"/>
<label class="text-dark" for="r5">@(_userRating >= 5 ? "\u2605" : "\u2606")</label>
<input id="r6" type="radio" name="rate" checked="@(_userRating == 6 )" onclick="@(async () => await AddRating(6 ))"/>
<label class="text-dark" for="r6">@(_userRating >= 6 ? "\u2605" : "\u2606")</label>
<input id="r7" type="radio" name="rate" checked="@(_userRating == 7 )" onclick="@(async () => await AddRating(7 ))"/>
<label class="text-dark" for="r7">@(_userRating >= 7 ? "\u2605" : "\u2606")</label>
<input id="r8" type="radio" name="rate" checked="@(_userRating == 8 )" onclick="@(async () => await AddRating(8 ))"/>
<label class="text-dark" for="r8">@(_userRating >= 8 ? "\u2605" : "\u2606")</label>
<input id="r9" type="radio" name="rate" checked="@(_userRating == 9 )" onclick="@(async () => await AddRating(9 ))"/>
<label class="text-dark" for="r9">@(_userRating >= 9 ? "\u2605" : "\u2606")</label>
<input id="r10" type="radio" name="rate" checked="@(_userRating == 10)" onclick="@(async () => await AddRating(10))"/>
<label class="text-dark" for="r10">@(_userRating == 10 ? "\u2605" : "\u2606")</label>
</div>
}
else
{
<p class="text-dark">You must be logged in to add a rating</p>
}
</div>
</div>
</div>
</div>
</div>
</div>
}
else
{
<ErrorComponent ErrorMessage="@_error"/>
}
}
else
{
<LoadingComponent/>
}
</div>
@if (_background is not null)
{
<style>
body {
background-image: url('@($"data:{_background.MimeType};base64,{Convert.ToBase64String(_background.Image)}")') !important;
}
.logo, .main-button {
background-image: linear-gradient(45deg, @($"#{BitConverter.ToString(_background.Background.FirstGradientColor).Replace("-", string.Empty)}"), @($"#{BitConverter.ToString(_background.Background.SecondGradientColor).Replace("-", string.Empty)}")) !important;
}
</style>
}

View File

@@ -0,0 +1,128 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using WatchIt.Common.Model.Genres;
using WatchIt.Common.Model.Media;
using WatchIt.Common.Model.Movies;
using WatchIt.Common.Model.Series;
using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website.Pages;
public partial class MediaPage : ComponentBase
{
#region SERVICES
[Inject] public NavigationManager NavigationManager { get; set; } = default!;
[Inject] public IAuthenticationService AuthenticationService { get; set; } = default!;
[Inject] public IMediaWebAPIService MediaWebAPIService { get; set; } = default!;
[Inject] public IMoviesWebAPIService MoviesWebAPIService { get; set; } = default!;
[Inject] public ISeriesWebAPIService SeriesWebAPIService { get; set; } = default!;
#endregion
#region PARAMETERS
[Parameter] public long Id { get; set; }
#endregion
#region FIELDS
private bool _loaded;
private string? _error;
private MediaResponse? _media;
private User? _user;
private MediaPhotoResponse? _background;
private MediaPosterResponse? _poster;
private IEnumerable<GenreResponse> _genres;
private MediaRatingResponse _globalRating;
private MovieResponse? _movie;
private SeriesResponse? _series;
private short? _userRating;
#endregion
#region PRIVATE METHODS
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
List<Task> step1Tasks = new List<Task>();
List<Task> step2Tasks = new List<Task>();
List<Task> endTasks = new List<Task>();
// STEP 0
step1Tasks.AddRange(
[
MediaWebAPIService.GetMedia(Id, data => _media = data, () => _error = $"Media with id {Id} was not found")
]);
// STEP 1
await Task.WhenAll(step1Tasks);
if (_error is null)
{
step2Tasks.AddRange(
[
Task.Run(async () => _user = await AuthenticationService.GetUserAsync())
]);
endTasks.AddRange(
[
MediaWebAPIService.PostMediaView(Id),
MediaWebAPIService.GetPhotoMediaRandomBackground(Id, data => _background = data),
MediaWebAPIService.GetPoster(Id, data => _poster = data),
MediaWebAPIService.GetMediaGenres(Id, data => _genres = data),
MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data),
_media.Type == MediaType.Movie ? MoviesWebAPIService.GetMovie(Id, data => _movie = data) : SeriesWebAPIService.GetSeries(Id, data => _series = data),
]);
}
// STEP 2
await Task.WhenAll(step2Tasks);
if (_error is null && _user is not null)
{
endTasks.AddRange(
[
MediaWebAPIService.GetMediaRatingByUser(Id, _user.Id, data => _userRating = data)
]);
}
// END
await Task.WhenAll(endTasks);
_loaded = true;
StateHasChanged();
}
}
private async Task AddRating(short rating)
{
if (_userRating == rating)
{
await MediaWebAPIService.DeleteMediaRating(Id);
_userRating = null;
}
else
{
await MediaWebAPIService.PutMediaRating(Id, new MediaRatingRequest(rating));
_userRating = rating;
}
await MediaWebAPIService.GetMediaRating(Id, data => _globalRating = data);
}
#endregion
}

View File

@@ -0,0 +1,38 @@
/* CLASSES */
.metadata-pill {
border-color: gray;
border-width: 2px;
border-radius: 30px;
border-style: solid;
padding: 2px 10px;
}
.title-shadow {
text-shadow: 2px 2px 2px #000;
}
.description-shadow {
text-shadow: 1px 1px 1px #000;
}
.rating-separator {
border-color: black;
border-width: 1px;
margin: 10px 0;
}
.rating > input {
display: none;
}
.rating > label {
font-size: 30px;
}
.rating > label:hover, .rating > input:hover+label {
color: gray !important;
cursor: pointer;
}

View File

@@ -1,7 +1,14 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Components.Authorization;
using WatchIt.Common.Services.HttpClient;
using WatchIt.Website.Services.Utility.Authentication;
using WatchIt.Website.Services.Utility.Configuration;
using WatchIt.Website.Services.Utility.Tokens;
using WatchIt.Website.Services.WebAPI.Accounts;
using WatchIt.Website.Services.WebAPI.Media;
using WatchIt.Website.Services.WebAPI.Movies;
using WatchIt.Website.Services.WebAPI.Series;
namespace WatchIt.Website;
@@ -13,6 +20,7 @@ public static class Program
{
WebApplication app = WebApplication.CreateBuilder(args)
.SetupServices()
.SetupAuthentication()
.SetupApplication()
.Build();
@@ -43,15 +51,27 @@ public static class Program
private static WebApplicationBuilder SetupServices(this WebApplicationBuilder builder)
{
builder.Services.AddHttpClient();
builder.Services.AddSingleton<HttpClient>();
// Utility
builder.Services.AddSingleton<IHttpClientService, HttpClientService>();
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
builder.Services.AddScoped<ITokensService, TokensService>();
builder.Services.AddScoped<IAuthenticationService, AuthenticationService>();
// WebAPI
builder.Services.AddSingleton<IAccountsWebAPIService, AccountsWebAPIService>();
builder.Services.AddScoped<IAccountsWebAPIService, AccountsWebAPIService>();
builder.Services.AddSingleton<IMediaWebAPIService, MediaWebAPIService>();
builder.Services.AddSingleton<IMoviesWebAPIService, MoviesWebAPIService>();
builder.Services.AddSingleton<ISeriesWebAPIService, SeriesWebAPIService>();
return builder;
}
private static WebApplicationBuilder SetupAuthentication(this WebApplicationBuilder builder)
{
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, JWTAuthenticationStateProvider>();
return builder;
}

View File

@@ -15,9 +15,14 @@
<ItemGroup>
<ProjectReference Include="..\..\WatchIt.Common\WatchIt.Common.Services\WatchIt.Common.Services.HttpClient\WatchIt.Common.Services.HttpClient.csproj" />
<ProjectReference Include="..\..\WatchIt.WebAPI\WatchIt.WebAPI.Services\WatchIt.WebAPI.Services.Utility\WatchIt.WebAPI.Services.Utility.Tokens\WatchIt.WebAPI.Services.Utility.Tokens.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Authentication\WatchIt.Website.Services.Utility.Authentication.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Configuration\WatchIt.Website.Services.Utility.Configuration.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.Utility\WatchIt.Website.Services.Utility.Tokens\WatchIt.Website.Services.Utility.Tokens.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Accounts\WatchIt.Website.Services.WebAPI.Accounts.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Media\WatchIt.Website.Services.WebAPI.Media.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Movies\WatchIt.Website.Services.WebAPI.Movies.csproj" />
<ProjectReference Include="..\WatchIt.Website.Services\WatchIt.Website.Services.WebAPI\WatchIt.Website.Services.WebAPI.Series\WatchIt.Website.Services.WebAPI.Series.csproj" />
</ItemGroup>
<ItemGroup>
@@ -27,16 +32,12 @@
<_ContentIncludedByDefault Remove="Components\Pages\Error.razor" />
<_ContentIncludedByDefault Remove="Components\Pages\Home.razor" />
<_ContentIncludedByDefault Remove="Components\Pages\Weather.razor" />
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\bootstrap\bootstrap.min.css.map" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Layout\MainLayout.razor" />
<AdditionalFiles Include="Pages\Home.razor" />
</ItemGroup>
<ItemGroup>
<Folder Include="Components\" />
<Folder Include="wwwroot\assets\" />
</ItemGroup>
</Project>

View File

@@ -8,3 +8,10 @@
@using Microsoft.JSInterop
@using WatchIt.Website
@using WatchIt.Website.Layout
@using WatchIt.Website.Components
@using WatchIt.Common.Model.Accounts
@using WatchIt.Common.Model.Media
@using WatchIt.Website.Services.Utility.Tokens
@using WatchIt.Website.Services.Utility.Authentication
@using WatchIt.Website.Services.WebAPI.Accounts
@using WatchIt.Website.Services.WebAPI.Media

Some files were not shown because too many files have changed in this diff Show More