1
0
mirror of synced 2025-01-19 00:04:05 +01:00

Major authentication refactors

This commit is contained in:
S-Sebb 2024-05-16 23:32:46 +01:00
parent f4cbee2e29
commit 17331bcad4
60 changed files with 1254 additions and 988 deletions

View File

@ -1,6 +0,0 @@
namespace SharedProject.Models.Responses;
public class DashboardResponse
{
public List<User> Users { get; set; } = new();
}

View File

@ -2,18 +2,20 @@
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using TaikoLocalServer.Settings; using TaikoLocalServer.Settings;
using OtpNet; using OtpNet;
using SharedProject.Models.Requests; using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController(ICredentialService credentialService, ICardService cardService, public class AuthController(IAuthService authService, IUserDatumService userDatumService,
IUserDatumService userDatumService, IOptions<AuthSettings> settings) : BaseController<AuthController> IOptions<AuthSettings> settings) : BaseController<AuthController>
{ {
private readonly AuthSettings authSettings = settings.Value; private readonly AuthSettings authSettings = settings.Value;
@ -79,16 +81,17 @@ public class AuthController(ICredentialService credentialService, ICardService c
} }
[HttpPost("Login")] [HttpPost("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginRequest loginRequest) public async Task<IActionResult> Login(LoginRequest loginRequest)
{ {
var accessCode = loginRequest.AccessCode; var accessCode = loginRequest.AccessCode;
var password = loginRequest.Password; var password = loginRequest.Password;
var card = await cardService.GetCardByAccessCode(accessCode); var card = await authService.GetCardByAccessCode(accessCode);
if (card == null) if (card == null)
return Unauthorized(new { message = "Access Code Not Found" }); return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.GetCredentialByBaid(card.Baid); var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null) if (credential == null)
return Unauthorized(new { message = "Credential Not Found" }); return Unauthorized(new { message = "Credential Not Found" });
@ -112,7 +115,22 @@ public class AuthController(ICredentialService credentialService, ICardService c
return Ok(new { authToken }); return Ok(new { authToken });
} }
[HttpPost("LoginWithToken")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public IActionResult LoginWithToken()
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
return Ok();
}
[HttpPost("Register")] [HttpPost("Register")]
[AllowAnonymous]
public async Task<IActionResult> Register(RegisterRequest registerRequest) public async Task<IActionResult> Register(RegisterRequest registerRequest)
{ {
var accessCode = registerRequest.AccessCode; var accessCode = registerRequest.AccessCode;
@ -121,11 +139,11 @@ public class AuthController(ICredentialService credentialService, ICardService c
var registerWithLastPlayTime = registerRequest.RegisterWithLastPlayTime; var registerWithLastPlayTime = registerRequest.RegisterWithLastPlayTime;
var inviteCode = registerRequest.InviteCode; var inviteCode = registerRequest.InviteCode;
var card = await cardService.GetCardByAccessCode(accessCode); var card = await authService.GetCardByAccessCode(accessCode);
if (card == null) if (card == null)
return Unauthorized(new { message = "Access Code Not Found" }); return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.GetCredentialByBaid(card.Baid); var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null) if (credential == null)
return Unauthorized(new { message = "Credential Not Found" }); return Unauthorized(new { message = "Credential Not Found" });
@ -156,22 +174,41 @@ public class AuthController(ICredentialService credentialService, ICardService c
var salt = CreateSalt(); var salt = CreateSalt();
var hashedPassword = ComputeHash(password, salt); var hashedPassword = ComputeHash(password, salt);
var result = await credentialService.UpdatePassword(card.Baid, hashedPassword, salt); var result = await authService.UpdatePassword(card.Baid, hashedPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" }); return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
} }
[HttpPost("ChangePassword")] [HttpPost("ChangePassword")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> ChangePassword(ChangePasswordRequest changePasswordRequest) public async Task<IActionResult> ChangePassword(ChangePasswordRequest changePasswordRequest)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin)
{
var requestBaid = authService.GetCardByAccessCode(changePasswordRequest.AccessCode).Result?.Baid;
if (requestBaid != tokenInfo.Value.baid)
{
return Forbid();
}
}
}
var accessCode = changePasswordRequest.AccessCode; var accessCode = changePasswordRequest.AccessCode;
var oldPassword = changePasswordRequest.OldPassword; var oldPassword = changePasswordRequest.OldPassword;
var newPassword = changePasswordRequest.NewPassword; var newPassword = changePasswordRequest.NewPassword;
var card = await cardService.GetCardByAccessCode(accessCode); var card = await authService.GetCardByAccessCode(accessCode);
if (card == null) if (card == null)
return Unauthorized(new { message = "Access Code Not Found" }); return Unauthorized(new { message = "Access Code Not Found" });
var credential = await credentialService.GetCredentialByBaid(card.Baid); var credential = await authService.GetCredentialByBaid(card.Baid);
if (credential == null) if (credential == null)
return Unauthorized(new { message = "Credential Not Found" }); return Unauthorized(new { message = "Credential Not Found" });
@ -187,35 +224,57 @@ public class AuthController(ICredentialService credentialService, ICardService c
var salt = CreateSalt(); var salt = CreateSalt();
var hashedNewPassword = ComputeHash(newPassword, salt); var hashedNewPassword = ComputeHash(newPassword, salt);
var result = await credentialService.UpdatePassword(card.Baid, hashedNewPassword, salt); var result = await authService.UpdatePassword(card.Baid, hashedNewPassword, salt);
return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" }); return result ? Ok() : Unauthorized( new { message = "Failed to Update Password" });
} }
[HttpPost("ResetPassword")] [HttpPost("ResetPassword")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> ResetPassword(ResetPasswordRequest resetPasswordRequest) public async Task<IActionResult> ResetPassword(ResetPasswordRequest resetPasswordRequest)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && resetPasswordRequest.Baid != tokenInfo.Value.baid)
{
return Forbid();
}
}
var baid = resetPasswordRequest.Baid; var baid = resetPasswordRequest.Baid;
var credential = await credentialService.GetCredentialByBaid(baid); var credential = await authService.GetCredentialByBaid(baid);
if (credential == null) if (credential == null)
return Unauthorized(new { message = "Credential Not Found" }); return Unauthorized(new { message = "Credential Not Found" });
var result = await credentialService.UpdatePassword(baid, "", ""); var result = await authService.UpdatePassword(baid, "", "");
return result ? Ok() : Unauthorized( new { message = "Failed to Reset Password" }); return result ? Ok() : Unauthorized( new { message = "Failed to Reset Password" });
} }
[HttpPost("GenerateOtp")] [HttpPost("GenerateOtp")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public IActionResult GenerateOtp(GenerateOtpRequest generateOtpRequest) public IActionResult GenerateOtp(GenerateOtpRequest generateOtpRequest)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var totp = MakeTotp(generateOtpRequest.Baid); var totp = MakeTotp(generateOtpRequest.Baid);
return Ok(new { otp = totp.ComputeTotp() }); return Ok(new { otp = totp.ComputeTotp() });
} }
[HttpPost("VerifyOtp")]
public IActionResult VerifyOtpHandler(VerifyOtpRequest verifyOtpRequest)
{
if (VerifyOtp(verifyOtpRequest.Otp, verifyOtpRequest.Baid))
return Ok();
return Unauthorized();
}
} }

View File

@ -1,42 +1,80 @@
using SharedProject.Models.Requests; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class CardsController : BaseController<CardsController> public class CardsController(IAuthService authService, IOptions<AuthSettings> settings) : BaseController<CardsController>
{ {
private readonly ICardService cardService; private readonly AuthSettings authSettings = settings.Value;
public CardsController(ICardService cardService)
{
this.cardService = cardService;
}
[HttpDelete("{accessCode}")] [HttpDelete("{accessCode}")]
public async Task<IActionResult> DeleteUser(string accessCode) [ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> DeleteAccessCode(string accessCode)
{ {
var result = await cardService.DeleteCard(accessCode); if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
var card = await authService.GetCardByAccessCode(accessCode);
if (card == null)
{
return Unauthorized();
}
if (card.Baid != tokenInfo.Value.baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var result = await authService.DeleteCard(accessCode);
return result ? NoContent() : NotFound(); return result ? NoContent() : NotFound();
} }
[HttpPost] [HttpPost("BindAccessCode")]
public async Task<IActionResult> BindAccessCode(BindAccessCodeRequest request) [ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> BindAccessCode(BindAccessCodeRequest bindAccessCodeRequest)
{ {
var accessCode = request.AccessCode; if (authSettings.LoginRequired)
var baid = request.Baid; {
var existingCard = await cardService.GetCardByAccessCode(accessCode); var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != bindAccessCodeRequest.Baid)
{
return Forbid();
}
}
var accessCode = bindAccessCodeRequest.AccessCode;
var baid = bindAccessCodeRequest.Baid;
var existingCard = await authService.GetCardByAccessCode(accessCode);
if (existingCard is not null) if (existingCard is not null)
{ {
return BadRequest("Access code already exists"); return BadRequest("Access code already exists");
} }
var newCard = new Card var newCard = new Card
{ {
Baid = baid, Baid = baid,
AccessCode = accessCode AccessCode = accessCode
}; };
await cardService.AddCard(newCard); await authService.AddCard(newCard);
return NoContent(); return NoContent();
} }
} }

View File

@ -1,23 +1,37 @@
using SharedProject.Models; using Microsoft.Extensions.Options;
using SharedProject.Models;
using SharedProject.Models.Responses; using SharedProject.Models.Responses;
using Swan.Mapping; using Swan.Mapping;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class DanBestDataController : BaseController<DanBestDataController> public class DanBestDataController(IDanScoreDatumService danScoreDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<DanBestDataController>
{ {
private readonly IDanScoreDatumService danScoreDatumService; private readonly AuthSettings authSettings = settings.Value;
public DanBestDataController(IDanScoreDatumService danScoreDatumService)
{
this.danScoreDatumService = danScoreDatumService;
}
[HttpGet("{baid}")] [HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> GetDanBestData(uint baid) public async Task<IActionResult> GetDanBestData(uint baid)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return Forbid();
}
}
// FIXME: Handle gaiden in here and web ui // FIXME: Handle gaiden in here and web ui
var danScores = await danScoreDatumService.GetDanScoreDataList(baid, DanType.Normal); var danScores = await danScoreDatumService.GetDanScoreDataList(baid, DanType.Normal);
var danDataList = new List<DanBestData>(); var danDataList = new List<DanBestData>();

View File

@ -1,26 +0,0 @@
using SharedProject.Models.Responses;
namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("/api/[controller]")]
public class DashboardController : BaseController<DashboardController>
{
private readonly ICardService cardService;
public DashboardController(ICardService cardService)
{
this.cardService = cardService;
}
[HttpGet]
public async Task<DashboardResponse> GetDashboard()
{
var users = await cardService.GetUsersFromCards();
return new DashboardResponse
{
Users = users
};
}
}

View File

@ -1,21 +1,35 @@
using SharedProject.Models.Requests; using Microsoft.Extensions.Options;
using SharedProject.Models.Requests;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class FavoriteSongsController : BaseController<FavoriteSongsController> public class FavoriteSongsController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<FavoriteSongsController>
{ {
private readonly IUserDatumService userDatumService; private readonly AuthSettings authSettings = settings.Value;
public FavoriteSongsController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
[HttpPost] [HttpPost]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> UpdateFavoriteSong(SetFavoriteRequest request) public async Task<IActionResult> UpdateFavoriteSong(SetFavoriteRequest request)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != request.Baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(request.Baid); var user = await userDatumService.GetFirstUserDatumOrNull(request.Baid);
if (user is null) if (user is null)
@ -28,8 +42,23 @@ public class FavoriteSongsController : BaseController<FavoriteSongsController>
} }
[HttpGet("{baid}")] [HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> GetFavoriteSongs(uint baid) public async Task<IActionResult> GetFavoriteSongs(uint baid)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid); var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null) if (user is null)

View File

@ -1,75 +1,79 @@
using Riok.Mapperly.Abstractions; using Microsoft.Extensions.Options;
using Riok.Mapperly.Abstractions;
using SharedProject.Models.Responses; using SharedProject.Models.Responses;
using SharedProject.Models; using SharedProject.Models;
using GameDatabase.Entities; using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api namespace TaikoLocalServer.Controllers.Api;
[ApiController]
[Route("api/[controller]")]
public class PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService, IAuthService authService, IOptions<AuthSettings> settings)
: BaseController<PlayDataController>
{ {
[ApiController] private readonly AuthSettings authSettings = settings.Value;
[Route("api/[controller]")]
public class PlayDataController : BaseController<PlayDataController> [HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<SongBestResponse>> GetSongBestRecords(uint baid)
{ {
private readonly IUserDatumService userDatumService; if (authSettings.LoginRequired)
private readonly ISongBestDatumService songBestDatumService;
private readonly ISongPlayDatumService songPlayDatumService;
private readonly SongBestResponseMapper _songBestResponseMapper; // Inject SongBestResponseMapper
public PlayDataController(IUserDatumService userDatumService, ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService, SongBestResponseMapper songBestResponseMapper)
{
this.userDatumService = userDatumService;
this.songBestDatumService = songBestDatumService;
this.songPlayDatumService = songPlayDatumService;
_songBestResponseMapper = songBestResponseMapper; // Assign the injected mapper
}
[HttpGet("{baid}")]
public async Task<ActionResult<SongBestResponse>> GetSongBestRecords(uint baid)
{ {
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{ {
var user = await userDatumService.GetFirstUserDatumOrNull(baid); return Unauthorized();
if (user is null) }
{
return NotFound();
}
var songBestRecords = await songBestDatumService.GetAllSongBestAsModel(baid); if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
var songPlayData = await songPlayDatumService.GetSongPlayDatumByBaid(baid); {
foreach (var songBestData in songBestRecords) return Forbid();
{
var songPlayLogs = songPlayData.Where(datum => datum.SongId == songBestData.SongId &&
datum.Difficulty == songBestData.Difficulty).ToList();
songBestData.PlayCount = songPlayLogs.Count;
songBestData.ClearCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Clear);
songBestData.FullComboCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Gold);
songBestData.PerfectCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Dondaful);
}
var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid);
var favoriteSet = favoriteSongs.ToHashSet();
foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSet.Contains(songBestRecord.SongId)))
{
songBestRecord.IsFavorite = true;
}
foreach (var songBestRecord in songBestRecords)
{
songBestRecord.RecentPlayData = songPlayData
.Where(datum => datum.SongId == songBestRecord.SongId && datum.Difficulty == songBestRecord.Difficulty)
.Select(SongBestResponseMapper.MapToDto)
.ToList();
}
return Ok(new SongBestResponse
{
SongBestData = songBestRecords
});
} }
} }
}
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] var user = await userDatumService.GetFirstUserDatumOrNull(baid);
public partial class SongBestResponseMapper if (user is null)
{ {
public static partial SongPlayDatumDto MapToDto(SongPlayDatum entity); return NotFound();
}
var songBestRecords = await songBestDatumService.GetAllSongBestAsModel(baid);
var songPlayData = await songPlayDatumService.GetSongPlayDatumByBaid(baid);
foreach (var songBestData in songBestRecords)
{
var songPlayLogs = songPlayData.Where(datum => datum.SongId == songBestData.SongId &&
datum.Difficulty == songBestData.Difficulty).ToList();
songBestData.PlayCount = songPlayLogs.Count;
songBestData.ClearCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Clear);
songBestData.FullComboCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Gold);
songBestData.PerfectCount = songPlayLogs.Count(datum => datum.Crown >= CrownType.Dondaful);
}
var favoriteSongs = await userDatumService.GetFavoriteSongIds(baid);
var favoriteSet = favoriteSongs.ToHashSet();
foreach (var songBestRecord in songBestRecords.Where(songBestRecord => favoriteSet.Contains(songBestRecord.SongId)))
{
songBestRecord.IsFavorite = true;
}
foreach (var songBestRecord in songBestRecords)
{
songBestRecord.RecentPlayData = songPlayData
.Where(datum => datum.SongId == songBestRecord.SongId && datum.Difficulty == songBestRecord.Difficulty)
.Select(SongBestResponseMapper.MapToDto)
.ToList();
}
return Ok(new SongBestResponse
{
SongBestData = songBestRecords
});
} }
} }
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public partial class SongBestResponseMapper
{
public static partial SongPlayDatumDto MapToDto(SongPlayDatum entity);
}

