2024-04-27 22:36:16 +02:00
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using SimpleToolkit.Extensions;
|
|
|
|
|
|
using WatchIt.Common.Model.Accounts;
|
2024-11-01 01:03:00 +01:00
|
|
|
|
using WatchIt.Common.Model.Media;
|
|
|
|
|
|
using WatchIt.Common.Model.Movies;
|
2024-11-01 20:44:01 +01:00
|
|
|
|
using WatchIt.Common.Model.Persons;
|
2024-11-05 20:04:15 +01:00
|
|
|
|
using WatchIt.Common.Model.Photos;
|
2024-11-01 01:03:00 +01:00
|
|
|
|
using WatchIt.Common.Model.Series;
|
2024-04-27 22:36:16 +02:00
|
|
|
|
using WatchIt.Database;
|
|
|
|
|
|
using WatchIt.Database.Model.Account;
|
2024-11-01 01:03:00 +01:00
|
|
|
|
using WatchIt.Database.Model.Media;
|
|
|
|
|
|
using WatchIt.Database.Model.Rating;
|
2024-04-27 22:36:16 +02:00
|
|
|
|
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;
|
2024-10-26 02:23:33 +02:00
|
|
|
|
using Account = WatchIt.Database.Model.Account.Account;
|
2024-07-30 16:19:51 +02:00
|
|
|
|
using AccountProfilePicture = WatchIt.Common.Model.Accounts.AccountProfilePicture;
|
2024-11-01 20:44:01 +01:00
|
|
|
|
using Person = WatchIt.Database.Model.Person.Person;
|
2024-04-27 22:36:16 +02:00
|
|
|
|
|
|
|
|
|
|
namespace WatchIt.WebAPI.Services.Controllers.Accounts;
|
|
|
|
|
|
|
|
|
|
|
|
public class AccountsControllerService(
|
|
|
|
|
|
ILogger<AccountsControllerService> logger,
|
|
|
|
|
|
DatabaseContext database,
|
|
|
|
|
|
ITokensService tokensService,
|
|
|
|
|
|
IUserService userService
|
|
|
|
|
|
) : IAccountsControllerService
|
|
|
|
|
|
{
|
|
|
|
|
|
#region PUBLIC METHODS
|
|
|
|
|
|
|
2024-11-05 20:04:15 +01:00
|
|
|
|
#region Basic
|
|
|
|
|
|
|
2024-04-27 22:36:16 +02:00
|
|
|
|
public async Task<RequestResult> Register(RegisterRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = new Account
|
|
|
|
|
|
{
|
|
|
|
|
|
Username = data.Username,
|
|
|
|
|
|
Email = data.Email,
|
|
|
|
|
|
};
|
2024-11-06 14:56:02 +01:00
|
|
|
|
|
|
|
|
|
|
SetPassword(account, data.Password);
|
2024-04-27 22:36:16 +02:00
|
|
|
|
await database.Accounts.AddAsync(account);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation($"New account with ID {account.Id} was created (username: {account.Username}; email: {account.Email})");
|
|
|
|
|
|
return RequestResult.Created($"accounts/{account.Id}", new RegisterResponse(account));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> Authenticate(AuthenticateRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => string.Equals(x.Email, data.UsernameOrEmail) || string.Equals(x.Username, data.UsernameOrEmail));
|
|
|
|
|
|
if (account is null || !ComputeHash(data.Password, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password))
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Task<string> refreshTokenTask = tokensService.CreateRefreshTokenAsync(account, true);
|
|
|
|
|
|
Task<string> accessTokenTask = tokensService.CreateAccessTokenAsync(account);
|
|
|
|
|
|
AuthenticateResponse response = new AuthenticateResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
AccessToken = await accessTokenTask,
|
|
|
|
|
|
RefreshToken = await refreshTokenTask,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-10-30 23:28:47 +01:00
|
|
|
|
account.LastActive = DateTime.UtcNow;
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
2024-04-27 22:36:16 +02:00
|
|
|
|
logger.LogInformation($"Account with ID {account.Id} was authenticated");
|
|
|
|
|
|
return RequestResult.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> AuthenticateRefresh()
|
|
|
|
|
|
{
|
|
|
|
|
|
Guid jti = userService.GetJti();
|
|
|
|
|
|
AccountRefreshToken? token = await database.AccountRefreshTokens.FirstOrDefaultAsync(x => x.Id == jti);
|
|
|
|
|
|
if (token is null || token.ExpirationDate < DateTime.UtcNow)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-30 16:19:51 +02:00
|
|
|
|
string refreshToken;
|
2024-04-27 22:36:16 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2024-07-30 16:19:51 +02:00
|
|
|
|
refreshToken = await tokensService.ExtendRefreshTokenAsync(token.Account, token.Id);
|
2024-04-27 22:36:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (TokenNotFoundException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (TokenNotExtendableException)
|
|
|
|
|
|
{
|
2024-07-30 16:19:51 +02:00
|
|
|
|
refreshToken = userService.GetRawToken().Replace("Bearer ", string.Empty);
|
2024-04-27 22:36:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-30 16:19:51 +02:00
|
|
|
|
string accessToken = await tokensService.CreateAccessTokenAsync(token.Account);
|
|
|
|
|
|
|
2024-10-30 23:28:47 +01:00
|
|
|
|
token.Account.LastActive = DateTime.UtcNow;
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
2024-04-27 22:36:16 +02:00
|
|
|
|
logger.LogInformation($"Account with ID {token.AccountId} was authenticated by token refreshing");
|
2024-07-30 16:19:51 +02:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-05 20:04:15 +01:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Profile picture
|
|
|
|
|
|
|
2024-07-30 16:19:51 +02:00
|
|
|
|
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);
|
2024-04-27 22:36:16 +02:00
|
|
|
|
}
|
2024-11-03 23:01:34 +01:00
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> PutAccountProfilePicture(AccountProfilePictureRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
Database.Model.Account.AccountProfilePicture? picture = account.ProfilePicture;
|
|
|
|
|
|
|
|
|
|
|
|
if (picture is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
picture = data.CreateMediaPosterImage();
|
|
|
|
|
|
await database.AccountProfilePictures.AddAsync(picture);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
account.ProfilePictureId = picture.Id;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
data.UpdateMediaPosterImage(picture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
AccountProfilePictureResponse returnData = new AccountProfilePictureResponse(picture);
|
|
|
|
|
|
return RequestResult.Ok(returnData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> DeleteAccountProfilePicture()
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
Database.Model.Account.AccountProfilePicture? picture = account.ProfilePicture;
|
|
|
|
|
|
|
|
|
|
|
|
if (picture is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
account.ProfilePictureId = null;
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
database.AccountProfilePictures.Attach(picture);
|
|
|
|
|
|
database.AccountProfilePictures.Remove(picture);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return RequestResult.NoContent();
|
|
|
|
|
|
}
|
2024-04-27 22:36:16 +02:00
|
|
|
|
|
2024-11-05 20:04:15 +01:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Profile background
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> GetAccountProfileBackground(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.BackgroundPicture is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PhotoResponse response = new PhotoResponse(account.BackgroundPicture);
|
|
|
|
|
|
return RequestResult.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> PutAccountProfileBackground(AccountProfileBackgroundRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
|
|
|
|
|
|
account.BackgroundPictureId = data.Id;
|
|
|
|
|
|
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
PhotoResponse returnData = new PhotoResponse(account.BackgroundPicture!);
|
|
|
|
|
|
return RequestResult.Ok(returnData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> DeleteAccountProfileBackground()
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
|
|
|
|
|
|
if (account.BackgroundPicture is not null)
|
|
|
|
|
|
{
|
|
|
|
|
|
account.BackgroundPictureId = null;
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return RequestResult.NoContent();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2024-11-06 00:11:45 +01:00
|
|
|
|
|
|
|
|
|
|
#region Info
|
|
|
|
|
|
|
2024-11-06 15:52:26 +01:00
|
|
|
|
public async Task<RequestResult> GetAccount(long id)
|
2024-10-26 02:23:33 +02:00
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
if (account is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-03 23:01:34 +01:00
|
|
|
|
AccountResponse profileInfoResponse = new AccountResponse(account);
|
|
|
|
|
|
return RequestResult.Ok(profileInfoResponse);
|
2024-10-26 02:23:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-03 23:01:34 +01:00
|
|
|
|
public async Task<RequestResult> PutAccountProfileInfo(AccountProfileInfoRequest data)
|
2024-10-26 02:23:33 +02:00
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
if (account is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.UpdateAccount(account);
|
2024-11-03 23:01:34 +01:00
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
2024-10-26 02:23:33 +02:00
|
|
|
|
return RequestResult.Ok();
|
|
|
|
|
|
}
|
2024-11-06 00:11:45 +01:00
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> PatchAccountUsername(AccountUsernameRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
|
|
|
|
|
|
if (!ComputeHash(data.Password, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password))
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.UpdateAccount(account);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return RequestResult.Ok();
|
|
|
|
|
|
}
|
2024-11-06 14:56:02 +01:00
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> PatchAccountEmail(AccountEmailRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
|
|
|
|
|
|
if (!ComputeHash(data.Password, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password))
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data.UpdateAccount(account);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return RequestResult.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> PatchAccountPassword(AccountPasswordRequest data)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account account = await database.Accounts.FirstAsync(x => x.Id == userService.GetUserId());
|
|
|
|
|
|
|
|
|
|
|
|
if (!ComputeHash(data.OldPassword, account.LeftSalt, account.RightSalt).SequenceEqual(account.Password))
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.Unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SetPassword(account, data.NewPassword);
|
|
|
|
|
|
await database.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return RequestResult.Ok();
|
|
|
|
|
|
}
|
2024-11-06 00:11:45 +01:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
2024-11-01 01:03:00 +01:00
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> GetAccountRatedMovies(long id, MovieRatedQueryParameters query)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
if (account is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<MovieRatedResponse> response = account.RatingMedia.Join(database.MediaMovies, x => x.MediaId, x => x.Id, (x, y) => new MovieRatedResponse(y, x));
|
|
|
|
|
|
response = query.PrepareData(response);
|
|
|
|
|
|
return RequestResult.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> GetAccountRatedSeries(long id, SeriesRatedQueryParameters query)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
if (account is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<SeriesRatedResponse> response = account.RatingMedia.Join(database.MediaSeries, x => x.MediaId, x => x.Id, (x, y) => new SeriesRatedResponse(y, x));
|
|
|
|
|
|
response = query.PrepareData(response);
|
|
|
|
|
|
return RequestResult.Ok(response);
|
|
|
|
|
|
}
|
2024-11-01 20:44:01 +01:00
|
|
|
|
|
|
|
|
|
|
public async Task<RequestResult> GetAccountRatedPersons(long id, PersonRatedQueryParameters query)
|
|
|
|
|
|
{
|
|
|
|
|
|
Account? account = await database.Accounts.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
if (account is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RequestResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<RatingPersonActorRole> actorRolesRatings = account.RatingPersonActorRole;
|
|
|
|
|
|
IEnumerable<RatingPersonCreatorRole> creatorRolesRatings = account.RatingPersonCreatorRole;
|
|
|
|
|
|
IEnumerable<Person> persons = actorRolesRatings.Select(x => x.PersonActorRole.Person)
|
|
|
|
|
|
.Union(creatorRolesRatings.Select(x => x.PersonCreatorRole.Person));
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<PersonRatedResponse> response = persons.Select(x => new PersonRatedResponse(x, actorRolesRatings.Where(y => y.PersonActorRole.Person.Id == x.Id), creatorRolesRatings.Where(y => y.PersonCreatorRole.Person.Id == x.Id)));
|
|
|
|
|
|
response = query.PrepareData(response);
|
|
|
|
|
|
return RequestResult.Ok(response);
|
|
|
|
|
|
}
|
2024-10-26 02:23:33 +02:00
|
|
|
|
|
2024-04-27 22:36:16 +02:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region PRIVATE METHODS
|
|
|
|
|
|
|
|
|
|
|
|
protected byte[] ComputeHash(string password, string leftSalt, string rightSalt) => SHA512.HashData(Encoding.UTF8.GetBytes($"{leftSalt}{password}{rightSalt}"));
|
|
|
|
|
|
|
2024-11-06 14:56:02 +01:00
|
|
|
|
private void SetPassword(Account account, string password)
|
|
|
|
|
|
{
|
|
|
|
|
|
string leftSalt = StringExtensions.CreateRandom(20);
|
|
|
|
|
|
string rightSalt = StringExtensions.CreateRandom(20);
|
|
|
|
|
|
byte[] hash = ComputeHash(password, leftSalt, rightSalt);
|
|
|
|
|
|
|
|
|
|
|
|
account.Password = hash;
|
|
|
|
|
|
account.LeftSalt = leftSalt;
|
|
|
|
|
|
account.RightSalt = rightSalt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-27 22:36:16 +02:00
|
|
|
|
#endregion
|
|
|
|
|
|
}
|