View File

@ -1,22 +1,36 @@
using GameDatabase.Entities; using Microsoft.Extensions.Options;
using SharedProject.Models; using SharedProject.Models;
using SharedProject.Models.Responses; using SharedProject.Models.Responses;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class PlayHistoryController( public class PlayHistoryController(IUserDatumService userDatumService, ISongPlayDatumService songPlayDatumService,
IUserDatumService userDatumService, IAuthService authService, IOptions<AuthSettings> settings) : BaseController<PlayDataController>
ISongBestDatumService songBestDatumService,
ISongPlayDatumService songPlayDatumService)
: BaseController<PlayDataController>
{ {
private readonly ISongBestDatumService songBestDatumService = songBestDatumService; private readonly AuthSettings authSettings = settings.Value;
[HttpGet("{baid}")] [HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<SongHistoryResponse>> GetSongHistory(uint baid) public async Task<ActionResult<SongHistoryResponse>> GetSongHistory(uint baid)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid); var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null) if (user is null)
{ {

View File

@ -1,22 +1,36 @@
using SharedProject.Models; using Microsoft.Extensions.Options;
using SharedProject.Models;
using SharedProject.Utils; using SharedProject.Utils;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api; namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("/api/[controller]/{baid}")] [Route("/api/[controller]/{baid}")]
public class UserSettingsController : BaseController<UserSettingsController> public class UserSettingsController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<UserSettingsController>
{ {
private readonly IUserDatumService userDatumService; private readonly AuthSettings authSettings = settings.Value;
public UserSettingsController(IUserDatumService userDatumService)
{
this.userDatumService = userDatumService;
}
[HttpGet] [HttpGet]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<ActionResult<UserSetting>> GetUserSetting(uint baid) public async Task<ActionResult<UserSetting>> GetUserSetting(uint baid)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid); var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null) if (user is null)
@ -75,8 +89,23 @@ public class UserSettingsController : BaseController<UserSettingsController>
} }
[HttpPost] [HttpPost]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> SaveUserSetting(uint baid, UserSetting userSetting) public async Task<IActionResult> SaveUserSetting(uint baid, UserSetting userSetting)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo is null)
{
return Unauthorized();
}
if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin)
{
return Forbid();
}
}
var user = await userDatumService.GetFirstUserDatumOrNull(baid); var user = await userDatumService.GetFirstUserDatumOrNull(baid);
if (user is null) if (user is null)

View File

@ -1,19 +1,78 @@
namespace TaikoLocalServer.Controllers.Api; using Microsoft.Extensions.Options;
using SharedProject.Models;
using TaikoLocalServer.Filters;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Controllers.Api;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class UsersController : BaseController<UsersController> public class UsersController(IUserDatumService userDatumService, IAuthService authService,
IOptions<AuthSettings> settings) : BaseController<UsersController>
{ {
private readonly IUserDatumService userDatumService; private readonly AuthSettings authSettings = settings.Value;
public UsersController(IUserDatumService userDatumService) [HttpGet("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<User?> GetUser(uint baid)
{ {
this.userDatumService = userDatumService; if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return null;
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return null;
}
}
var user = await authService.GetUserByBaid(baid);
return user;
}
[HttpGet]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IEnumerable<User>> GetUsers()
{
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Array.Empty<User>();
}
if (!tokenInfo.Value.isAdmin)
{
return Array.Empty<User>();
}
}
return await authService.GetUsersFromCards();
} }
[HttpDelete("{baid}")] [HttpDelete("{baid}")]
[ServiceFilter(typeof(AuthorizeIfRequiredAttribute))]
public async Task<IActionResult> DeleteUser(uint baid) public async Task<IActionResult> DeleteUser(uint baid)
{ {
if (authSettings.LoginRequired)
{
var tokenInfo = authService.ExtractTokenInfo(HttpContext);
if (tokenInfo == null)
{
return Unauthorized();
}
if (!tokenInfo.Value.isAdmin && tokenInfo.Value.baid != baid)
{
return Forbid();
}
}
var result = await userDatumService.DeleteUser(baid); var result = await userDatumService.DeleteUser(baid);
return result ? NoContent() : NotFound(); return result ? NoContent() : NotFound();

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using TaikoLocalServer.Settings;
namespace TaikoLocalServer.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AuthorizeIfRequiredAttribute(IOptions<AuthSettings> settings) : Attribute, IAsyncAuthorizationFilter
{
private readonly bool loginRequired = settings.Value.LoginRequired;
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (!loginRequired)
{
return; // Skip authorization if login is not required
}
var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var policyProvider = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
var policy = await policyProvider.GetPolicyAsync(AuthorizationPolicyNames.Default);
if (policy != null)
{
var authResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, policy);
if (!authResult.Succeeded)
{
context.Result = new UnauthorizedResult();
}
}
}
}
public static class AuthorizationPolicyNames
{
public const string Default = "Default";
}
}

View File

@ -2,7 +2,7 @@
namespace TaikoLocalServer.Models; namespace TaikoLocalServer.Models;
public class MusicInfoes public class MusicInfos
{ {
[JsonPropertyName("items")] [JsonPropertyName("items")]
public List<MusicInfoEntry> MusicInfoEntries { get; set; } = new(); public List<MusicInfoEntry> MusicInfoEntries { get; set; } = new();

View File

@ -14,6 +14,7 @@ using Throw;
using Serilog; using Serilog;
using SharedProject.Utils; using SharedProject.Utils;
using TaikoLocalServer.Controllers.Api; using TaikoLocalServer.Controllers.Api;
using TaikoLocalServer.Filters;
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Console() .WriteTo.Console()
@ -43,6 +44,7 @@ try
builder.Configuration.AddJsonFile($"{configurationsDirectory}/ServerSettings.json", optional: false, reloadOnChange: false); builder.Configuration.AddJsonFile($"{configurationsDirectory}/ServerSettings.json", optional: false, reloadOnChange: false);
builder.Configuration.AddJsonFile($"{configurationsDirectory}/DataSettings.json", optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile($"{configurationsDirectory}/DataSettings.json", optional: true, reloadOnChange: false);
builder.Configuration.AddJsonFile($"{configurationsDirectory}/AuthSettings.json", optional: true, reloadOnChange: false); builder.Configuration.AddJsonFile($"{configurationsDirectory}/AuthSettings.json", optional: true, reloadOnChange: false);
builder.Configuration.AddJsonFile("wwwroot/appsettings.json", optional: true, reloadOnChange: true); // Add appsettings.json
builder.Host.UseSerilog((context, configuration) => builder.Host.UseSerilog((context, configuration) =>
{ {
@ -70,6 +72,11 @@ try
builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection(nameof(ServerSettings))); builder.Services.Configure<ServerSettings>(builder.Configuration.GetSection(nameof(ServerSettings)));
builder.Services.Configure<DataSettings>(builder.Configuration.GetSection(nameof(DataSettings))); builder.Services.Configure<DataSettings>(builder.Configuration.GetSection(nameof(DataSettings)));
builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection(nameof(AuthSettings))); builder.Services.Configure<AuthSettings>(builder.Configuration.GetSection(nameof(AuthSettings)));
// Read LoginRequired setting from appsettings.json
var loginRequired = builder.Configuration.GetValue<bool>("LoginRequired");
builder.Services.Configure<AuthSettings>(options => { options.LoginRequired = loginRequired; });
// Add Authentication with JWT // Add Authentication with JWT
builder.Services.AddAuthentication(options => builder.Services.AddAuthentication(options =>
{ {
@ -90,6 +97,8 @@ try
}; };
}); });
builder.Services.AddScoped<AuthorizeIfRequiredAttribute>(); // Register the custom attribute
builder.Services.AddControllers().AddProtoBufNet(); builder.Services.AddControllers().AddProtoBufNet();
builder.Services.AddDbContext<TaikoDbContext>(option => builder.Services.AddDbContext<TaikoDbContext>(option =>
{ {
@ -151,6 +160,10 @@ try
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
// Enable Authentication and Authorization middleware
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpLogging(); app.UseHttpLogging();
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
@ -160,6 +173,7 @@ try
{ {
Log.Error("Unknown request from: {RemoteIpAddress} {Method} {Path} {StatusCode}", Log.Error("Unknown request from: {RemoteIpAddress} {Method} {Path} {StatusCode}",
context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path, context.Response.StatusCode); context.Connection.RemoteIpAddress, context.Request.Method, context.Request.Path, context.Response.StatusCode);
Log.Error("Request headers: {Headers}", context.Request.Headers);
} }
}); });
app.MapControllers(); app.MapControllers();

View File

@ -0,0 +1,139 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using GameDatabase.Context;
using SharedProject.Models;
using Swan.Mapping;
namespace TaikoLocalServer.Services;
public class AuthService(TaikoDbContext context) : IAuthService
{
public async Task<Card?> GetCardByAccessCode(string accessCode)
{
return await context.Cards.FindAsync(accessCode);
}
public async Task<User?> GetUserByBaid(uint baid)
{
var userDatum = await context.UserData.FindAsync(baid);
if (userDatum == null) return null;
var cardEntries = await context.Cards.Where(card => card.Baid == baid).ToListAsync();
return new User
{
Baid = userDatum.Baid,
AccessCodes = cardEntries.Select(card => card.AccessCode).ToList(),
IsAdmin = userDatum.IsAdmin
};
}
public async Task<List<User>> GetUsersFromCards()
{
var cardEntries = await context.Cards.ToListAsync();
var userEntries = await context.UserData.ToListAsync();
var users = userEntries.Select(userEntry => new User
{
Baid = userEntry.Baid,
AccessCodes = cardEntries.Where(cardEntry => cardEntry.Baid == userEntry.Baid).Select(cardEntry => cardEntry.AccessCode).ToList(),
IsAdmin = userEntry.IsAdmin
}).ToList();
return users;
}
public async Task AddCard(Card card)
{
context.Add(card);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCard(string accessCode)
{
var card = await context.Cards.FindAsync(accessCode);
if (card == null) return false;
context.Cards.Remove(card);
await context.SaveChangesAsync();
return true;
}
public async Task<List<UserCredential>> GetUserCredentialsFromCredentials()
{
return await context.Credentials.Select(credential => credential.CopyPropertiesToNew<UserCredential>(null)).ToListAsync();
}
public async Task AddCredential(Credential credential)
{
context.Add(credential);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCredential(uint baid)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
context.Credentials.Remove(credential);
await context.SaveChangesAsync();
return true;
}
public async Task<bool> UpdatePassword(uint baid, string password, string salt)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
credential.Password = password;
credential.Salt = salt;
await context.SaveChangesAsync();
return true;
}
public async Task<Credential?> GetCredentialByBaid(uint baid)
{
return await context.Credentials.FindAsync(baid);
}
public (uint baid, bool isAdmin)? ExtractTokenInfo(HttpContext httpContext)
{
var authHeader = httpContext.Request.Headers.Authorization.FirstOrDefault();
if (authHeader == null || !authHeader.StartsWith("Bearer "))
{
Console.WriteLine("Invalid auth header");
return null;
}
var token = authHeader["Bearer ".Length..].Trim();
var handler = new JwtSecurityTokenHandler();
if (!handler.CanReadToken(token))
{
Console.WriteLine("Invalid token");
return null;
}
var jwtToken = handler.ReadJwtToken(token);
if (jwtToken.ValidTo < DateTime.UtcNow)
{
Console.WriteLine("Token expired");
return null;
}
var claimBaid = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
var claimRole = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value;
if (claimBaid == null || claimRole == null)
{
Console.WriteLine("Invalid token claims");
return null;
}
if (!uint.TryParse(claimBaid, out var baid))
{
Console.WriteLine("Invalid baid");
return null;
}
var isAdmin = claimRole == "Admin";
return (baid, isAdmin);
}
}

View File

@ -1,47 +0,0 @@
using GameDatabase.Context;
using SharedProject.Models;
namespace TaikoLocalServer.Services;
public class CardService : ICardService
{
private readonly TaikoDbContext context;
public CardService(TaikoDbContext context)
{
this.context = context;
}
public async Task<Card?> GetCardByAccessCode(string accessCode)
{
return await context.Cards.FindAsync(accessCode);
}
public async Task<List<User>> GetUsersFromCards()
{
var cardEntries = await context.Cards.ToListAsync();
var userEntries = await context.UserData.ToListAsync();
var users = userEntries.Select(userEntry => new User
{
Baid = (uint)userEntry.Baid,
AccessCodes = cardEntries.Where(cardEntry => cardEntry.Baid == userEntry.Baid).Select(cardEntry => cardEntry.AccessCode).ToList(),
IsAdmin = userEntry.IsAdmin
}).ToList();
return users;
}
public async Task AddCard(Card card)
{
context.Add(card);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCard(string accessCode)
{
var card = await context.Cards.FindAsync(accessCode);
if (card == null) return false;
context.Cards.Remove(card);
await context.SaveChangesAsync();
return true;
}
}

View File

@ -1,49 +0,0 @@
using GameDatabase.Context;
using SharedProject.Models;
using Swan.Mapping;
namespace TaikoLocalServer.Services;
public class CredentialService(TaikoDbContext context) : ICredentialService
{
public async Task<List<UserCredential>> GetUserCredentialsFromCredentials()
{
return await context.Credentials.Select(credential => credential.CopyPropertiesToNew<UserCredential>(null)).ToListAsync();
}
public async Task AddCredential(Credential credential)
{
context.Add(credential);
await context.SaveChangesAsync();
}
public async Task<bool> DeleteCredential(uint baid)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
context.Credentials.Remove(credential);
await context.SaveChangesAsync();
return true;
}
public async Task<bool> UpdatePassword(uint baid, string password, string salt)
{
var credential = await context.Credentials.FindAsync(baid);
if (credential is null) return false;
credential.Password = password;
credential.Salt = salt;
await context.SaveChangesAsync();
return true;
}
public async Task<Credential?> GetCredentialByBaid(uint baid)
{
return await context.Credentials.FindAsync(baid);
}
}

View File

@ -4,8 +4,7 @@ public static class ServiceExtensions
{ {
public static IServiceCollection AddTaikoDbServices(this IServiceCollection services) public static IServiceCollection AddTaikoDbServices(this IServiceCollection services)
{ {
services.AddScoped<ICardService, CardService>(); services.AddScoped<IAuthService, AuthService>();
services.AddScoped<ICredentialService, CredentialService>();
services.AddScoped<IUserDatumService, UserDatumService>(); services.AddScoped<IUserDatumService, UserDatumService>();
services.AddScoped<ISongPlayDatumService, SongPlayDatumService>(); services.AddScoped<ISongPlayDatumService, SongPlayDatumService>();
services.AddScoped<ISongBestDatumService, SongBestDatumService>(); services.AddScoped<ISongBestDatumService, SongBestDatumService>();

View File

@ -65,11 +65,6 @@ public class GameDataService : IGameDataService
return musicsWithUra; return musicsWithUra;
} }
public ImmutableDictionary<uint, MusicInfoEntry> GetMusicInfoes()
{
return musicInfoes;
}
public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary() public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary()
{ {
return movieDataDictionary; return movieDataDictionary;
@ -161,7 +156,7 @@ public class GameDataService : IGameDataService
var shopFolderDataPath = Path.Combine(dataPath, settings.ShopFolderDataFileName); var shopFolderDataPath = Path.Combine(dataPath, settings.ShopFolderDataFileName);
var tokenDataPath = Path.Combine(dataPath, settings.TokenDataFileName); var tokenDataPath = Path.Combine(dataPath, settings.TokenDataFileName);
var lockedSongsDataPath = Path.Combine(dataPath, settings.LockedSongsDataFileName); var lockedSongsDataPath = Path.Combine(dataPath, settings.LockedSongsDataFileName);
var qrCodeDataPath = Path.Combine(dataPath, settings.QRCodeDataFileName); var qrCodeDataPath = Path.Combine(dataPath, settings.QrCodeDataFileName);
var encryptedFiles = new List<string> var encryptedFiles = new List<string>
{ {
@ -213,7 +208,7 @@ public class GameDataService : IGameDataService
await using var neiroFile = File.OpenRead(neiroPath); await using var neiroFile = File.OpenRead(neiroPath);
await using var qrCodeDataFile = File.OpenRead(qrCodeDataPath); await using var qrCodeDataFile = File.OpenRead(qrCodeDataPath);
var infoesData = await JsonSerializer.DeserializeAsync<MusicInfoes>(musicInfoFile); var infosData = await JsonSerializer.DeserializeAsync<MusicInfos>(musicInfoFile);
var danData = await JsonSerializer.DeserializeAsync<List<DanData>>(danDataFile); var danData = await JsonSerializer.DeserializeAsync<List<DanData>>(danDataFile);
var gaidenData = await JsonSerializer.DeserializeAsync<List<DanData>>(gaidenDataFile); var gaidenData = await JsonSerializer.DeserializeAsync<List<DanData>>(gaidenDataFile);
var introData = await JsonSerializer.DeserializeAsync<List<SongIntroductionData>>(songIntroDataFile); var introData = await JsonSerializer.DeserializeAsync<List<SongIntroductionData>>(songIntroDataFile);
@ -227,7 +222,7 @@ public class GameDataService : IGameDataService
var neiroData = await JsonSerializer.DeserializeAsync<Neiros>(neiroFile); var neiroData = await JsonSerializer.DeserializeAsync<Neiros>(neiroFile);
var qrCodeData = await JsonSerializer.DeserializeAsync<List<QRCodeData>>(qrCodeDataFile); var qrCodeData = await JsonSerializer.DeserializeAsync<List<QRCodeData>>(qrCodeDataFile);
InitializeMusicInfoes(infoesData); InitializeMusicInfos(infosData);
InitializeDanData(danData); InitializeDanData(danData);
@ -305,11 +300,11 @@ public class GameDataService : IGameDataService
eventFolderDictionary = eventFolderData.ToImmutableDictionary(d => d.FolderId); eventFolderDictionary = eventFolderData.ToImmutableDictionary(d => d.FolderId);
} }
private void InitializeMusicInfoes(MusicInfoes? infoesData) private void InitializeMusicInfos(MusicInfos? infosData)
{ {
infoesData.ThrowIfNull("Shouldn't happen!"); infosData.ThrowIfNull("Shouldn't happen!");
musicInfoes = infoesData.MusicInfoEntries.ToImmutableDictionary(info => info.MusicId); musicInfoes = infosData.MusicInfoEntries.ToImmutableDictionary(info => info.MusicId);
musics = musicInfoes.Select(pair => pair.Key) musics = musicInfoes.Select(pair => pair.Key)
.ToList(); .ToList();

View File

@ -2,8 +2,18 @@
namespace TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Services.Interfaces;
public interface ICredentialService public interface IAuthService
{ {
public Task<User?> GetUserByBaid(uint baid);
public Task<Card?> GetCardByAccessCode(string accessCode);
public Task<List<User>> GetUsersFromCards();
public Task AddCard(Card card);
public Task<bool> DeleteCard(string accessCode);
public Task<List<UserCredential>> GetUserCredentialsFromCredentials(); public Task<List<UserCredential>> GetUserCredentialsFromCredentials();
public Task AddCredential(Credential credential); public Task AddCredential(Credential credential);
@ -13,4 +23,6 @@ public interface ICredentialService
public Task<bool> UpdatePassword(uint baid, string password, string salt); public Task<bool> UpdatePassword(uint baid, string password, string salt);
public Task<Credential?> GetCredentialByBaid(uint baid); public Task<Credential?> GetCredentialByBaid(uint baid);
public (uint baid, bool isAdmin)? ExtractTokenInfo(HttpContext httpContext);
} }

View File

@ -1,14 +0,0 @@
using SharedProject.Models;
namespace TaikoLocalServer.Services.Interfaces;
public interface ICardService
{
public Task<Card?> GetCardByAccessCode(string accessCode);
public Task<List<User>> GetUsersFromCards();
public Task AddCard(Card card);
public Task<bool> DeleteCard(string accessCode);
}

View File

@ -5,36 +5,33 @@ namespace TaikoLocalServer.Services.Interfaces;
public interface IGameDataService public interface IGameDataService
{ {
public Task InitializeAsync(); public Task InitializeAsync();
public List<uint> GetMusicList(); public List<uint> GetMusicList();
public List<uint> GetMusicWithUraList(); public List<uint> GetMusicWithUraList();
public ImmutableDictionary<uint, MusicInfoEntry> GetMusicInfoes(); public ImmutableDictionary<uint, SongIntroductionData> GetSongIntroductionDictionary();
public ImmutableDictionary<uint, SongIntroductionData> GetSongIntroductionDictionary(); public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary();
public ImmutableDictionary<uint, MovieData> GetMovieDataDictionary(); public ImmutableDictionary<uint, EventFolderData> GetEventFolderDictionary();
public ImmutableDictionary<uint, EventFolderData> GetEventFolderDictionary(); public ImmutableDictionary<uint, DanData> GetCommonDanDataDictionary();
public ImmutableDictionary<uint, DanData> GetCommonDanDataDictionary(); public ImmutableDictionary<uint, DanData> GetCommonGaidenDataDictionary();
public ImmutableDictionary<uint, DanData> GetCommonGaidenDataDictionary(); public List<ShopFolderData> GetShopFolderList();
public List<ShopFolderData> GetShopFolderList(); public Dictionary<string, int> GetTokenDataDictionary();
public Dictionary<string, int> GetTokenDataDictionary(); public List<uint> GetLockedSongsList();
public List<uint> GetLockedSongsList(); public List<int> GetCostumeFlagArraySizes();
public List<int> GetCostumeFlagArraySizes(); public int GetTitleFlagArraySize();
public int GetTitleFlagArraySize(); public int GetToneFlagArraySize();
public int GetToneFlagArraySize(); public ImmutableDictionary<string, uint> GetQRCodeDataDictionary();
public ImmutableDictionary<string, uint> GetQRCodeDataDictionary();
} }

View File

@ -3,18 +3,8 @@ using Throw;
namespace TaikoLocalServer.Services; namespace TaikoLocalServer.Services;
public class UserDatumService : IUserDatumService public class UserDatumService(TaikoDbContext context) : IUserDatumService
{ {
private readonly TaikoDbContext context;
private readonly ILogger<UserDatumService> logger;
public UserDatumService(TaikoDbContext context, ILogger<UserDatumService> logger)
{
this.context = context;
this.logger = logger;
}
public async Task<UserDatum?> GetFirstUserDatumOrNull(uint baid) public async Task<UserDatum?> GetFirstUserDatumOrNull(uint baid)
{ {
return await context.UserData return await context.UserData

View File

@ -7,4 +7,6 @@ public class AuthSettings
public string JwtIssuer { get; set; } = string.Empty; public string JwtIssuer { get; set; } = string.Empty;
public string JwtAudience { get; set; } = string.Empty; public string JwtAudience { get; set; } = string.Empty;
public bool LoginRequired { get; set; }
} }

View File

@ -18,5 +18,9 @@ public class DataSettings
public string LockedSongsDataFileName { get; set; } = "locked_songs_data.json"; public string LockedSongsDataFileName { get; set; } = "locked_songs_data.json";
public string QRCodeDataFileName { get; set; } = "qrcode_data.json"; public string QrCodeDataFileName { get; set; } = "qrcode_data.json";
public string LockedCostumeDataFileName { get; set; } = "locked_costume_data.json";
public string LockedTitleDataFileName { get; set; } = "locked_title_data.json";
} }

View File

@ -1,7 +1,7 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage @inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
<MudThemeProvider IsDarkMode="@isDarkMode" Theme="@taikoWebUiTheme" /> <MudThemeProvider IsDarkMode="@isDarkMode" Theme="@taikoWebUiTheme" />
<MudDialogProvider /> <MudDialogProvider />
@ -53,24 +53,10 @@
isDarkMode = await LocalStorage.GetItemAsync<bool>("isDarkMode"); isDarkMode = await LocalStorage.GetItemAsync<bool>("isDarkMode");
} }
if (LoginService.LoginRequired) if (AuthService.LoginRequired)
{ {
// If not logged in, attempt to use JwtToken from local storage to log in // If not logged in, attempt to use JwtToken from local storage to log in
var hasJwtToken = await LocalStorage.ContainKeyAsync("authToken"); await AuthService.LoginWithAuthToken();
if (hasJwtToken)
{
var authToken = await LocalStorage.GetItemAsync<string>("authToken");
if (!string.IsNullOrWhiteSpace(authToken))
{
// Attempt to log in with the token
var loginResult = await LoginService.LoginWithAuthToken(authToken, Client);
if (!loginResult)
{
// Failed to log in with the token, remove it
await LocalStorage.RemoveItemAsync("authToken");
}
}
}
} }
} }

View File

@ -1,4 +1,4 @@
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IDialogService DialogService @inject IDialogService DialogService
@ -6,38 +6,38 @@
<MudNavMenu Rounded="true" Class="pa-2" Margin="Margin.Dense" Color="Color.Primary"> <MudNavMenu Rounded="true" Class="pa-2" Margin="Margin.Dense" Color="Color.Primary">
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["Dashboard"]</MudNavLink> <MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">@Localizer["Dashboard"]</MudNavLink>
@if (LoginService.IsAdmin || !LoginService.LoginRequired) @if (AuthService.IsAdmin || !AuthService.LoginRequired)
{ {
<MudNavLink Href="/Users" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.People">@Localizer["Users"]</MudNavLink> <MudNavLink Href="/Users" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.People">@Localizer["Users"]</MudNavLink>
} }
@{ @{
var currentUser = LoginService.GetLoggedInUser(); var baid = AuthService.GetLoggedInBaid();
if (LoginService.LoginRequired && !LoginService.OnlyAdmin && !LoginService.IsLoggedIn) { if (AuthService.LoginRequired && !AuthService.OnlyAdmin && !AuthService.IsLoggedIn) {
<MudDivider /> <MudDivider />
<MudNavLink Href="/Login" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Login">@Localizer["Log In"]</MudNavLink> <MudNavLink Href="/Login" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Login">@Localizer["Log In"]</MudNavLink>
<MudNavLink Href="/Register" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AddCard">@Localizer["Register"]</MudNavLink> <MudNavLink Href="/Register" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AddCard">@Localizer["Register"]</MudNavLink>
} }
if (LoginService.IsLoggedIn && currentUser != null) if (AuthService.IsLoggedIn)
{ {
<MudDivider /> <MudDivider />
<MudNavLink Href="@($"Users/{currentUser.Baid}/Profile")" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person">@Localizer["Profile"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/Profile")" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person">@Localizer["Profile"]</MudNavLink>
<MudNavGroup Title=@Localizer["Play Data"] Expanded="true" Icon="@Icons.Material.Filled.EmojiEvents"> <MudNavGroup Title=@Localizer["Play Data"] Expanded="true" Icon="@Icons.Material.Filled.EmojiEvents">
<MudNavLink Href="@($"Users/{currentUser.Baid}/Songs")" Match="NavLinkMatch.All">@Localizer["Song List"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/Songs")" Match="NavLinkMatch.All">@Localizer["Song List"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/HighScores")" Match="NavLinkMatch.All">@Localizer["High Scores"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/HighScores")" Match="NavLinkMatch.All">@Localizer["High Scores"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/PlayHistory")" Match="NavLinkMatch.All">@Localizer["Play History"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/PlayHistory")" Match="NavLinkMatch.All">@Localizer["Play History"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/DaniDojo")" Match="NavLinkMatch.All">@Localizer["Dani Dojo"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/DaniDojo")" Match="NavLinkMatch.All">@Localizer["Dani Dojo"]</MudNavLink>
</MudNavGroup> </MudNavGroup>
<MudNavGroup Title=@Localizer["Settings"] Expanded="_settingsOpen" Icon="@Icons.Material.Filled.Settings"> <MudNavGroup Title=@Localizer["Settings"] Expanded="settingsOpen" Icon="@Icons.Material.Filled.Settings">
<MudNavLink OnClick="ShowQrCode">@Localizer["Show QR Code"]</MudNavLink> <MudNavLink OnClick="ShowQrCode">@Localizer["Show QR Code"]</MudNavLink>
<MudNavLink Href="/ChangePassword" Match="NavLinkMatch.All">@Localizer["Change Password"]</MudNavLink> <MudNavLink Href="/ChangePassword" Match="NavLinkMatch.All">@Localizer["Change Password"]</MudNavLink>
<MudNavLink Href="@($"Users/{currentUser.Baid}/AccessCode")" Match="NavLinkMatch.All">@Localizer["Access Codes"]</MudNavLink> <MudNavLink Href="@($"Users/{baid}/AccessCode")" Match="NavLinkMatch.All">@Localizer["Access Codes"]</MudNavLink>
</MudNavGroup> </MudNavGroup>
} }
if (LoginService.IsLoggedIn) if (AuthService.IsLoggedIn)
{ {
<MudDivider /> <MudDivider />
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Logout" IconColor="Color.Error" OnClick="Logout">@Localizer["Log Out"]</MudNavLink> <MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Logout" IconColor="Color.Error" OnClick="Logout">@Localizer["Log Out"]</MudNavLink>
@ -46,14 +46,14 @@
</MudNavMenu> </MudNavMenu>
@code { @code {
private bool _settingsOpen = false; private bool settingsOpen = false;
protected override void OnInitialized() protected override void OnInitialized()
{ {
LoginService.LoginStatusChanged += HandleLoginStatusChanged; AuthService.LoginStatusChanged += HandleAuthStatusChanged;
} }
private void HandleLoginStatusChanged(object? sender, EventArgs e) private void HandleAuthStatusChanged(object? sender, EventArgs e)
{ {
StateHasChanged(); StateHasChanged();
} }
@ -62,22 +62,25 @@
{ {
if (firstRender) if (firstRender)
{ {
LoginService.LoginStatusChanged += HandleLoginStatusChanged; AuthService.LoginStatusChanged += HandleAuthStatusChanged;
} }
} }
private void ShowQrCode() private async Task ShowQrCode()
{ {
var user = await AuthService.GetLoggedInUser();
if (user == null) return;
var parameters = new DialogParameters var parameters = new DialogParameters
{ {
["user"] = LoginService.GetLoggedInUser() ["user"] = user
}; };
var options = new DialogOptions() { DisableBackdropClick = true }; var options = new DialogOptions { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>(@Localizer["QR Code"], parameters, options); await DialogService.ShowAsync<UserQrCodeDialog>(Localizer["QR Code"], parameters, options);
// Prevent the settings menu from closing // Prevent the settings menu from closing
_settingsOpen = true; settingsOpen = true;
} }
private async Task Logout() private async Task Logout()
@ -90,7 +93,7 @@
if (result == true) if (result == true)
{ {
LoginService.Logout(); await AuthService.Logout();
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
} }

View File

@ -17,52 +17,52 @@
<MudTable Items="Items" Elevation="0" Striped="true"> <MudTable Items="Items" Elevation="0" Striped="true">
<HeaderContent> <HeaderContent>
<MudTh> <MudTh>
<MudTableSortLabel InitialDirection="SortDirection.Descending" T="SongPlayDatumDto" SortBy="x => x.PlayTime"> <MudTableSortLabel InitialDirection="SortDirection.Descending" T="SongHistoryData" SortBy="x => x.PlayTime">
@Localizer["Play Time"] @Localizer["Play Time"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Difficulty"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.Difficulty">
@Localizer["Difficulty"] @Localizer["Difficulty"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Crown"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.Crown">
@Localizer["Crown"] @Localizer["Crown"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.ScoreRank"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.ScoreRank">
@Localizer["Rank"] @Localizer["Rank"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.Score"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.Score">
@Localizer["Score"] @Localizer["Score"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.GoodCount"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.GoodCount">
@Localizer["Good"] @Localizer["Good"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.OkCount"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.OkCount">
@Localizer["OK"] @Localizer["OK"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.MissCount"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.MissCount">
@Localizer["Bad"] @Localizer["Bad"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.DrumrollCount"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.DrumrollCount">
@Localizer["Drumroll"] @Localizer["Drumroll"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh> <MudTh>
<MudTableSortLabel T="SongPlayDatumDto" SortBy="x => x.ComboCount"> <MudTableSortLabel T="SongHistoryData" SortBy="x => x.ComboCount">
@Localizer["MAX Combo"] @Localizer["MAX Combo"]
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
@ -102,6 +102,6 @@
</MudCard> </MudCard>
@code { @code {
[Parameter] public List<SongPlayDatumDto> Items { get; set; } = new(); [Parameter] public List<SongHistoryData> Items { get; set; } = new();
private const string IconStyle = "width:25px; height:25px;"; private const string IconStyle = "width:25px; height:25px;";
} }

View File

@ -1,14 +1,13 @@
@using System.Text.Json @using System.Text.Json
@using Microsoft.AspNetCore.Components;
@using TaikoWebUI.Pages.Dialogs; @using TaikoWebUI.Pages.Dialogs;
@inject TaikoWebUI.Utilities.StringUtil StringUtil; @inject Utilities.StringUtil StringUtil;
@inject IDialogService DialogService; @inject IDialogService DialogService;
@inject LoginService LoginService; @inject AuthService AuthService;
@inject HttpClient Client @inject HttpClient Client
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (user is not null) @if (User is not null)
{ {
<MudCard Outlined="true"> <MudCard Outlined="true">
<MudCardHeader> <MudCardHeader>
@ -22,30 +21,30 @@
<MudSkeleton Width="35%" Height="32px" /> <MudSkeleton Width="35%" Height="32px" />
} }
@if (LoginService.LoginRequired && user?.IsAdmin == true) @if (AuthService.LoginRequired && User?.IsAdmin == true)
{ {
<MudChip Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.TwoTone.AdminPanelSettings">@Localizer["Admin"]</MudChip> <MudChip Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.TwoTone.AdminPanelSettings">@Localizer["Admin"]</MudChip>
} }
</div> </div>
<MudText Typo="Typo.caption">@Localizer["User"] BAID: @user?.Baid</MudText> <MudText Typo="Typo.caption">@Localizer["User"] BAID: @User?.Baid</MudText>
</CardHeaderContent> </CardHeaderContent>
<CardHeaderActions> <CardHeaderActions>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense="true" AnchorOrigin="Origin.BottomLeft" <MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense="true" AnchorOrigin="Origin.BottomLeft"
TransformOrigin="Origin.TopLeft" Size="Size.Small"> TransformOrigin="Origin.TopLeft" Size="Size.Small">
<MudMenuItem Icon="@Icons.Material.Filled.QrCode" <MudMenuItem Icon="@Icons.Material.Filled.QrCode"
OnClick="@(_ => ShowQrCode(user))" OnClick="@(_ => ShowQrCode(User))"
OnTouch="@(_ => ShowQrCode(user))" OnTouch="@(_ => ShowQrCode(User))"
IconColor="@Color.Primary"> IconColor="@Color.Primary">
@Localizer["Show QR Code"] @Localizer["Show QR Code"]
</MudMenuItem> </MudMenuItem>
<MudDivider /> <MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.FeaturedPlayList" <MudMenuItem Icon="@Icons.Material.Filled.FeaturedPlayList"
Href="@($"Users/{user.Baid}/AccessCode")" Href="@($"Users/{User.Baid}/AccessCode")"
IconColor="@Color.Primary"> IconColor="@Color.Primary">
@Localizer["Access Codes"] @Localizer["Access Codes"]
</MudMenuItem> </MudMenuItem>
<MudDivider /> <MudDivider />
@if (LoginService.OnlyAdmin || LoginService.LoginRequired) @if (AuthService.OnlyAdmin || AuthService.LoginRequired)
{ {
<MudMenuItem Icon="@Icons.Material.Filled.Lock" <MudMenuItem Icon="@Icons.Material.Filled.Lock"
Href="@($"/ChangePassword")" Href="@($"/ChangePassword")"
@ -54,31 +53,31 @@
</MudMenuItem> </MudMenuItem>
<MudDivider /> <MudDivider />
} }
@if (LoginService.LoginRequired && LoginService.IsAdmin) @if (AuthService.LoginRequired && AuthService.IsAdmin)
{ {
<MudMenuItem Icon="@Icons.Material.Filled.Password" <MudMenuItem Icon="@Icons.Material.Filled.Password"
OnClick="@(_ => GenerateInviteCode(user.Baid))" OnClick="@(_ => GenerateInviteCode(User.Baid))"
OnTouch="@(_ => GenerateInviteCode(user.Baid))" OnTouch="@(_ => GenerateInviteCode(User.Baid))"
IconColor="@Color.Primary"> IconColor="@Color.Primary">
@Localizer["Generate Invite Code"] @Localizer["Generate Invite Code"]
</MudMenuItem> </MudMenuItem>
<MudDivider /> <MudDivider />
} }
@if (LoginService.LoginRequired && LoginService.IsAdmin) @if (AuthService.LoginRequired && AuthService.IsAdmin)
{ {
<MudMenuItem Icon="@Icons.Material.Filled.LockReset" <MudMenuItem Icon="@Icons.Material.Filled.LockReset"
OnClick="@(_ => ResetPassword(user))" OnClick="@(_ => ResetPassword(User))"
OnTouch="@(_ => ResetPassword(user))" OnTouch="@(_ => ResetPassword(User))"
IconColor="@Color.Primary"> IconColor="@Color.Primary">
@Localizer["Unregister"] @Localizer["Unregister"]
</MudMenuItem> </MudMenuItem>
<MudDivider /> <MudDivider />
} }
@if (LoginService.AllowUserDelete) @if (AuthService.AllowUserDelete)
{ {
<MudMenuItem Icon="@Icons.Material.Filled.Delete" <MudMenuItem Icon="@Icons.Material.Filled.Delete"
OnClick="@(_ => DeleteUser(user))" OnClick="@(_ => DeleteUser(User))"
OnTouch="@(_ => DeleteUser(user))" OnTouch="@(_ => DeleteUser(User))"
IconColor="@Color.Error"> IconColor="@Color.Error">
@Localizer["Delete User"] @Localizer["Delete User"]
</MudMenuItem> </MudMenuItem>
@ -89,9 +88,9 @@
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.body2" Style="font-weight:bold">@Localizer["Access Code"]</MudText> <MudText Typo="Typo.body2" Style="font-weight:bold">@Localizer["Access Code"]</MudText>
<MudText Style="font-family:monospace;overflow:hidden;overflow-x:scroll"> <MudText Style="font-family:monospace;overflow:hidden;overflow-x:scroll">
@if (user.AccessCodes.Count > 0) @if (User.AccessCodes.Count > 0)
{ {
@foreach (var digitGroup in StringUtil.SplitIntoGroups(user.AccessCodes[0], 4)) @foreach (var digitGroup in StringUtil.SplitIntoGroups(User.AccessCodes[0], 4))
{ {
<span class="mr-2">@digitGroup</span> <span class="mr-2">@digitGroup</span>
} }
@ -100,9 +99,9 @@
<span class="mr-2">@Localizer["N/A"]</span> <span class="mr-2">@Localizer["N/A"]</span>
} }
</MudText> </MudText>
@if (user.AccessCodes.Count > 1) @if (User.AccessCodes.Count > 1)
{ {
<MudText Typo="Typo.caption">... @Localizer["and"] @(user.AccessCodes.Count - 1) @Localizer["other access code(s)"]</MudText> <MudText Typo="Typo.caption">... @Localizer["and"] @(User.AccessCodes.Count - 1) @Localizer["other access code(s)"]</MudText>
} }
else else
{ {
@ -112,7 +111,7 @@
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudStack Row="true" Style="width:100%" Spacing="4" Justify="Justify.FlexEnd"> <MudStack Row="true" Style="width:100%" Spacing="4" Justify="Justify.FlexEnd">
<MudButton Href="@($"Users/{user.Baid}/Profile")" <MudButton Href="@($"Users/{User.Baid}/Profile")"
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Edit" Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Edit"
Color="Color.Primary"> Color="Color.Primary">
@Localizer["edit profile"] @Localizer["edit profile"]
@ -126,10 +125,10 @@
FullWidth="true" FullWidth="true"
AnchorOrigin="Origin.BottomCenter" AnchorOrigin="Origin.BottomCenter"
TransformOrigin="Origin.TopCenter"> TransformOrigin="Origin.TopCenter">
<MudMenuItem Href="@($"Users/{user.Baid}/HighScores")">@Localizer["High Scores"]</MudMenuItem> <MudMenuItem Href="@($"Users/{User.Baid}/HighScores")">@Localizer["High Scores"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{user.Baid}/PlayHistory")">@Localizer["Play History"]</MudMenuItem> <MudMenuItem Href="@($"Users/{User.Baid}/PlayHistory")">@Localizer["Play History"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{user.Baid}/Songs")">@Localizer["Song List"]</MudMenuItem> <MudMenuItem Href="@($"Users/{User.Baid}/Songs")">@Localizer["Song List"]</MudMenuItem>
<MudMenuItem Href="@($"Users/{user.Baid}/DaniDojo")">@Localizer["Dani Dojo"]</MudMenuItem> <MudMenuItem Href="@($"Users/{User.Baid}/DaniDojo")">@Localizer["Dani Dojo"]</MudMenuItem>
</MudMenu> </MudMenu>
</MudStack> </MudStack>
</MudCardActions> </MudCardActions>
@ -137,14 +136,14 @@
} }
@code { @code {
[Parameter] public User? user { get; set; } [Parameter] public User? User { get; set; }
private DashboardResponse? response;
private UserSetting? userSetting; private UserSetting? userSetting;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ if (user is not null) {
if (User is not null)
{ {
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{user.Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{User.Baid}");
} }
} }
@ -155,7 +154,7 @@
["user"] = user ["user"] = user
}; };
var options = new DialogOptions() { DisableBackdropClick = true }; var options = new DialogOptions { DisableBackdropClick = true };
DialogService.Show<UserQrCodeDialog>(Localizer["QR Code"], parameters, options); DialogService.Show<UserQrCodeDialog>(Localizer["QR Code"], parameters, options);
return Task.CompletedTask; return Task.CompletedTask;
@ -163,8 +162,8 @@
private async Task ResetPassword(User user) private async Task ResetPassword(User user)
{ {
var options = new DialogOptions() { DisableBackdropClick = true }; var options = new DialogOptions { DisableBackdropClick = true };
if (LoginService.LoginRequired && !LoginService.IsAdmin) if (AuthService.LoginRequired && !AuthService.IsAdmin)
{ {
await DialogService.ShowMessageBox( await DialogService.ShowMessageBox(
Localizer["Error"], Localizer["Error"],
@ -177,18 +176,14 @@
["user"] = user ["user"] = user
}; };
var dialog = DialogService.Show<ResetPasswordConfirmDialog>(Localizer["Reset Password"], parameters, options); var dialog = await DialogService.ShowAsync<ResetPasswordConfirmDialog>(Localizer["Reset Password"], parameters, options);
var result = await dialog.Result; await dialog.Result;
if (result.Canceled) return;
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
} }
private async Task DeleteUser(User user) private async Task DeleteUser(User user)
{ {
var options = new DialogOptions() { DisableBackdropClick = true }; var options = new DialogOptions { DisableBackdropClick = true };
if (!LoginService.AllowUserDelete) if (!AuthService.AllowUserDelete)
{ {
await DialogService.ShowMessageBox( await DialogService.ShowMessageBox(
Localizer["Error"], Localizer["Error"],
@ -201,13 +196,16 @@
["user"] = user ["user"] = user
}; };
var dialog = DialogService.Show<UserDeleteConfirmDialog>(Localizer["Delete User"], parameters, options); var dialog = await DialogService.ShowAsync<UserDeleteConfirmDialog>(Localizer["Delete User"], parameters, options);
var result = await dialog.Result; var result = await dialog.Result;
if (result.Canceled) return; if (result.Canceled) return;
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard"); if (user.Baid == AuthService.GetLoggedInBaid())
LoginService.Logout(); {
await AuthService.Logout();
}
NavigationManager.NavigateTo("/Users"); NavigationManager.NavigateTo("/Users");
} }
@ -247,9 +245,7 @@
["otp"] = otp ["otp"] = otp
}; };
var options = new DialogOptions() { DisableBackdropClick = true }; var options = new DialogOptions { DisableBackdropClick = true };
DialogService.Show<OTPDialog>(Localizer["Invite Code"], parameters, options); await DialogService.ShowAsync<OTPDialog>(Localizer["Invite Code"], parameters, options);
} }
} }

View File

@ -1,94 +1,79 @@
@page "/Users/{baid:int}/AccessCode" @page "/Users/{baid:int}/AccessCode"
@inject HttpClient Client @inject HttpClient Client
@inject IDialogService DialogService @inject IDialogService DialogService
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject TaikoWebUI.Utilities.StringUtil StringUtil; @inject Utilities.StringUtil StringUtil;
@if (response is not null) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin))) {
{ NavigationManager.NavigateTo(!AuthService.IsLoggedIn ? "/Login" : "/");
@if ((LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) || User is null) } else if (User is null) {
{ // Loading ...
if (!LoginService.IsLoggedIn)
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
}
else
{
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">@Localizer["Access Codes"]</MudText>
<MudGrid Class="my-4 pb-10">
<MudItem xs="12">
<MudCard Outlined="true" Class="mb-6">
<MudCardContent>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudText Typo="Typo.h6">@Localizer["Add Access Code"]</MudText>
<MudForm @ref="bindAccessCodeForm">
<MudGrid Spacing="2" Class="mt-4">
<MudItem xs="12" md="10">
<MudTextField @bind-value="inputAccessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access Code is required" Variant="Variant.Outlined" Margin="Margin.Dense"
Label=@Localizer["New Access Code"] />
</MudItem>
<MudItem xs="12" md="2">
<MudButton OnClick="OnBind" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled" Class="mt-1">Add</MudButton>
</MudItem>
</MudGrid>
</MudForm>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Outlined="true">
<MudCardContent>
<MudGrid Spacing="3" Class="pb-2">
<MudItem xs="12">
<MudText Typo="Typo.h6">@Localizer["Access Code"]</MudText>
</MudItem>
@for (var idx = 0; idx < User.AccessCodes.Count; idx++)
{
var accessCode = User.AccessCodes[idx];
var localIdx = idx + 1;
<MudItem xs="12" Class="py-0">
<div Style="border-bottom:1px solid #eee; padding: 5px 0;">
<MudGrid Spacing="2" Class="d-flex align-center">
<MudItem xs="12" md="8" Class="d-flex align-center">
<pre class="mb-0" style="font-size:16px">
@foreach (var digitGroup in StringUtil.SplitIntoGroups(accessCode, 4))
{
<span class="mr-2">@digitGroup</span>
}
</pre>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton OnClick="@(_ => DeleteAccessCode(accessCode))"
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Delete"
Color="Color.Error">
@Localizer["Delete"]
</MudButton>
</MudItem>
</MudGrid>
</div>
</MudItem>
}
</MudGrid>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
}
}
else
{
<MudContainer Style="display:flex;margin:50px 0;align-items:center;justify-content:center;"> <MudContainer Style="display:flex;margin:50px 0;align-items:center;justify-content:center;">
<MudProgressCircular Indeterminate="true" Size="Size.Large" Color="Color.Primary" /> <MudProgressCircular Indeterminate="true" Size="Size.Large" Color="Color.Primary" />
</MudContainer> </MudContainer>
} else {
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-2"></MudBreadcrumbs>
<MudText Typo="Typo.h4">@Localizer["Access Codes"]</MudText>
<MudGrid Class="my-4 pb-10">
<MudItem xs="12">
<MudCard Outlined="true" Class="mb-6">
<MudCardContent>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudText Typo="Typo.h6">@Localizer["Add Access Code"]</MudText>
<MudForm @ref="bindAccessCodeForm">
<MudGrid Spacing="2" Class="mt-4">
<MudItem xs="12" md="10">
<MudTextField @bind-value="inputAccessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError="Access Code is required" Variant="Variant.Outlined" Margin="Margin.Dense"
Label=@Localizer["New Access Code"] />
</MudItem>
<MudItem xs="12" md="2">
<MudButton OnClick="OnBind" FullWidth="true" StartIcon="@Icons.Material.Filled.AddCard" Color="Color.Primary" Variant="Variant.Filled" Class="mt-1">@Localizer["Add"]</MudButton>
</MudItem>
</MudGrid>
</MudForm>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Outlined="true">
<MudCardContent>
<MudGrid Spacing="3" Class="pb-2">
<MudItem xs="12">
<MudText Typo="Typo.h6">@Localizer["Access Code"]</MudText>
</MudItem>
@for (var idx = 0; idx < User.AccessCodes.Count; idx++)
{
var accessCode = User.AccessCodes[idx];
var localIdx = idx + 1;
<MudItem xs="12" Class="py-0">
<div Style="border-bottom:1px solid #eee; padding: 5px 0;">
<MudGrid Spacing="2" Class="d-flex align-center">
<MudItem xs="12" md="8" Class="d-flex align-center">
<pre class="mb-0" style="font-size:16px">
@foreach (var digitGroup in StringUtil.SplitIntoGroups(accessCode, 4))
{
<span class="mr-2">@digitGroup</span>
}
</pre>
</MudItem>
<MudItem xs="12" md="4" Class="d-flex justify-end">
<MudButton OnClick="@(_ => DeleteAccessCode(accessCode))"
Size="Size.Small" Variant="Variant.Text" StartIcon="@Icons.Material.Filled.Delete"
Color="Color.Error">
@Localizer["Delete"]
</MudButton>
</MudItem>
</MudGrid>
</div>
</MudItem>
}
</MudGrid>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
} }

View File

@ -10,9 +10,8 @@ public partial class AccessCode
private string inputAccessCode = ""; private string inputAccessCode = "";
private MudForm bindAccessCodeForm = default!; private MudForm bindAccessCodeForm = default!;
private User? User { get; set; } = new(); private User? User { get; set; }
private DashboardResponse? response;
private UserSetting? userSetting; private UserSetting? userSetting;
private readonly List<BreadcrumbItem> breadcrumbs = new(); private readonly List<BreadcrumbItem> breadcrumbs = new();
@ -24,32 +23,28 @@ public partial class AccessCode
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
else else
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Users"], href: "/Users"));
}; }
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true)); breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Access Codes"], href: $"/Users/{Baid}/AccessCode", disabled: false)); breadcrumbs.Add(new BreadcrumbItem(Localizer["Access Codes"], href: $"/Users/{Baid}/AccessCode", disabled: false));
} }
private async Task InitializeUser() private async Task InitializeUser()
{ {
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard"); if (!AuthService.LoginRequired)
LoginService.ResetLoggedInUser(response);
if (LoginService.IsAdmin || !LoginService.LoginRequired)
{ {
if (response is not null) var users = await Client.GetFromJsonAsync<List<User>>("api/Users");
{ if (users != null) User = users.FirstOrDefault(u => u.Baid == Baid);
User = response.Users.FirstOrDefault(u => u.Baid == Baid);
}
} }
else if (LoginService.IsLoggedIn) else if (AuthService.IsLoggedIn)
{ {
User = LoginService.GetLoggedInUser(); User = await Client.GetFromJsonAsync<User>($"api/Users/{Baid}");
} }
} }
@ -61,7 +56,7 @@ public partial class AccessCode
{ x => x.AccessCode, accessCode } { x => x.AccessCode, accessCode }
}; };
var dialog = DialogService.Show<AccessCodeDeleteConfirmDialog>("Delete Access Code", parameters); var dialog = await DialogService.ShowAsync<AccessCodeDeleteConfirmDialog>("Delete Access Code", parameters);
var result = await dialog.Result; var result = await dialog.Result;
if (result.Canceled) return; if (result.Canceled) return;
@ -72,55 +67,53 @@ public partial class AccessCode
private async Task OnBind() private async Task OnBind()
{ {
if (response != null) if (User == null) return;
var result = await AuthService.BindAccessCode(inputAccessCode.ToUpper().Trim(), User);
switch (result)
{ {
var result = await LoginService.BindAccessCode(inputAccessCode.ToUpper().Trim(), response.Users.First(u => u.Baid == Baid), Client); case 0:
switch (result) await DialogService.ShowMessageBox(
{ "Error",
case 0: (MarkupString)
await DialogService.ShowMessageBox( "Not logged in.<br />Please log in first and try again.",
"Error", "Ok");
(MarkupString) break;
"Not logged in.<br />Please log in first and try again.", case 1:
"Ok"); await DialogService.ShowMessageBox(
break; "Success",
case 1: "New access code bound successfully.",
await DialogService.ShowMessageBox( "Ok");
"Success", await InitializeUser();
"New access code bound successfully.", NavigationManager.NavigateTo(NavigationManager.Uri);
"Ok"); break;
await InitializeUser(); case 2:
NavigationManager.NavigateTo(NavigationManager.Uri); await DialogService.ShowMessageBox(
break; "Error",
case 2: (MarkupString)
await DialogService.ShowMessageBox( "Bound access code upper limit reached.<br />Please delete one access code first.",
"Error", "Ok");
(MarkupString) break;
"Bound access code upper limit reached.<br />Please delete one access code first.", case 3:
"Ok"); await DialogService.ShowMessageBox(
break; "Error",
case 3: (MarkupString)
await DialogService.ShowMessageBox( "Access code already bound.<br />Please delete it from the bound user first.",
"Error", "Ok");
(MarkupString) break;
"Access code already bound.<br />Please delete it from the bound user first.", case 4:
"Ok"); await DialogService.ShowMessageBox(
break; "Error",
case 4: (MarkupString)
await DialogService.ShowMessageBox( "Access code cannot be empty.<br />Please enter a valid access code.",
"Error", "Ok");
(MarkupString) break;
"Access code cannot be empty.<br />Please enter a valid access code.", case 5:
"Ok"); await DialogService.ShowMessageBox(
break; "Error",
case 5: (MarkupString)
await DialogService.ShowMessageBox( "You can't do that!<br />You need to be an admin to edit someone else's access codes.",
"Error", "Ok");
(MarkupString) break;
"You can't do that!<br />You need to be an admin to edit someone else's access codes.",
"Ok");
break;
}
} }
} }
} }

View File

@ -1,17 +1,17 @@
@inject HttpClient Client @inject HttpClient Client
@inject IDialogService DialogService @inject IDialogService DialogService
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@page "/ChangePassword" @page "/ChangePassword"
@if (LoginService.OnlyAdmin || !LoginService.LoginRequired) @if (AuthService.OnlyAdmin || !AuthService.LoginRequired)
{ {
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
else else
{ {
if (LoginService.IsLoggedIn) if (AuthService.IsLoggedIn)
{ {
<MudContainer> <MudContainer>
<MudGrid Justify="Justify.Center"> <MudGrid Justify="Justify.Center">

View File

@ -8,70 +8,63 @@ public partial class ChangePassword
private string newPassword = ""; private string newPassword = "";
private string oldPassword = ""; private string oldPassword = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
} }
private async Task OnChangePassword() private async Task OnChangePassword()
{ {
if (response != null) var result = await AuthService.ChangePassword(cardNum, oldPassword, newPassword, confirmNewPassword);
switch (result)
{ {
var result = await LoginService.ChangePassword(cardNum, oldPassword, newPassword, confirmNewPassword, case 0:
response, Client); await DialogService.ShowMessageBox(
switch (result) Localizer["Error"],
{ "Only admin can log in.",
case 0: Localizer["Dialog OK"]);
await DialogService.ShowMessageBox( NavigationManager.NavigateTo("/Users");
Localizer["Error"], break;
"Only admin can log in.", case 1:
Localizer["Dialog OK"]); await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/Users"); Localizer["Success"],
break; "Password changed successfully.",
case 1: Localizer["Dialog OK"]);
await DialogService.ShowMessageBox( NavigationManager.NavigateTo("/Users");
Localizer["Success"], break;
"Password changed successfully.", case 2:
Localizer["Dialog OK"]); await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/Users"); Localizer["Error"],
break; "Confirm new password is not the same as new password.",
case 2: Localizer["Dialog OK"]);
await DialogService.ShowMessageBox( break;
Localizer["Error"], case 3:
"Confirm new password is not the same as new password.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"]); Localizer["Error"],
break; (MarkupString)
case 3: "Card number not found.<br />Please play one game with this card number to register it.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"]);
Localizer["Error"], break;
(MarkupString) case 4:
"Card number not found.<br />Please play one game with this card number to register it.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"]); Localizer["Error"],
break; (MarkupString)
case 4: "Old password is wrong!",
await DialogService.ShowMessageBox( Localizer["Dialog OK"]);
Localizer["Error"], break;
(MarkupString) case 5:
"Old password is wrong!", await DialogService.ShowMessageBox(
Localizer["Dialog OK"]); Localizer["Error"],
break; (MarkupString)
case 5: "Card number not registered.<br />Please use register button to create a password first.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"]);
Localizer["Error"], break;
(MarkupString) case 6:
"Card number not registered.<br />Please use register button to create a password first.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"]); Localizer["Error"],
break; Localizer["Unknown Error"],
case 6: Localizer["Dialog OK"]);
await DialogService.ShowMessageBox( break;
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"]);
break;
}
} }
} }
} }

View File

@ -1,14 +1,14 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
@inject IJSRuntime JSRuntime @inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@page "/Users/{baid:int}/DaniDojo" @page "/Users/{baid:int}/DaniDojo"
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
if (!LoginService.IsLoggedIn) if (!AuthService.IsLoggedIn)
{ {
NavigationManager.NavigateTo("/Login"); NavigationManager.NavigateTo("/Login");
} }
@ -370,7 +370,7 @@ else
var redRequirement = GetSongBorderCondition(border, songNumber, false); var redRequirement = GetSongBorderCondition(border, songNumber, false);
var goldRequirement = GetSongBorderCondition(border, songNumber, true); var goldRequirement = GetSongBorderCondition(border, songNumber, true);
var barClass = "bar-default"; var barClass = "bar-default";
var resultText = @Localizer["Not Cleared"]; var resultText = Localizer["Not Cleared"];
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudCard Outlined="true"> <MudCard Outlined="true">
@ -381,7 +381,7 @@ else
</MudCardHeader> </MudCardHeader>
<MudCardContent> <MudCardContent>
<MudText Typo="Typo.subtitle2" Style="font-weight:bold;">@Localizer["Result"]</MudText> <MudText Typo="Typo.subtitle2" Style="font-weight:bold;">@Localizer["Result"]</MudText>
@if (bestDataMap.TryGetValue(danId, out var danBestData)) @if (bestDataMap.TryGetValue(danId, out var danBestData) && (danBestData.DanBestStageDataList.Count > songNumber))
{ {
var bestData = GetSongBestFromData((DanConditionType)border.OdaiType, danBestData, songNumber); var bestData = GetSongBestFromData((DanConditionType)border.OdaiType, danBestData, songNumber);
if ((DanConditionType)border.OdaiType is DanConditionType.BadCount or DanConditionType.OkCount) if ((DanConditionType)border.OdaiType is DanConditionType.BadCount or DanConditionType.OkCount)
@ -389,14 +389,15 @@ else
if (bestData <= redRequirement) if (bestData <= redRequirement)
{ {
barClass = "bar-pass-red"; barClass = "bar-pass-red";
resultText = @Localizer["Pass"]; resultText = Localizer["Pass"];
} }
if (bestData <= goldRequirement) if (bestData <= goldRequirement)
{ {
barClass = "bar-pass-gold"; barClass = "bar-pass-gold";
resultText = @Localizer["Gold"]; resultText = Localizer["Gold"];
} }
var resultValue = redRequirement - bestData; var resultValue = redRequirement - bestData;
if (bestData >= redRequirement) resultValue = 0; if (bestData >= redRequirement) resultValue = 0;
@ -410,13 +411,13 @@ else
if (bestData >= redRequirement) if (bestData >= redRequirement)
{ {
barClass = "bar-pass-red"; barClass = "bar-pass-red";
resultText = @Localizer["Pass"]; resultText = Localizer["Pass"];
} }
if (bestData >= goldRequirement) if (bestData >= goldRequirement)
{ {
barClass = "bar-pass-gold"; barClass = "bar-pass-gold";
resultText = @Localizer["Gold"]; resultText = Localizer["Gold"];
} }
<MudProgressLinear Class="@barClass" Rounded="true" Size="Size.Large" Max="@(goldRequirement > 0 ? goldRequirement : 1)" Value="@(goldRequirement > 0 ? bestData : 1)"> <MudProgressLinear Class="@barClass" Rounded="true" Size="Size.Large" Max="@(goldRequirement > 0 ? goldRequirement : 1)" Value="@(goldRequirement > 0 ? bestData : 1)">
@ -426,7 +427,6 @@ else
<MudText Typo="Typo.caption">@resultText</MudText> <MudText Typo="Typo.caption">@resultText</MudText>
</MudStack> </MudStack>
} }
} }
else else
{ {

View File

@ -25,11 +25,11 @@ public partial class DaniDojo
.Sort((stageData, otherStageData) => stageData.SongNumber.CompareTo(otherStageData.SongNumber))); .Sort((stageData, otherStageData) => stageData.SongNumber.CompareTo(otherStageData.SongNumber)));
bestDataMap = response.DanBestDataList.ToDictionary(data => data.DanId); bestDataMap = response.DanBestDataList.ToDictionary(data => data.DanId);
CurrentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get"); CurrentLanguage = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
@ -98,6 +98,7 @@ public partial class DaniDojo
private static uint GetSongBestFromData(DanConditionType type, DanBestData data, int songNumber) private static uint GetSongBestFromData(DanConditionType type, DanBestData data, int songNumber)
{ {
songNumber.Throw().IfOutOfRange(0, 2); songNumber.Throw().IfOutOfRange(0, 2);
return type switch return type switch
{ {
DanConditionType.SoulGauge => throw new ArgumentException("Soul gauge should not be here"), DanConditionType.SoulGauge => throw new ArgumentException("Soul gauge should not be here"),

View File

@ -16,7 +16,7 @@
<MudText> <MudText>
<code> <code>
<pre> <pre>
@String.Format("{0:0000 0000 0000 0000 0000}", (Int64.Parse(AccessCode))) @AccessCode
</pre> </pre>
</code> </code>
</MudText> </MudText>

View File

@ -1,10 +1,12 @@
namespace TaikoWebUI.Pages.Dialogs; using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class AccessCodeDeleteConfirmDialog public partial class AccessCodeDeleteConfirmDialog
{ {
[CascadingParameter] [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter] [Parameter]
public User User { get; set; } = new(); public User User { get; set; } = new();
@ -12,6 +14,13 @@ public partial class AccessCodeDeleteConfirmDialog
[Parameter] [Parameter]
public string AccessCode { get; set; } = ""; public string AccessCode { get; set; } = "";
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel(); private void Cancel() => MudDialog.Cancel();
private async Task DeleteAccessCode() private async Task DeleteAccessCode()

View File

@ -65,6 +65,9 @@
[Parameter] [Parameter]
public bool AllowFreeProfileEditing { get; set; } public bool AllowFreeProfileEditing { get; set; }
[Parameter]
public List<uint> TitleUniqueIdList { get; set; } = new();
private IEnumerable<Title> titles = new List<Title>(); private IEnumerable<Title> titles = new List<Title>();
private Title? selectedTitle; private Title? selectedTitle;
@ -78,7 +81,12 @@
if (!AllowFreeProfileEditing) if (!AllowFreeProfileEditing)
{ {
var unlockedTitle = UserSetting.UnlockedTitle; var unlockedTitle = UserSetting.UnlockedTitle;
titleSet = titleSet.Where(title => unlockedTitle.Contains((uint)title.TitleId)).ToImmutableHashSet(); titleSet = titleSet.Where(title => unlockedTitle.Contains(title.TitleId)).ToImmutableHashSet();
}
else
{
// Only allow titles in titleUniqueIdList
titleSet = titleSet.Where(title => TitleUniqueIdList.Contains(title.TitleId)).ToImmutableHashSet();
} }
titles = titleSet.ToImmutableList().Sort((title, title1) => title.TitleId.CompareTo(title1.TitleId)); titles = titleSet.ToImmutableList().Sort((title, title1) => title.TitleId.CompareTo(title1.TitleId));
var currentTitle = new Title var currentTitle = new Title

View File

@ -1,4 +1,7 @@
namespace TaikoWebUI.Pages.Dialogs; using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class ResetPasswordConfirmDialog public partial class ResetPasswordConfirmDialog
{ {
@ -7,6 +10,12 @@ public partial class ResetPasswordConfirmDialog
[Parameter] [Parameter]
public User User { get; set; } = new(); public User User { get; set; } = new();
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel(); private void Cancel() => MudDialog.Cancel();
private async Task ResetPassword() private async Task ResetPassword()

View File

@ -1,14 +1,21 @@
namespace TaikoWebUI.Pages.Dialogs; using System.Net.Http.Headers;
using Blazored.LocalStorage;
namespace TaikoWebUI.Pages.Dialogs;
public partial class UserDeleteConfirmDialog public partial class UserDeleteConfirmDialog
{ {
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = null!;
[CascadingParameter]
MudDialogInstance MudDialog { get; set; } = null!;
[Parameter] [Parameter]
public User User { get; set; } = new(); public User User { get; set; } = new();
[Inject]
public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject]
public AuthService AuthService { get; set; } = null!;
private void Cancel() => MudDialog.Cancel(); private void Cancel() => MudDialog.Cancel();
private async Task DeleteUser() private async Task DeleteUser()

View File

@ -1,6 +1,4 @@
@inject IGameDataService GameDataService <MudDialog Class="dialog-user-qr-code">
<MudDialog Class="dialog-user-qr-code">
<DialogContent> <DialogContent>
<MudExtensions.MudBarcode Value="@qrCode" BarcodeFormat="ZXing.BarcodeFormat.QR_CODE" Height="300" Width="300" /> <MudExtensions.MudBarcode Value="@qrCode" BarcodeFormat="ZXing.BarcodeFormat.QR_CODE" Height="300" Width="300" />
</DialogContent> </DialogContent>

View File

@ -1,7 +1,7 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
@inject IJSRuntime JSRuntime @inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject Blazored.LocalStorage.ILocalStorageService LocalStorage @inject Blazored.LocalStorage.ILocalStorageService LocalStorage
@using TaikoWebUI.Utilities; @using TaikoWebUI.Utilities;
@ -20,9 +20,9 @@
} }
else else
{ {
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
if (!LoginService.IsLoggedIn) if (!AuthService.IsLoggedIn)
{ {
NavigationManager.NavigateTo("/Login"); NavigationManager.NavigateTo("/Login");
} }

View File

@ -2,6 +2,7 @@
using System; using System;
using Microsoft.JSInterop; using Microsoft.JSInterop;
namespace TaikoWebUI.Pages; namespace TaikoWebUI.Pages;
public partial class HighScores public partial class HighScores
@ -24,7 +25,7 @@ public partial class HighScores
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
var language = await JSRuntime.InvokeAsync<string>("blazorCulture.get"); var language = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
response.SongBestData.ForEach(data => response.SongBestData.ForEach(data =>
{ {
@ -43,12 +44,11 @@ public partial class HighScores
.CompareTo(GameDataService.GetMusicIndexBySongId(data2.SongId))); .CompareTo(GameDataService.GetMusicIndexBySongId(data2.SongId)));
} }
// Set last selected tab from local storage // Set last selected tab from local storage
selectedDifficultyTab = await localStorage.GetItemAsync<int>($"highScoresTab"); selectedDifficultyTab = await LocalStorage.GetItemAsync<int>($"highScoresTab");
// Breadcrumbs // Breadcrumbs
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
@ -78,6 +78,6 @@ public partial class HighScores
private async Task OnTabChanged(int index) private async Task OnTabChanged(int index)
{ {
selectedDifficultyTab = index; selectedDifficultyTab = index;
await localStorage.SetItemAsync($"highScoresTab", selectedDifficultyTab); await LocalStorage.SetItemAsync($"highScoresTab", selectedDifficultyTab);
} }
} }

View File

@ -1,12 +1,12 @@
@inject HttpClient Client @inject HttpClient Client
@inject IDialogService DialogService @inject IDialogService DialogService
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@page "/Login" @page "/Login"
@if (!LoginService.IsLoggedIn) @if (!AuthService.IsLoggedIn)
{ {
// Not logged in, show login form // Not logged in, show login form
<MudContainer> <MudContainer>

View File

@ -5,59 +5,54 @@ public partial class Login
private string inputAccessCode = ""; private string inputAccessCode = "";
private MudForm loginForm = default!; private MudForm loginForm = default!;
private string inputPassword = ""; private string inputPassword = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
} }
private async Task OnLogin() private async Task OnLogin()
{ {
if (response != null) var result = await AuthService.Login(inputAccessCode, inputPassword);
var options = new DialogOptions { DisableBackdropClick = true };
switch (result)
{ {
var result = await LoginService.Login(inputAccessCode, inputPassword, Client); case 0:
var options = new DialogOptions { DisableBackdropClick = true }; await DialogService.ShowMessageBox(
switch (result) Localizer["Error"],
{ "Only admin can log in.",
case 0: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( await loginForm.ResetAsync();
Localizer["Error"], break;
"Only admin can log in.", case 1:
Localizer["Dialog OK"], null, null, options); NavigationManager.NavigateTo("/Users");
await loginForm.ResetAsync(); break;
break; case 2:
case 1: await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/Users"); Localizer["Error"],
break; "Wrong password!",
case 2: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( break;
Localizer["Error"], case 3:
"Wrong password!", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; (MarkupString)
case 3: "Access code not found.<br />Please play one game with this access code to register it.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"], null, null, options);
Localizer["Error"], break;
(MarkupString) case 4:
"Access code not found.<br />Please play one game with this access code to register it.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; (MarkupString)
case 4: "Access code not registered.<br />Please use register button to create a password first.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"], null, null, options);
Localizer["Error"], break;
(MarkupString) case 5:
"Access code not registered.<br />Please use register button to create a password first.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; Localizer["Unknown Error"],
case 5: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( break;
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, options);
break;
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@using TaikoWebUI.Utilities; @using TaikoWebUI.Utilities;
@ -21,7 +21,7 @@
} }
else else
{ {
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
<MudItem xs="12"> <MudItem xs="12">
<MudText Align="Align.Center" Class="my-8"> <MudText Align="Align.Center" Class="my-8">
@ -135,7 +135,7 @@
</MudTable> </MudTable>
</ChildRowContent> </ChildRowContent>
<PagerContent> <PagerContent>
<MudTablePager RowsPerPageString=@Localizer["Rows Per Page"] /> <MudTablePager RowsPerPageString=@Localizer["Rows Per Page:"] />
</PagerContent> </PagerContent>
</MudTable> </MudTable>
</MudItem> </MudItem>

View File

@ -16,7 +16,7 @@ public partial class PlayHistory
private string Search { get; set; } = string.Empty; private string Search { get; set; } = string.Empty;
private string? CurrentLanguage; private string? currentLanguage;
private SongHistoryResponse? response; private SongHistoryResponse? response;
@ -31,14 +31,14 @@ public partial class PlayHistory
response = await Client.GetFromJsonAsync<SongHistoryResponse>($"api/PlayHistory/{(uint)Baid}"); response = await Client.GetFromJsonAsync<SongHistoryResponse>($"api/PlayHistory/{(uint)Baid}");
response.ThrowIfNull(); response.ThrowIfNull();
CurrentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get"); currentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get");
response.SongHistoryData.ForEach(data => response.SongHistoryData.ForEach(data =>
{ {
var songId = data.SongId; var songId = data.SongId;
data.Genre = GameDataService.GetMusicGenreBySongId(songId); data.Genre = GameDataService.GetMusicGenreBySongId(songId);
data.MusicName = GameDataService.GetMusicNameBySongId(songId, string.IsNullOrEmpty(CurrentLanguage) ? "ja" : CurrentLanguage); data.MusicName = GameDataService.GetMusicNameBySongId(songId, string.IsNullOrEmpty(currentLanguage) ? "ja" : currentLanguage);
data.MusicArtist = GameDataService.GetMusicArtistBySongId(songId, string.IsNullOrEmpty(CurrentLanguage) ? "ja" : CurrentLanguage); data.MusicArtist = GameDataService.GetMusicArtistBySongId(songId, string.IsNullOrEmpty(currentLanguage) ? "ja" : currentLanguage);
data.Stars = GameDataService.GetMusicStarLevel(songId, data.Difficulty); data.Stars = GameDataService.GetMusicStarLevel(songId, data.Difficulty);
data.ShowDetails = false; data.ShowDetails = false;
}); });
@ -134,7 +134,7 @@ public partial class PlayHistory
return true; return true;
} }
var language = CurrentLanguage ?? "ja"; var language = currentLanguage ?? "ja";
if (songHistoryDataList[0].PlayTime if (songHistoryDataList[0].PlayTime
.ToString("dddd d MMMM yyyy - HH:mm", CultureInfo.CreateSpecificCulture(language)) .ToString("dddd d MMMM yyyy - HH:mm", CultureInfo.CreateSpecificCulture(language))

View File

@ -2,15 +2,15 @@
@inject HttpClient Client @inject HttpClient Client
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject IDialogService DialogService @inject IDialogService DialogService
@inject LoginService LoginService @inject AuthService AuthService
@inject IJSRuntime Js @inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (response is not null) @if (response is not null)
{ {
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
if (!LoginService.IsLoggedIn) if (!AuthService.IsLoggedIn)
{ {
NavigationManager.NavigateTo("/Login"); NavigationManager.NavigateTo("/Login");
} }
@ -47,7 +47,7 @@
<MudGrid> <MudGrid>
<MudItem xs="12" md="8"> <MudItem xs="12" md="8">
@if (LoginService.AllowFreeProfileEditing) @if (AuthService.AllowFreeProfileEditing)
{ {
<MudTextField TextChanged="UpdateTitle" @bind-Value="@response.Title" Label=@Localizer["Title"]/> <MudTextField TextChanged="UpdateTitle" @bind-Value="@response.Title" Label=@Localizer["Title"]/>
} }
@ -55,18 +55,17 @@
{ {
<MudTextField ReadOnly="true" @bind-Value="@response.Title" Label=@Localizer["Title"]/> <MudTextField ReadOnly="true" @bind-Value="@response.Title" Label=@Localizer["Title"]/>
} }
<MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@((_) => OpenChooseTitleDialog())"> <MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@(_ => OpenChooseTitleDialog())">
@Localizer["Select a Title"] @Localizer["Select a Title"]
</MudButton> </MudButton>
</MudItem> </MudItem>
@if (LoginService.AllowFreeProfileEditing) @if (AuthService.AllowFreeProfileEditing)
{ {
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudSelect @bind-Value="@response.TitlePlateId" Label=@Localizer["Title Plate"]> <MudSelect @bind-Value="@response.TitlePlateId" Label=@Localizer["Title Plate"]>
@for (uint i = 0; i < TitlePlateStrings.Length; i++) @foreach (var index in titlePlateIdList)
{ {
var index = i; <MudSelectItem Value="@index">@TitlePlateStrings[index]</MudSelectItem>
<MudSelectItem Value="@i">@TitlePlateStrings[index]</MudSelectItem>
} }
</MudSelect> </MudSelect>
</MudItem> </MudItem>
@ -402,17 +401,17 @@ else
@code { @code {
private async Task UpdateMyDonName() private async Task UpdateMyDonName()
{ {
@if (response is not null) await Js.InvokeVoidAsync("updateMyDonNameText", response.MyDonName); @if (response is not null) await JsRuntime.InvokeVoidAsync("updateMyDonNameText", response.MyDonName);
} }
private async Task UpdateTitle() private async Task UpdateTitle()
{ {
@if (response is not null) await Js.InvokeVoidAsync("updateTitleText", response.Title); @if (response is not null) await JsRuntime.InvokeVoidAsync("updateTitleText", response.Title);
} }
private async Task UpdateScoreboard(Difficulty difficulty) private async Task UpdateScoreboard(Difficulty difficulty)
{ {
UpdateScores(difficulty); UpdateScores(difficulty);
await Js.InvokeVoidAsync("updateScoreboardText", scoresArray); await JsRuntime.InvokeVoidAsync("updateScoreboardText", scoresArray);
} }
} }

View File

@ -187,7 +187,7 @@ public partial class Profile
response = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); response = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
response.ThrowIfNull(); response.ThrowIfNull();
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
@ -238,7 +238,7 @@ public partial class Profile
var unlockedFace = response != null ? response.UnlockedFace : new List<uint>(); var unlockedFace = response != null ? response.UnlockedFace : new List<uint>();
var unlockedPuchi = response != null ? response.UnlockedPuchi : new List<uint>(); var unlockedPuchi = response != null ? response.UnlockedPuchi : new List<uint>();
if (LoginService.AllowFreeProfileEditing) if (AuthService.AllowFreeProfileEditing)
{ {
kigurumiUniqueIdList = GameDataService.GetKigurumiUniqueIdList(); kigurumiUniqueIdList = GameDataService.GetKigurumiUniqueIdList();
headUniqueIdList = GameDataService.GetHeadUniqueIdList(); headUniqueIdList = GameDataService.GetHeadUniqueIdList();
@ -272,7 +272,7 @@ public partial class Profile
private void InitializeAvailableTitlePlates() private void InitializeAvailableTitlePlates()
{ {
titlePlateIdList = GameDataService.GetTitlePlateIdList().Except(GameDataService.GetLockedTitlePlateIdList()).ToList(); titlePlateIdList = GameDataService.GetTitlePlateIdList().ToList();
// Cut off ids longer than TitlePlateStrings // Cut off ids longer than TitlePlateStrings
titlePlateIdList = titlePlateIdList.Where(id => id < TitlePlateStrings.Length).Except(GameDataService.GetLockedTitlePlateIdList()).ToList(); titlePlateIdList = titlePlateIdList.Where(id => id < TitlePlateStrings.Length).Except(GameDataService.GetLockedTitlePlateIdList()).ToList();
} }
@ -283,15 +283,18 @@ public partial class Profile
var unlockedTitle = response != null ? response.UnlockedTitle : new List<uint>(); var unlockedTitle = response != null ? response.UnlockedTitle : new List<uint>();
if (LoginService.AllowFreeProfileEditing) if (AuthService.AllowFreeProfileEditing)
{ {
titleUniqueIdList = GameDataService.GetTitleUniqueIdList(); titleUniqueIdList = GameDataService.GetTitleUniqueIdList();
var titles = GameDataService.GetTitles(); var titles = GameDataService.GetTitles();
// Lock titles in LockedTitlesList but not in UnlockedTitle // Lock titles in LockedTitlesList but not in UnlockedTitle
var lockedTitleUniqueIdList = GameDataService.GetLockedTitleUniqueIdList().Except(unlockedTitle).ToList(); var lockedTitleUniqueIdList = GameDataService.GetLockedTitleUniqueIdList().ToList();
// Lock titles with rarity not in titlePlateIdList and not in unlockedTitle var lockedTitlePlateIdList = GameDataService.GetLockedTitlePlateIdList().ToList();
lockedTitleUniqueIdList.AddRange(titles.Where(title => !titlePlateIdList.Contains(title.TitleRarity) && !unlockedTitle.Contains(title.TitleId)).Select(title => title.TitleId)); // Unlock titles in UnlockedTitlesList
lockedTitleUniqueIdList = lockedTitleUniqueIdList.Except(unlockedTitle).ToList();
// Find uniqueIds of titles with rarity in lockedTitlePlateIdList
lockedTitleUniqueIdList.AddRange(titles.Where(title => lockedTitlePlateIdList.Contains(title.TitleRarity)).Select(title => title.TitleId));
titleUniqueIdList = titleUniqueIdList.Except(lockedTitleUniqueIdList).ToList(); titleUniqueIdList = titleUniqueIdList.Except(lockedTitleUniqueIdList).ToList();
} }
else else
@ -387,7 +390,8 @@ public partial class Profile
var parameters = new DialogParameters<ChooseTitleDialog> var parameters = new DialogParameters<ChooseTitleDialog>
{ {
{x => x.UserSetting, response}, {x => x.UserSetting, response},
{x => x.AllowFreeProfileEditing, LoginService.AllowFreeProfileEditing} {x => x.AllowFreeProfileEditing, AuthService.AllowFreeProfileEditing},
{x => x.TitleUniqueIdList, titleUniqueIdList}
}; };
var dialog = DialogService.Show<ChooseTitleDialog>("Player Titles", parameters, options); var dialog = DialogService.Show<ChooseTitleDialog>("Player Titles", parameters, options);
var result = await dialog.Result; var result = await dialog.Result;

View File

@ -1,16 +1,16 @@
@inject HttpClient Client @inject HttpClient Client
@inject IDialogService DialogService @inject IDialogService DialogService
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@page "/Register" @page "/Register"
@if (LoginService.OnlyAdmin || !LoginService.LoginRequired) @if (AuthService.OnlyAdmin || !AuthService.LoginRequired)
{ {
Console.WriteLine("Registration is disabled. Redirecting to Dashboard..."); Console.WriteLine("Registration is disabled. Redirecting to Dashboard...");
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
else if (LoginService.IsLoggedIn) else if (AuthService.IsLoggedIn)
{ {
// User is already logged in. Redirect to dashboard. // User is already logged in. Redirect to dashboard.
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
@ -30,7 +30,7 @@ else
<MudTextField @bind-value="accessCode" InputType="InputType.Text" T="string" <MudTextField @bind-value="accessCode" InputType="InputType.Text" T="string"
FullWidth="true" Required="@true" RequiredError=@Localizer["Access Code is required"] FullWidth="true" Required="@true" RequiredError=@Localizer["Access Code is required"]
Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" /> Label=@Localizer["Access Code"] Variant="Variant.Outlined" Margin="Margin.Dense" />
@if (LoginService.RegisterWithLastPlayTime) @if (AuthService.RegisterWithLastPlayTime)
{ {
<MudTextField @bind-value="inviteCode" InputType="InputType.Text" T="string" <MudTextField @bind-value="inviteCode" InputType="InputType.Text" T="string"
FullWidth="true" Label=@Localizer["Invite Code (Optional)"]/> FullWidth="true" Label=@Localizer["Invite Code (Optional)"]/>

View File

@ -13,72 +13,66 @@ public partial class Register
private TimeSpan? time = new TimeSpan(00, 45, 00); private TimeSpan? time = new TimeSpan(00, 45, 00);
private string inviteCode = ""; private string inviteCode = "";
private DashboardResponse? response;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard");
} }
private async Task OnRegister() private async Task OnRegister()
{ {
var inputDateTime = date!.Value.Date + time!.Value; var inputDateTime = date!.Value.Date + time!.Value;
if (response != null) var result = await AuthService.Register(accessCode, inputDateTime, password, confirmPassword, inviteCode);
var options = new DialogOptions { DisableBackdropClick = true };
switch (result)
{ {
var result = await LoginService.Register(accessCode, inputDateTime, password, confirmPassword, response, Client, inviteCode); case 0:
var options = new DialogOptions { DisableBackdropClick = true }; await DialogService.ShowMessageBox(
switch (result) Localizer["Error"],
{ "Only admin can register.",
case 0: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( NavigationManager.NavigateTo("/");
Localizer["Error"], break;
"Only admin can register.", case 1:
Localizer["Dialog OK"], null, null, options); await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/"); Localizer["Success"],
break; "Access code registered successfully.",
case 1: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( NavigationManager.NavigateTo("/Login");
Localizer["Success"], break;
"Access code registered successfully.", case 2:
Localizer["Dialog OK"], null, null, options); await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/Login"); Localizer["Error"],
break; "Confirm password is not the same as password.",
case 2: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( break;
Localizer["Error"], case 3:
"Confirm password is not the same as password.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; (MarkupString)
case 3: "Access code not found.<br />Please play one game with this access code to register it.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"], null, null, options);
Localizer["Error"], break;
(MarkupString) case 4:
"Access code not found.<br />Please play one game with this access code to register it.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; (MarkupString)
case 4: "Access code is already registered, please use set password to login.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"], null, null, options);
Localizer["Error"], NavigationManager.NavigateTo("/Login");
(MarkupString) break;
"Access code is already registered, please use set password to login.", case 5:
Localizer["Dialog OK"], null, null, options); await DialogService.ShowMessageBox(
NavigationManager.NavigateTo("/Login"); Localizer["Error"],
break; (MarkupString)
case 5: "Wrong last play time.<br />If you have forgotten when you last played, please play another game with this access code.",
await DialogService.ShowMessageBox( Localizer["Dialog OK"], null, null, options);
Localizer["Error"], break;
(MarkupString) case 6:
"Wrong last play time.<br />If you have forgotten when you last played, please play another game with this access code.", await DialogService.ShowMessageBox(
Localizer["Dialog OK"], null, null, options); Localizer["Error"],
break; Localizer["Unknown Error"],
case 6: Localizer["Dialog OK"], null, null, options);
await DialogService.ShowMessageBox( break;
Localizer["Error"],
Localizer["Unknown Error"],
Localizer["Dialog OK"], null, null, options);
break;
}
} }
} }
} }

View File

@ -2,34 +2,26 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime @inject IJSRuntime JsRuntime
@using TaikoWebUI.Utilities;
@using TaikoWebUI.Components.Song; @using TaikoWebUI.Components.Song;
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
if (!LoginService.IsLoggedIn) NavigationManager.NavigateTo(AuthService.IsLoggedIn ? "/" : "/Login");
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
} }
else else
{ {
if (response is not null) if (response is not null)
{ {
<MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-3"></MudBreadcrumbs> <MudBreadcrumbs Items="breadcrumbs" Class="p-0 mb-3"></MudBreadcrumbs>
<MudText Typo="Typo.h5">@SongTitle</MudText> <MudText Typo="Typo.h5">@songTitle</MudText>
<MudText Typo="Typo.body2">@SongArtist</MudText> <MudText Typo="Typo.body2">@songArtist</MudText>
<MudGrid Class="my-4 pb-10"> <MudGrid Class="my-4 pb-10">
<MudItem xs="12"> <MudItem xs="12">
<PlayHistoryCard Items="@SongBestData?.RecentPlayData" /> <PlayHistoryCard Items="@songHistoryData" />
</MudItem> </MudItem>
</MudGrid> </MudGrid>
} }

View File

@ -11,37 +11,38 @@ namespace TaikoWebUI.Pages
public int Baid { get; set; } public int Baid { get; set; }
private UserSetting? userSetting; private UserSetting? userSetting;
private SongBestResponse? response; private SongHistoryResponse? response;
private SongBestData? SongBestData; private List<SongHistoryData>? songHistoryData;
private List<BreadcrumbItem> breadcrumbs = new List<BreadcrumbItem>(); private readonly List<BreadcrumbItem> breadcrumbs = new();
private string SongTitle = string.Empty; private string songTitle = string.Empty;
private string SongArtist = string.Empty; private string songArtist = string.Empty;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<SongBestResponse>($"api/PlayData/{Baid}"); response = await Client.GetFromJsonAsync<SongHistoryResponse>($"api/PlayHistory/{(uint)Baid}");
response.ThrowIfNull(); response.ThrowIfNull();
SongBestData = response.SongBestData.FirstOrDefault(x => x.SongId == SongId); // Get all song best data with SongId
songHistoryData = response.SongHistoryData.Where(data => data.SongId == (uint)SongId).ToList();
// Get user settings // Get user settings
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
// Get song title and artist // Get song title and artist
var language = await JSRuntime.InvokeAsync<string>("blazorCulture.get"); var language = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
SongTitle = GameDataService.GetMusicNameBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language); songTitle = GameDataService.GetMusicNameBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
SongArtist = GameDataService.GetMusicArtistBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language); songArtist = GameDataService.GetMusicArtistBySongId((uint)SongId, string.IsNullOrEmpty(language) ? "ja" : language);
// Breadcrumbs // Breadcrumbs
var _songTitle = SongTitle; var formattedSongTitle = songTitle;
if (_songTitle.Length > 20) if (formattedSongTitle.Length > 20)
{ {
_songTitle = _songTitle.Substring(0, 20) + "..."; formattedSongTitle = string.Concat(formattedSongTitle.AsSpan(0, 20), "...");
} }
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
@ -51,7 +52,7 @@ namespace TaikoWebUI.Pages
}; };
breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true)); breadcrumbs.Add(new BreadcrumbItem($"{userSetting?.MyDonName}", href: null, disabled: true));
breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false)); breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
breadcrumbs.Add(new BreadcrumbItem(_songTitle, href: $"/Users/{Baid}/Songs/{SongId}", disabled: false)); breadcrumbs.Add(new BreadcrumbItem(formattedSongTitle, href: $"/Users/{Baid}/Songs/{SongId}", disabled: false));
} }
} }
} }

View File

@ -1,11 +1,10 @@
@inject IGameDataService GameDataService @inject IGameDataService GameDataService
@inject HttpClient Client @inject HttpClient Client
@inject LoginService LoginService @inject AuthService AuthService
@inject IJSRuntime JSRuntime @inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@using TaikoWebUI.Utilities; @using TaikoWebUI.Utilities;
@using TaikoWebUI.Shared.Models; @using TaikoWebUI.Shared.Models;
@using SharedProject.Enums;
@page "/Users/{baid:int}/Songs" @page "/Users/{baid:int}/Songs"
@ -21,16 +20,9 @@
} }
else else
{ {
@if (LoginService.LoginRequired && (!LoginService.IsLoggedIn || (LoginService.GetLoggedInUser().Baid != Baid && !LoginService.IsAdmin))) @if (AuthService.LoginRequired && (!AuthService.IsLoggedIn || (AuthService.GetLoggedInBaid() != Baid && !AuthService.IsAdmin)))
{ {
if (!LoginService.IsLoggedIn) NavigationManager.NavigateTo(AuthService.IsLoggedIn ? "/" : "/Login");
{
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
} }
else else
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.JSInterop; using System.Reflection.Emit;
using Microsoft.JSInterop;
using TaikoWebUI.Shared.Models; using TaikoWebUI.Shared.Models;
@ -9,8 +10,6 @@ public partial class SongList
[Parameter] [Parameter]
public int Baid { get; set; } public int Baid { get; set; }
private const string IconStyle = "width:25px; height:25px;";
private string Search { get; set; } = string.Empty; private string Search { get; set; } = string.Empty;
private string GenreFilter { get; set; } = string.Empty; private string GenreFilter { get; set; } = string.Empty;
private string CurrentLanguage { get; set; } = "ja"; private string CurrentLanguage { get; set; } = "ja";
@ -18,8 +17,6 @@ public partial class SongList
private SongBestResponse? response; private SongBestResponse? response;
private UserSetting? userSetting; private UserSetting? userSetting;
private Dictionary<Difficulty, List<SongBestData>> songBestDataMap = new();
private readonly List<BreadcrumbItem> breadcrumbs = new(); private readonly List<BreadcrumbItem> breadcrumbs = new();
private List<MusicDetail> musicMap = new(); private List<MusicDetail> musicMap = new();
@ -33,9 +30,9 @@ public partial class SongList
userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}"); userSetting = await Client.GetFromJsonAsync<UserSetting>($"api/UserSettings/{Baid}");
musicMap = GameDataService.GetMusicList(); musicMap = GameDataService.GetMusicList();
CurrentLanguage = await JSRuntime.InvokeAsync<string>("blazorCulture.get"); CurrentLanguage = await JsRuntime.InvokeAsync<string>("blazorCulture.get");
if (LoginService.IsLoggedIn && !LoginService.IsAdmin) if (AuthService.IsLoggedIn && !AuthService.IsAdmin)
{ {
breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/")); breadcrumbs.Add(new BreadcrumbItem(Localizer["Dashboard"], href: "/"));
} }
@ -47,21 +44,6 @@ public partial class SongList
breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false)); breadcrumbs.Add(new BreadcrumbItem(Localizer["Song List"], href: $"/Users/{Baid}/Songs", disabled: false));
} }
private async Task OnFavoriteToggled(SongBestData data)
{
var request = new SetFavoriteRequest
{
Baid = (uint)Baid,
IsFavorite = !data.IsFavorite,
SongId = data.SongId
};
var result = await Client.PostAsJsonAsync("api/FavoriteSongs", request);
if (result.IsSuccessStatusCode)
{
data.IsFavorite = !data.IsFavorite;
}
}
private bool FilterSongs(MusicDetail musicDetail) private bool FilterSongs(MusicDetail musicDetail)
{ {
var stringsToCheck = new List<string> var stringsToCheck = new List<string>

View File

@ -1,6 +1,5 @@
@inject HttpClient Client @inject HttpClient Client
@inject IDialogService DialogService @inject AuthService AuthService
@inject LoginService LoginService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@using TaikoWebUI.Components @using TaikoWebUI.Components
@ -9,61 +8,44 @@
<MudText Typo="Typo.h4">@Localizer["Users"]</MudText> <MudText Typo="Typo.h4">@Localizer["Users"]</MudText>
<MudGrid Class="my-8"> <MudGrid Class="my-8">
@if (response is not null) @if (!AuthService.LoginRequired || (AuthService.LoginRequired && AuthService.IsAdmin)) {
{ if (users == null) {
// Response received and users are available // Loading...
if (response.Users.Count != 0) for (uint i = 0; i < 6; i++) {
{ <MudItem xs="12" md="6" lg="4">
if (LoginService.IsAdmin || !LoginService.LoginRequired) // Admin mode, can see all users <MudCard Outlined="true">
{ <MudCardContent>
@foreach (var user in response.Users) <MudSkeleton Width="30%" Height="42px;" Class="mb-5" />
{ <MudSkeleton Width="80%" />
<MudItem xs="12" md="6" lg="4"> <MudSkeleton Width="100%" />
<UserCard user="user" /> </MudCardContent>
</MudItem> <MudCardActions>
} <MudStack Row="true" Style="width:100%" Spacing="4" Justify="Justify.FlexEnd">
<MudSkeleton Width="64px" Height="40px" />
<MudSkeleton Width="64px" Height="40px" />
</MudStack>
</MudCardActions>
</MudCard>
</MudItem>
} }
else } else if (users.Count > 0) {
foreach (var user in users)
{ {
// Not admin, redirect <MudItem xs="12" md="6" lg="4">
@if (!LoginService.IsLoggedIn) // Not logged in, show login form <UserCard User="user" />
{ </MudItem>
NavigationManager.NavigateTo("/Login");
}
else
{
NavigationManager.NavigateTo("/");
}
} }
} } else { // No users in the database
else
{ // No users in the database
<MudItem xs="12"> <MudItem xs="12">
<MudText Align="Align.Center" Class="my-8"> <MudText Align="Align.Center" Class="my-8">
@Localizer["No data."] @Localizer["No data."]
</MudText> </MudText>
</MudItem> </MudItem>
} }
} else } else if (AuthService.LoginRequired && !AuthService.IsLoggedIn) {
{ // Not logged in, redirect
// Loading... NavigationManager.NavigateTo("/Login");
@for (uint i = 0; i < 6; i++) } else {
{ NavigationManager.NavigateTo("/");
<MudItem xs="12" md="6" lg="4">
<MudCard Outlined="true">
<MudCardContent>
<MudSkeleton Width="30%" Height="42px;" Class="mb-5" />
<MudSkeleton Width="80%" />
<MudSkeleton Width="100%" />
</MudCardContent>
<MudCardActions>
<MudStack Row="true" Style="width:100%" Spacing="4" Justify="Justify.FlexEnd">
<MudSkeleton Width="64px" Height="40px" />
<MudSkeleton Width="64px" Height="40px" />
</MudStack>
</MudCardActions>
</MudCard>
</MudItem>
}
} }
</MudGrid> </MudGrid>

View File

@ -2,11 +2,14 @@
public partial class Users public partial class Users
{ {
private DashboardResponse? response; private List<User>? users;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await base.OnInitializedAsync(); await base.OnInitializedAsync();
response = await Client.GetFromJsonAsync<DashboardResponse>("api/Dashboard"); if (AuthService.IsAdmin || !AuthService.LoginRequired)
{
users = await Client.GetFromJsonAsync<List<User>>("api/Users");
}
} }
} }

View File

@ -19,7 +19,7 @@ builder.Services.AddSingleton<IGameDataService, GameDataService>();
builder.Services.Configure<WebUiSettings>(builder.Configuration.GetSection(nameof(WebUiSettings))); builder.Services.Configure<WebUiSettings>(builder.Configuration.GetSection(nameof(WebUiSettings)));
builder.Services.AddScoped<LoginService>(); builder.Services.AddScoped<AuthService>();
builder.Services.AddLocalization(); builder.Services.AddLocalization();
builder.Services.AddSingleton<MudLocalizer, ResXMudLocalizer>(); builder.Services.AddSingleton<MudLocalizer, ResXMudLocalizer>();
builder.Services.AddSingleton<ScoreUtils>(); builder.Services.AddSingleton<ScoreUtils>();

View File

@ -1,4 +1,6 @@
using System.IdentityModel.Tokens.Jwt; using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -7,10 +9,9 @@ using Blazored.LocalStorage;
namespace TaikoWebUI.Services; namespace TaikoWebUI.Services;
public class LoginService public sealed class AuthService
{ {
public event EventHandler? LoginStatusChanged; public event EventHandler? LoginStatusChanged;
public delegate void LoginStatusChangedEventHandler(object? sender, EventArgs e);
public bool LoginRequired { get; } public bool LoginRequired { get; }
public bool OnlyAdmin { get; } public bool OnlyAdmin { get; }
private readonly int boundAccessCodeUpperLimit; private readonly int boundAccessCodeUpperLimit;
@ -18,11 +19,12 @@ public class LoginService
public bool AllowUserDelete { get; } public bool AllowUserDelete { get; }
public bool AllowFreeProfileEditing { get; } public bool AllowFreeProfileEditing { get; }
public bool IsLoggedIn { get; private set; } public bool IsLoggedIn { get; private set; }
private User LoggedInUser { get; set; } = new(); private uint LoggedInBaid { get; set; }
public bool IsAdmin { get; private set; } public bool IsAdmin { get; private set; }
private readonly ILocalStorageService localStorage; private readonly ILocalStorageService localStorage;
private readonly HttpClient client;
public LoginService(IOptions<WebUiSettings> settings, ILocalStorageService localStorage) public AuthService(IOptions<WebUiSettings> settings, ILocalStorageService localStorage, HttpClient client)
{ {
this.localStorage = localStorage; this.localStorage = localStorage;
IsLoggedIn = false; IsLoggedIn = false;
@ -34,14 +36,24 @@ public class LoginService
RegisterWithLastPlayTime = webUiSettings.RegisterWithLastPlayTime; RegisterWithLastPlayTime = webUiSettings.RegisterWithLastPlayTime;
AllowUserDelete = webUiSettings.AllowUserDelete; AllowUserDelete = webUiSettings.AllowUserDelete;
AllowFreeProfileEditing = webUiSettings.AllowFreeProfileEditing; AllowFreeProfileEditing = webUiSettings.AllowFreeProfileEditing;
this.client = client;
} }
protected virtual void OnLoginStatusChanged() private void OnLoginStatusChanged()
{ {
LoginStatusChanged?.Invoke(this, EventArgs.Empty); LoginStatusChanged?.Invoke(this, EventArgs.Empty);
} }
public async Task<int> Login(string inputAccessCode, string inputPassword, HttpClient client) private static (uint, bool) GetBaidAndIsAdminFromToken(string authToken)
{
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(authToken);
var baid = uint.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value);
var isAdmin = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Role).Value == "Admin";
return (baid, isAdmin);
}
public async Task<int> Login(string inputAccessCode, string inputPassword)
{ {
// strip spaces or dashes from card number // strip spaces or dashes from card number
inputAccessCode = inputAccessCode.Replace(" ", "").Replace("-", "").Replace(":", ""); inputAccessCode = inputAccessCode.Replace(" ", "").Replace("-", "").Replace(":", "");
@ -79,37 +91,45 @@ public class LoginService
var authToken = responseJson["authToken"]; var authToken = responseJson["authToken"];
await localStorage.SetItemAsync("authToken", authToken); await localStorage.SetItemAsync("authToken", authToken);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
return await LoginWithAuthToken(authToken, client) == false ? 5 : 1; var (baid, isAdmin) = GetBaidAndIsAdminFromToken(authToken);
IsLoggedIn = true;
IsAdmin = isAdmin;
LoggedInBaid = baid;
OnLoginStatusChanged();
return 1;
} }
} }
public async Task<bool> LoginWithAuthToken(string authToken, HttpClient client) public async Task LoginWithAuthToken()
{ {
var handler = new JwtSecurityTokenHandler(); var hasAuthToken = await localStorage.ContainKeyAsync("authToken");
var jwtSecurityToken = handler.ReadJwtToken(authToken); if (!hasAuthToken) return;
// Check whether token is expired // Attempt to get JWT token from local storage
if (jwtSecurityToken.ValidTo < DateTime.UtcNow) return false; var authToken = await localStorage.GetItemAsync<string>("authToken");
if (authToken == null) return;
var baid = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
var isAdmin = jwtSecurityToken.Claims.First(claim => claim.Type == ClaimTypes.Role).Value == "Admin"; var responseMessage = await client.PostAsync("api/Auth/LoginWithToken", null);
if (!responseMessage.IsSuccessStatusCode)
{
// Clear JWT token
await localStorage.RemoveItemAsync("authToken");
return;
}
var response = await client.GetFromJsonAsync<DashboardResponse>("api/Dashboard"); var (baid, isAdmin) = GetBaidAndIsAdminFromToken(authToken);
var user = response?.Users.FirstOrDefault(u => u.Baid == uint.Parse(baid));
if (user is null) return false;
IsLoggedIn = true; IsLoggedIn = true;
IsAdmin = isAdmin; IsAdmin = isAdmin;
LoggedInUser = user; LoggedInBaid = baid;
OnLoginStatusChanged(); OnLoginStatusChanged();
return true;
} }
public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword, public async Task<int> Register(string inputCardNum, DateTime inputDateTime, string inputPassword,
string inputConfirmPassword, string inputConfirmPassword, string inviteCode)
DashboardResponse response, HttpClient client, string inviteCode)
{ {
if (OnlyAdmin) return 0; if (OnlyAdmin) return 0;
@ -147,7 +167,7 @@ public class LoginService
} }
public async Task<int> ChangePassword(string inputAccessCode, string inputOldPassword, string inputNewPassword, public async Task<int> ChangePassword(string inputAccessCode, string inputOldPassword, string inputNewPassword,
string inputConfirmNewPassword, DashboardResponse response, HttpClient client) string inputConfirmNewPassword)
{ {
if (OnlyAdmin) return 0; if (OnlyAdmin) return 0;
@ -182,7 +202,7 @@ public class LoginService
public async Task Logout() public async Task Logout()
{ {
IsLoggedIn = false; IsLoggedIn = false;
LoggedInUser = new User(); LoggedInBaid = 0;
IsAdmin = false; IsAdmin = false;
// Clear JWT token // Clear JWT token
@ -190,32 +210,31 @@ public class LoginService
OnLoginStatusChanged(); OnLoginStatusChanged();
} }
public User GetLoggedInUser() public async Task<User?> GetLoggedInUser()
{ {
return LoggedInUser; return await client.GetFromJsonAsync<User>($"api/Users/{LoggedInBaid}");
} }
public void ResetLoggedInUser(DashboardResponse? response) public uint GetLoggedInBaid()
{ {
if (response is null) return; return LoggedInBaid;
var baid = LoggedInUser.Baid;
var newLoggedInUser = response.Users.FirstOrDefault(u => u.Baid == baid);
if (newLoggedInUser is null) return;
LoggedInUser = newLoggedInUser;
} }
public async Task<int> BindAccessCode(string inputAccessCode, User user, HttpClient client) public async Task<int> BindAccessCode(string inputAccessCode, User user)
{ {
if (inputAccessCode.Trim() == "") return 4; /*Empty access code*/ if (inputAccessCode.Trim() == "") return 4; /*Empty access code*/
if (!IsLoggedIn && LoginRequired) return 0; /*User not connected and login is required*/ if (!IsLoggedIn && LoginRequired) return 0; /*User not connected and login is required*/
if (LoginRequired && !IsAdmin && user.Baid != GetLoggedInUser().Baid) return 5; /*User not admin trying to update someone elses Access Codes*/ var loggedInUser = await GetLoggedInUser();
if (loggedInUser == null) return 0;
if (LoginRequired && !IsAdmin && user.Baid != loggedInUser.Baid) return 5; /*User not admin trying to update someone else's Access Codes*/
if (user.AccessCodes.Count >= boundAccessCodeUpperLimit) return 2; /*Limit of codes has been reached*/ if (user.AccessCodes.Count >= boundAccessCodeUpperLimit) return 2; /*Limit of codes has been reached*/
var request = new BindAccessCodeRequest var request = new BindAccessCodeRequest
{ {
AccessCode = inputAccessCode, AccessCode = inputAccessCode,
Baid = user.Baid Baid = user.Baid
}; };
var responseMessage = await client.PostAsJsonAsync("api/Cards", request); var responseMessage = await client.PostAsJsonAsync("api/Cards/BindAccessCode", request);
return responseMessage.IsSuccessStatusCode ? 1 : 3; return responseMessage.IsSuccessStatusCode ? 1 : 3;
} }
} }

View File

@ -234,6 +234,11 @@ public class GameDataService : IGameDataService
.Select(entry => entry.Rarity) .Select(entry => entry.Rarity)
.FirstOrDefault(); .FirstOrDefault();
if (!titlePlateIdList.Contains(titleRarity))
{
titlePlateIdList.Add(titleRarity);
}
set.Add(new Title set.Add(new Title
{ {
TitleName = titleWordlistItem.JapaneseText, TitleName = titleWordlistItem.JapaneseText,

View File

@ -68,6 +68,12 @@
<_ContentIncludedByDefault Remove="Pages\Pages\Profile.razor" /> <_ContentIncludedByDefault Remove="Pages\Pages\Profile.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Register.razor" /> <_ContentIncludedByDefault Remove="Pages\Pages\Register.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" /> <_ContentIncludedByDefault Remove="Pages\Pages\Users.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\OTPDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Dialogs\ResetPasswordConfirmDialog.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Login.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\PlayHistory.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\Song.razor" />
<_ContentIncludedByDefault Remove="Pages\Pages\SongList.razor" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -105,6 +111,15 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Pages\Pages\AccessCode.razor" />
<UpToDateCheckInput Remove="Pages\Pages\ChangePassword.razor" />
<UpToDateCheckInput Remove="Pages\Pages\DaniDojo.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dashboard.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dialogs\AccessCodeDeleteConfirmDialog.razor" />
<UpToDateCheckInput Remove="Pages\Pages\Dialogs\ChooseTitleDialog.razor" />
</ItemGroup>
</Project> </Project>