Starting updating mooncore dependency usage
This commit is contained in:
@@ -29,34 +29,18 @@ public class AppConfiguration
|
|||||||
|
|
||||||
public class AuthenticationConfig
|
public class AuthenticationConfig
|
||||||
{
|
{
|
||||||
public string AccessSecret { get; set; } = Formatter.GenerateString(32);
|
public string Secret { get; set; } = Formatter.GenerateString(32);
|
||||||
public string RefreshSecret { get; set; } = Formatter.GenerateString(32);
|
public int TokenDuration { get; set; } = 3600;
|
||||||
public int AccessDuration { get; set; } = 60;
|
|
||||||
public int RefreshDuration { get; set; } = 3600;
|
|
||||||
|
|
||||||
public OAuth2Data OAuth2 { get; set; } = new();
|
public OAuth2Data OAuth2 { get; set; } = new();
|
||||||
public bool UseLocalOAuth2 { get; set; } = true;
|
|
||||||
|
|
||||||
public LocalOAuth2Data LocalOAuth2 { get; set; } = new();
|
|
||||||
|
|
||||||
public class LocalOAuth2Data
|
|
||||||
{
|
|
||||||
public string AccessSecret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
public string RefreshSecret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
public string CodeSecret { get; set; } = Formatter.GenerateString(32);
|
|
||||||
|
|
||||||
public int AccessTokenDuration { get; set; } = 60;
|
|
||||||
public int RefreshTokenDuration { get; set; } = 3600;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OAuth2Data
|
public class OAuth2Data
|
||||||
{
|
{
|
||||||
|
public string Secret { get; set; } = Formatter.GenerateString(32);
|
||||||
public string ClientId { get; set; } = Formatter.GenerateString(8);
|
public string ClientId { get; set; } = Formatter.GenerateString(8);
|
||||||
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
|
public string ClientSecret { get; set; } = Formatter.GenerateString(32);
|
||||||
public string? AuthorizationUri { get; set; }
|
public string? AuthorizationEndpoint { get; set; }
|
||||||
public string? AuthorizationRedirect { get; set; }
|
public string? AuthorizationRedirect { get; set; }
|
||||||
public string? AccessEndpoint { get; set; }
|
|
||||||
public string? RefreshEndpoint { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using MoonCore.Extended.OAuth2.Consumer;
|
namespace Moonlight.ApiServer.Database.Entities;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Database.Entities;
|
public class User
|
||||||
|
|
||||||
public class User : IUserModel
|
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
@@ -10,10 +8,6 @@ public class User : IUserModel
|
|||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
|
||||||
public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow;
|
public DateTime TokenValidTimestamp { get; set; } = DateTime.MinValue;
|
||||||
public string PermissionsJson { get; set; } = "[]";
|
public string PermissionsJson { get; set; } = "[]";
|
||||||
|
|
||||||
public string AccessToken { get; set; } = "";
|
|
||||||
public string RefreshToken { get; set; } = "";
|
|
||||||
public DateTime RefreshTimestamp { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MoonCore.Attributes;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
|
using MoonCore.Extended.PermFilter;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.Models;
|
using MoonCore.Models;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using MoonCore.Attributes;
|
using System.Text;
|
||||||
using MoonCore.Authentication;
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.Shared.Http.Requests.Auth;
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
using Moonlight.Shared.Http.Responses.OAuth2;
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||||
|
|
||||||
@@ -10,206 +20,138 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
|||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
private readonly OAuth2Service OAuth2Service;
|
|
||||||
private readonly TokenHelper TokenHelper;
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
private readonly ILogger<AuthController> Logger;
|
|
||||||
private readonly AppConfiguration Configuration;
|
private readonly AppConfiguration Configuration;
|
||||||
private readonly IOAuth2Provider[] OAuth2Providers;
|
private readonly ILogger<AuthController> Logger;
|
||||||
private readonly IAuthInterceptor[] AuthInterceptors;
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
|
|
||||||
public AuthController(
|
public AuthController(
|
||||||
OAuth2Service oAuth2Service,
|
AppConfiguration configuration,
|
||||||
TokenHelper tokenHelper,
|
|
||||||
DatabaseRepository<User> userRepository,
|
|
||||||
ILogger<AuthController> logger,
|
ILogger<AuthController> logger,
|
||||||
IOAuth2Provider[] oAuth2Providers,
|
DatabaseRepository<User> userRepository
|
||||||
IAuthInterceptor[] authInterceptors,
|
)
|
||||||
AppConfiguration configuration)
|
|
||||||
{
|
{
|
||||||
OAuth2Service = oAuth2Service;
|
|
||||||
TokenHelper = tokenHelper;
|
|
||||||
UserRepository = userRepository;
|
|
||||||
Logger = logger;
|
|
||||||
OAuth2Providers = oAuth2Providers;
|
|
||||||
AuthInterceptors = authInterceptors;
|
|
||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
|
Logger = logger;
|
||||||
|
UserRepository = userRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[AllowAnonymous]
|
||||||
public async Task<OAuth2StartResponse> Start()
|
[HttpGet("start")]
|
||||||
|
public Task<LoginStartResponse> Start()
|
||||||
{
|
{
|
||||||
var data = await OAuth2Service.StartAuthorizing();
|
var response = new LoginStartResponse()
|
||||||
|
|
||||||
return Mapper.Map<OAuth2StartResponse>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<OAuth2HandleResponse> Handle([FromBody] OAuth2HandleRequest request)
|
|
||||||
{
|
|
||||||
var accessData = await OAuth2Service.RequestAccess(request.Code);
|
|
||||||
|
|
||||||
// Find oauth2 provider
|
|
||||||
var provider = OAuth2Providers.FirstOrDefault();
|
|
||||||
|
|
||||||
if (provider == null)
|
|
||||||
throw new HttpApiException("No oauth2 provider has been registered", 500);
|
|
||||||
|
|
||||||
// Sync user from oauth2 provider
|
|
||||||
var user = await provider.Sync(HttpContext.RequestServices, accessData.AccessToken);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new HttpApiException("The oauth2 provider was unable to authenticate you", 401);
|
|
||||||
|
|
||||||
// Allow plugins to intercept access calls
|
|
||||||
if (AuthInterceptors.Any(interceptor => !interceptor.AllowAccess(user, HttpContext.RequestServices)))
|
|
||||||
throw new HttpApiException("Unable to get access token", 401);
|
|
||||||
|
|
||||||
// Save oauth2 refresh and access tokens for later use (re-authentication etc.).
|
|
||||||
// Fetch user model in current db context, just in case the oauth2 provider
|
|
||||||
// uses a different db context or smth
|
|
||||||
|
|
||||||
var userModel = UserRepository
|
|
||||||
.Get()
|
|
||||||
.First(x => x.Id == user.Id);
|
|
||||||
|
|
||||||
userModel.AccessToken = accessData.AccessToken;
|
|
||||||
userModel.RefreshToken = accessData.RefreshToken;
|
|
||||||
userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
|
|
||||||
|
|
||||||
UserRepository.Update(userModel);
|
|
||||||
|
|
||||||
// Generate local token-pair for the authentication
|
|
||||||
// between client and the api server
|
|
||||||
|
|
||||||
var authConfig = Configuration.Authentication;
|
|
||||||
|
|
||||||
var tokenPair = TokenHelper.GeneratePair(
|
|
||||||
authConfig.AccessSecret,
|
|
||||||
authConfig.RefreshSecret,
|
|
||||||
data => { data.Add("userId", user.Id); },
|
|
||||||
authConfig.AccessDuration,
|
|
||||||
authConfig.RefreshDuration
|
|
||||||
);
|
|
||||||
|
|
||||||
// Authentication finished. Return data to client
|
|
||||||
|
|
||||||
return new OAuth2HandleResponse()
|
|
||||||
{
|
{
|
||||||
AccessToken = tokenPair.AccessToken,
|
ClientId = Configuration.Authentication.OAuth2.ClientId,
|
||||||
RefreshToken = tokenPair.RefreshToken,
|
RedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl,
|
||||||
ExpiresAt = DateTime.UtcNow.AddSeconds(authConfig.AccessDuration)
|
Endpoint = Configuration.Authentication.OAuth2.AuthorizationEndpoint ?? Configuration.PublicUrl + "/oauth2/authorize"
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("refresh")]
|
|
||||||
public async Task<RefreshResponse> Refresh([FromBody] RefreshRequest request)
|
|
||||||
{
|
|
||||||
var authConfig = Configuration.Authentication;
|
|
||||||
|
|
||||||
var tokenPair = TokenHelper.RefreshPair(
|
|
||||||
request.RefreshToken,
|
|
||||||
authConfig.AccessSecret,
|
|
||||||
authConfig.RefreshSecret,
|
|
||||||
(refreshData, newData)
|
|
||||||
=> ProcessRefreshData(refreshData, newData, HttpContext.RequestServices),
|
|
||||||
authConfig.AccessDuration,
|
|
||||||
authConfig.RefreshDuration
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle refresh error
|
|
||||||
if (!tokenPair.HasValue)
|
|
||||||
throw new HttpApiException("Unable to refresh token", 401);
|
|
||||||
|
|
||||||
// Return data
|
|
||||||
return new RefreshResponse()
|
|
||||||
{
|
|
||||||
AccessToken = tokenPair.Value.AccessToken,
|
|
||||||
RefreshToken = tokenPair.Value.RefreshToken,
|
|
||||||
ExpiresAt = DateTime.UtcNow.AddSeconds(authConfig.AccessDuration)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ProcessRefreshData(Dictionary<string, JsonElement> refreshTokenData, Dictionary<string, object> newData, IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
// Find oauth2 provider
|
|
||||||
var provider = OAuth2Providers.FirstOrDefault();
|
|
||||||
|
|
||||||
if (provider == null)
|
|
||||||
throw new HttpApiException("No oauth2 provider has been registered", 500);
|
|
||||||
|
|
||||||
// Check if the userId is present in the refresh token
|
|
||||||
if (!refreshTokenData.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Load user from database if existent
|
|
||||||
var user = UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefault(x => x.Id == userId);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Allow plugins to intercept the refresh call
|
|
||||||
if (AuthInterceptors.Any(interceptor => !interceptor.AllowRefresh(user, serviceProvider)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if it's time to resync with the oauth2 provider
|
|
||||||
if (DateTime.UtcNow >= user.RefreshTimestamp)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// It's time to refresh the access to the external oauth2 provider
|
|
||||||
var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result;
|
|
||||||
|
|
||||||
// Sync user with oauth2 provider
|
|
||||||
var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken).Result;
|
|
||||||
|
|
||||||
if (syncedUser == null) // User sync has failed. No refresh allowed
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Save oauth2 refresh and access tokens for later use (re-authentication etc.).
|
|
||||||
// Fetch user model in current db context, just in case the oauth2 provider
|
|
||||||
// uses a different db context or smth
|
|
||||||
|
|
||||||
var userModel = UserRepository
|
|
||||||
.Get()
|
|
||||||
.First(x => x.Id == syncedUser.Id);
|
|
||||||
|
|
||||||
userModel.AccessToken = refreshData.AccessToken;
|
|
||||||
userModel.RefreshToken = refreshData.RefreshToken;
|
|
||||||
userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(refreshData.ExpiresIn);
|
|
||||||
|
|
||||||
UserRepository.Update(userModel);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// We are handling this error more softly, because it will occur when a user hasn't logged in a long period of time
|
|
||||||
Logger.LogDebug("An error occured while refreshing external oauth2 access: {e}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All checks have passed, allow refresh
|
|
||||||
newData.Add("userId", user.Id);
|
|
||||||
return true;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
[HttpGet("check")]
|
|
||||||
[RequirePermission("meta.authenticated")]
|
|
||||||
public Task<CheckResponse> Check()
|
|
||||||
{
|
|
||||||
var permClaim = (HttpContext.User as PermClaimsPrinciple)!;
|
|
||||||
var user = (User)permClaim.IdentityModel;
|
|
||||||
|
|
||||||
var response = new CheckResponse()
|
|
||||||
{
|
|
||||||
Email = user.Email,
|
|
||||||
Username = user.Username,
|
|
||||||
Permissions = permClaim.Permissions
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return Task.FromResult(response);
|
return Task.FromResult(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("complete")]
|
||||||
|
public async Task<LoginCompleteResponse> Complete([FromBody] LoginCompleteRequest request)
|
||||||
|
{
|
||||||
|
// TODO: Make modular
|
||||||
|
|
||||||
|
// Create http client to call the auth provider
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
httpClient.BaseAddress = new Uri(Configuration.PublicUrl);
|
||||||
|
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {Configuration.Authentication.OAuth2.ClientSecret}");
|
||||||
|
|
||||||
|
var httpApiClient = new HttpApiClient(httpClient);
|
||||||
|
|
||||||
|
// Call the auth provider
|
||||||
|
OAuth2HandleResponse handleData;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
handleData = await httpApiClient.PostJson<OAuth2HandleResponse>("oauth2/handle", new FormUrlEncodedContent(
|
||||||
|
[
|
||||||
|
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||||
|
new KeyValuePair<string, string>("code", request.Code),
|
||||||
|
new KeyValuePair<string, string>("redirect_uri", Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl),
|
||||||
|
new KeyValuePair<string, string>("client_id", Configuration.Authentication.OAuth2.ClientId)
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (HttpApiException e)
|
||||||
|
{
|
||||||
|
if (e.Status == 400)
|
||||||
|
Logger.LogTrace("The auth server returned an error: {e}", e);
|
||||||
|
else
|
||||||
|
Logger.LogCritical("The auth server returned an error: {e}", e);
|
||||||
|
|
||||||
|
throw new HttpApiException("Unable to request user data", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the returned data
|
||||||
|
var userId = handleData.UserId;
|
||||||
|
|
||||||
|
var user = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
throw new HttpApiException("Unable to load user data", 500);
|
||||||
|
|
||||||
|
//
|
||||||
|
var permissions = JsonSerializer.Deserialize<string[]>(user.PermissionsJson) ?? [];
|
||||||
|
|
||||||
|
// Generate token
|
||||||
|
var securityTokenDescriptor = new SecurityTokenDescriptor()
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddDays(10),
|
||||||
|
IssuedAt = DateTime.Now,
|
||||||
|
NotBefore = DateTime.Now.AddMinutes(-1),
|
||||||
|
Claims = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"userId",
|
||||||
|
user.Id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"permissions",
|
||||||
|
string.Join(";", permissions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SigningCredentials = new SigningCredentials(
|
||||||
|
new SymmetricSecurityKey(
|
||||||
|
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
|
||||||
|
),
|
||||||
|
SecurityAlgorithms.HmacSha256
|
||||||
|
),
|
||||||
|
Issuer = Configuration.PublicUrl,
|
||||||
|
Audience = Configuration.PublicUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
|
||||||
|
|
||||||
|
var jwt = jwtSecurityTokenHandler.WriteToken(securityToken);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
AccessToken = jwt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("check")]
|
||||||
|
public async Task<CheckResponse> Check()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.Claims.First(x => x.Type == "userId");
|
||||||
|
var userId = int.Parse(userIdClaim.Value);
|
||||||
|
var user = await UserRepository.Get().FirstAsync(x => x.Id == userId);
|
||||||
|
|
||||||
|
var permissions = JsonSerializer.Deserialize<string[]>(user.PermissionsJson) ?? [];
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Email = user.Email,
|
||||||
|
Username = user.Username,
|
||||||
|
Permissions = string.Join(";", permissions)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ public class FrontendController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("frontend.json")]
|
[HttpGet("frontend.json")]
|
||||||
public async Task<FrontendConfiguration> GetConfiguration()
|
public Task<FrontendConfiguration> GetConfiguration()
|
||||||
{
|
{
|
||||||
var configuration = new FrontendConfiguration()
|
var configuration = new FrontendConfiguration()
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ public class FrontendController : Controller
|
|||||||
|
|
||||||
configuration.Scripts = AssetService.GetJavascriptAssets();
|
configuration.Scripts = AssetService.GetJavascriptAssets();
|
||||||
|
|
||||||
return configuration;
|
return Task.FromResult(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("plugins/{assemblyName}")] // TODO: Test this
|
[HttpGet("plugins/{assemblyName}")] // TODO: Test this
|
||||||
|
|||||||
67
Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor
Normal file
67
Moonlight.ApiServer/Http/Controllers/OAuth2/Login.razor
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<html lang="en" class="h-full bg-gray-900">
|
||||||
|
<head>
|
||||||
|
<title>Login into your account</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full">
|
||||||
|
|
||||||
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<img class="mx-auto h-14 w-auto" src="https://help.moonlightpanel.xyz/images/logo.svg" alt="Your Company">
|
||||||
|
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-100">Login into your account</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
|
<div class="bg-gray-800 px-6 py-12 shadow sm:rounded-lg sm:px-12">
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="rounded-lg bg-red-500 p-5 text-center text-white mb-8">
|
||||||
|
@ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form class="space-y-6" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium leading-6 text-gray-100">Email address</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="email" name="email" type="email" autocomplete="email" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-100">Password</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="password" name="password" type="password" autocomplete="current-password" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-5 text-center text-sm text-gray-500">
|
||||||
|
No account?
|
||||||
|
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=register" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">
|
||||||
|
Register
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public string ClientId { get; set; }
|
||||||
|
[Parameter] public string RedirectUri { get; set; }
|
||||||
|
[Parameter] public string ResponseType { get; set; }
|
||||||
|
[Parameter] public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
290
Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs
Normal file
290
Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Extended.Abstractions;
|
||||||
|
using MoonCore.Extended.Helpers;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.ApiServer.Configuration;
|
||||||
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
|
using Moonlight.Shared.Http.Responses.OAuth2;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("oauth2")]
|
||||||
|
public class OAuth2Controller : Controller
|
||||||
|
{
|
||||||
|
private readonly AppConfiguration Configuration;
|
||||||
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
|
|
||||||
|
public OAuth2Controller(AppConfiguration configuration, DatabaseRepository<User> userRepository)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
UserRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("authorize")]
|
||||||
|
public async Task Authorize(
|
||||||
|
[FromQuery(Name = "client_id")] string clientId,
|
||||||
|
[FromQuery(Name = "redirect_uri")] string redirectUri,
|
||||||
|
[FromQuery(Name = "response_type")] string responseType,
|
||||||
|
[FromQuery(Name = "view")] string view = "login"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var requiredRedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl;
|
||||||
|
|
||||||
|
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
|
||||||
|
requiredRedirectUri != redirectUri ||
|
||||||
|
responseType != "code")
|
||||||
|
{
|
||||||
|
throw new HttpApiException("Invalid oauth2 request", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.StatusCode = 200;
|
||||||
|
|
||||||
|
if (view == "register")
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("ClientId", clientId);
|
||||||
|
parameters.Add("RedirectUri", redirectUri);
|
||||||
|
parameters.Add("ResponseType", responseType);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Response.WriteAsync(html);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("ClientId", clientId);
|
||||||
|
parameters.Add("RedirectUri", redirectUri);
|
||||||
|
parameters.Add("ResponseType", responseType);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Response.WriteAsync(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("authorize")]
|
||||||
|
public async Task AuthorizePost(
|
||||||
|
[FromQuery(Name = "client_id")] string clientId,
|
||||||
|
[FromQuery(Name = "redirect_uri")] string redirectUri,
|
||||||
|
[FromQuery(Name = "response_type")] string responseType,
|
||||||
|
[FromForm(Name = "email")] string email,
|
||||||
|
[FromForm(Name = "password")] string password,
|
||||||
|
[FromForm(Name = "username")] string username = "",
|
||||||
|
[FromQuery(Name = "view")] string view = "login"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var requiredRedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl;
|
||||||
|
|
||||||
|
if (Configuration.Authentication.OAuth2.ClientId != clientId ||
|
||||||
|
requiredRedirectUri != redirectUri ||
|
||||||
|
responseType != "code")
|
||||||
|
{
|
||||||
|
throw new HttpApiException("Invalid oauth2 request", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view == "register" && string.IsNullOrEmpty(username))
|
||||||
|
throw new HttpApiException("You need to provide a username", 400);
|
||||||
|
|
||||||
|
string? errorMessage = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (view == "register")
|
||||||
|
{
|
||||||
|
var user = await Register(username, email, password);
|
||||||
|
var code = await GenerateCode(user);
|
||||||
|
|
||||||
|
Response.Redirect($"{redirectUri}?code={code}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var user = await Login(email, password);
|
||||||
|
var code = await GenerateCode(user);
|
||||||
|
|
||||||
|
Response.Redirect($"{redirectUri}?code={code}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpApiException e)
|
||||||
|
{
|
||||||
|
errorMessage = e.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.StatusCode = 200;
|
||||||
|
|
||||||
|
if (view == "register")
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderComponent<Register>(HttpContext.RequestServices, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("ClientId", clientId);
|
||||||
|
parameters.Add("RedirectUri", redirectUri);
|
||||||
|
parameters.Add("ResponseType", responseType);
|
||||||
|
parameters.Add("ErrorMessage", errorMessage!);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Response.WriteAsync(html);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var html = await ComponentHelper.RenderComponent<Login>(HttpContext.RequestServices, parameters =>
|
||||||
|
{
|
||||||
|
parameters.Add("ClientId", clientId);
|
||||||
|
parameters.Add("RedirectUri", redirectUri);
|
||||||
|
parameters.Add("ResponseType", responseType);
|
||||||
|
parameters.Add("ErrorMessage", errorMessage!);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Response.WriteAsync(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("handle")]
|
||||||
|
public async Task<OAuth2HandleResponse> Handle(
|
||||||
|
[FromForm(Name = "grant_type")] string grantType,
|
||||||
|
[FromForm(Name = "code")] string code,
|
||||||
|
[FromForm(Name = "redirect_uri")] string redirectUri,
|
||||||
|
[FromForm(Name = "client_id")] string clientId
|
||||||
|
)
|
||||||
|
{
|
||||||
|
// Check header
|
||||||
|
if(!Request.Headers.ContainsKey("Authorization"))
|
||||||
|
throw new HttpApiException("You are missing the Authorization header", 400);
|
||||||
|
|
||||||
|
var authorizationHeaderValue = Request.Headers["Authorization"].FirstOrDefault() ?? "";
|
||||||
|
|
||||||
|
if(authorizationHeaderValue != $"Basic {Configuration.Authentication.OAuth2.ClientSecret}")
|
||||||
|
throw new HttpApiException("Invalid Authorization header value", 400);
|
||||||
|
|
||||||
|
// Check form
|
||||||
|
if(grantType != "authorization_code")
|
||||||
|
throw new HttpApiException("Invalid grant type provided", 400);
|
||||||
|
|
||||||
|
if(clientId != Configuration.Authentication.OAuth2.ClientId)
|
||||||
|
throw new HttpApiException("Invalid client id provided", 400);
|
||||||
|
|
||||||
|
if(redirectUri != (Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl))
|
||||||
|
throw new HttpApiException("Invalid redirect uri provided", 400);
|
||||||
|
|
||||||
|
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
|
||||||
|
ClaimsPrincipal? codeData;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
codeData = jwtSecurityTokenHandler.ValidateToken(code, new TokenValidationParameters()
|
||||||
|
{
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
Configuration.Authentication.Secret
|
||||||
|
)),
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateIssuer = false
|
||||||
|
}, out _);
|
||||||
|
}
|
||||||
|
catch (SecurityTokenException)
|
||||||
|
{
|
||||||
|
throw new HttpApiException("Invalid code provided", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeData == null)
|
||||||
|
throw new HttpApiException("Invalid code provided", 400);
|
||||||
|
|
||||||
|
var userIdClaim = codeData.Claims.FirstOrDefault(x => x.Type == "id");
|
||||||
|
|
||||||
|
if (userIdClaim == null)
|
||||||
|
throw new HttpApiException("Malformed code provided", 400);
|
||||||
|
|
||||||
|
if(!int.TryParse(userIdClaim.Value, out var userId))
|
||||||
|
throw new HttpApiException("Malformed code provided", 400);
|
||||||
|
|
||||||
|
var user = UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefault(x => x.Id == userId);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
throw new HttpApiException("Malformed code provided", 400);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
UserId = user.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GenerateCode(User user)
|
||||||
|
{
|
||||||
|
var securityTokenDescriptor = new SecurityTokenDescriptor()
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddMinutes(1),
|
||||||
|
IssuedAt = DateTime.Now,
|
||||||
|
NotBefore = DateTime.Now.AddMinutes(-1),
|
||||||
|
Claims = new Dictionary<string, object>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"id",
|
||||||
|
user.Id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SigningCredentials = new SigningCredentials(
|
||||||
|
new SymmetricSecurityKey(
|
||||||
|
Encoding.UTF8.GetBytes(Configuration.Authentication.Secret)
|
||||||
|
),
|
||||||
|
SecurityAlgorithms.HmacSha256
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
|
||||||
|
|
||||||
|
return jwtSecurityTokenHandler.WriteToken(securityToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> Register(string username, string email, string password)
|
||||||
|
{
|
||||||
|
if (await UserRepository.Get().AnyAsync(x => x.Username == username))
|
||||||
|
throw new HttpApiException("A account with that username already exists", 400);
|
||||||
|
|
||||||
|
if (await UserRepository.Get().AnyAsync(x => x.Email == email))
|
||||||
|
throw new HttpApiException("A account with that email already exists", 400);
|
||||||
|
|
||||||
|
var user = new User()
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
Email = email,
|
||||||
|
Password = HashHelper.Hash(password)
|
||||||
|
};
|
||||||
|
|
||||||
|
var finalUser = await UserRepository.Add(user);
|
||||||
|
|
||||||
|
return finalUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> Login(string email, string password)
|
||||||
|
{
|
||||||
|
var user = await UserRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefaultAsync(x => x.Email == email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
throw new HttpApiException("Invalid combination of email and password", 400);
|
||||||
|
|
||||||
|
if (!HashHelper.Verify(password, user.Password))
|
||||||
|
throw new HttpApiException("Invalid combination of email and password", 400);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor
Normal file
72
Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<html lang="en" class="h-full bg-gray-900">
|
||||||
|
<head>
|
||||||
|
<title>Register a new account</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<script src="https://cdn.tailwindcss.com?plugins=forms"></script>
|
||||||
|
</head>
|
||||||
|
<body class="h-full">
|
||||||
|
|
||||||
|
<div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
|
<div class="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<img class="mx-auto h-14 w-auto" src="https://help.moonlightpanel.xyz/images/logo.svg" alt="Your Company">
|
||||||
|
<h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-100">Create your account</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
|
||||||
|
<div class="bg-gray-800 px-6 py-12 shadow sm:rounded-lg sm:px-12">
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="rounded-lg bg-red-500 p-5 text-center text-white mb-8">
|
||||||
|
@ErrorMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form class="space-y-6" method="POST">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium leading-6 text-gray-100">Username</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="username" name="username" type="text" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium leading-6 text-gray-100">Email address</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="email" name="email" type="email" autocomplete="email" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-100">Password</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="password" name="password" type="password" autocomplete="current-password" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||||
|
Create your account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-5 text-center text-sm text-gray-500">
|
||||||
|
Already registered?
|
||||||
|
<a href="?client_id=@(ClientId)&redirect_uri=@(RedirectUri)&response_type=@(ResponseType)&view=login" class="font-semibold leading-6 text-indigo-600 hover:text-indigo-500">Login</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public string ClientId { get; set; }
|
||||||
|
[Parameter] public string RedirectUri { get; set; }
|
||||||
|
[Parameter] public string ResponseType { get; set; }
|
||||||
|
[Parameter] public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Extended.Abstractions;
|
|
||||||
using MoonCore.Extended.Helpers;
|
|
||||||
using MoonCore.Extended.OAuth2.LocalProvider;
|
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
|
||||||
|
|
||||||
namespace Moonlight.ApiServer.Implementations.OAuth2;
|
|
||||||
|
|
||||||
public class LocalOAuth2Provider : ILocalProviderImplementation<User>
|
|
||||||
{
|
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
|
||||||
|
|
||||||
public LocalOAuth2Provider(DatabaseRepository<User> userRepository)
|
|
||||||
{
|
|
||||||
UserRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveChanges(User model)
|
|
||||||
{
|
|
||||||
await UserRepository.Update(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User?> LoadById(int id)
|
|
||||||
{
|
|
||||||
var res = await UserRepository
|
|
||||||
.Get()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == id);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<User> Login(string email, string password)
|
|
||||||
{
|
|
||||||
var user = UserRepository.Get().FirstOrDefault(x => x.Email == email);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw new HttpApiException("Invalid email or password", 400);
|
|
||||||
|
|
||||||
if(!HashHelper.Verify(password, user.Password))
|
|
||||||
throw new HttpApiException("Invalid email or password", 400);
|
|
||||||
|
|
||||||
return Task.FromResult(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<User> Register(string username, string email, string password)
|
|
||||||
{
|
|
||||||
if (UserRepository.Get().Any(x => x.Username == username))
|
|
||||||
throw new HttpApiException("A user with that username already exists", 400);
|
|
||||||
|
|
||||||
if (UserRepository.Get().Any(x => x.Email == email))
|
|
||||||
throw new HttpApiException("A user with that email address already exists", 400);
|
|
||||||
|
|
||||||
var user = new User()
|
|
||||||
{
|
|
||||||
Username = username,
|
|
||||||
Email = email,
|
|
||||||
Password = HashHelper.Hash(password)
|
|
||||||
};
|
|
||||||
|
|
||||||
var finalUser = await UserRepository.Add(user);
|
|
||||||
|
|
||||||
return finalUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="MoonCore" Version="1.8.1" />
|
<PackageReference Include="MoonCore" Version="1.8.2" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.2.6" />
|
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
|
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using MoonCore.Configuration;
|
using MoonCore.Configuration;
|
||||||
using MoonCore.Extended.Abstractions;
|
using MoonCore.Extended.Abstractions;
|
||||||
using MoonCore.Extended.Extensions;
|
using MoonCore.Extended.Extensions;
|
||||||
using MoonCore.Extended.Helpers;
|
using MoonCore.Extended.Helpers;
|
||||||
using MoonCore.Extended.OAuth2.Consumer;
|
using MoonCore.Extended.JwtInvalidation;
|
||||||
using MoonCore.Extended.OAuth2.Consumer.Extensions;
|
|
||||||
using MoonCore.Extended.OAuth2.LocalProvider;
|
|
||||||
using MoonCore.Extended.OAuth2.LocalProvider.Extensions;
|
|
||||||
using MoonCore.Extended.OAuth2.LocalProvider.Implementations;
|
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.PluginFramework.Extensions;
|
using MoonCore.PluginFramework.Extensions;
|
||||||
@@ -17,8 +17,6 @@ using MoonCore.Services;
|
|||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Database.Entities;
|
using Moonlight.ApiServer.Database.Entities;
|
||||||
using Moonlight.ApiServer.Helpers;
|
using Moonlight.ApiServer.Helpers;
|
||||||
using Moonlight.ApiServer.Http.Middleware;
|
|
||||||
using Moonlight.ApiServer.Implementations.OAuth2;
|
|
||||||
using Moonlight.ApiServer.Interfaces.Auth;
|
using Moonlight.ApiServer.Interfaces.Auth;
|
||||||
using Moonlight.ApiServer.Interfaces.OAuth2;
|
using Moonlight.ApiServer.Interfaces.OAuth2;
|
||||||
using Moonlight.ApiServer.Interfaces.Startup;
|
using Moonlight.ApiServer.Interfaces.Startup;
|
||||||
@@ -78,7 +76,7 @@ public class Startup
|
|||||||
await RegisterLogging();
|
await RegisterLogging();
|
||||||
await RegisterBase();
|
await RegisterBase();
|
||||||
await RegisterDatabase();
|
await RegisterDatabase();
|
||||||
await RegisterOAuth2();
|
await RegisterAuth();
|
||||||
await RegisterCaching();
|
await RegisterCaching();
|
||||||
await HookPluginBuild();
|
await HookPluginBuild();
|
||||||
await HandleConfigureArguments();
|
await HandleConfigureArguments();
|
||||||
@@ -90,13 +88,11 @@ public class Startup
|
|||||||
await PrepareDatabase();
|
await PrepareDatabase();
|
||||||
|
|
||||||
await UseBase();
|
await UseBase();
|
||||||
await UseOAuth2();
|
await UseAuth();
|
||||||
await UseBaseMiddleware();
|
|
||||||
await HookPluginConfigure();
|
await HookPluginConfigure();
|
||||||
await UsePluginAssets();
|
await UsePluginAssets();
|
||||||
|
|
||||||
await MapBase();
|
await MapBase();
|
||||||
await MapOAuth2();
|
|
||||||
await HookPluginEndpoints();
|
await HookPluginEndpoints();
|
||||||
|
|
||||||
await WebApplication.RunAsync();
|
await WebApplication.RunAsync();
|
||||||
@@ -240,14 +236,6 @@ public class Startup
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseBaseMiddleware()
|
|
||||||
{
|
|
||||||
WebApplication.UseMiddleware<AuthorizationMiddleware>();
|
|
||||||
WebApplication.UseMiddleware<ApiAuthenticationMiddleware>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task MapBase()
|
private Task MapBase()
|
||||||
{
|
{
|
||||||
WebApplication.MapControllers();
|
WebApplication.MapControllers();
|
||||||
@@ -593,50 +581,56 @@ public class Startup
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region OAuth2
|
#region Authentication & Authorisation
|
||||||
|
|
||||||
private Task RegisterOAuth2()
|
private Task RegisterAuth()
|
||||||
{
|
{
|
||||||
WebApplicationBuilder.Services.AddOAuth2Authentication<User>(configuration =>
|
WebApplicationBuilder.Services
|
||||||
|
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new()
|
||||||
|
{
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(
|
||||||
|
Configuration.Authentication.Secret
|
||||||
|
)),
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ClockSkew = TimeSpan.Zero,
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = Configuration.PublicUrl,
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = Configuration.PublicUrl
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
WebApplicationBuilder.Services.AddJwtInvalidation(options =>
|
||||||
{
|
{
|
||||||
configuration.AccessSecret = Configuration.Authentication.AccessSecret;
|
options.InvalidateTimeProvider = async (provider, principal) =>
|
||||||
configuration.RefreshSecret = Configuration.Authentication.RefreshSecret;
|
{
|
||||||
configuration.RefreshDuration = TimeSpan.FromSeconds(Configuration.Authentication.RefreshDuration);
|
var userIdClaim = principal.Claims.First(x => x.Type == "userId");
|
||||||
configuration.RefreshInterval = TimeSpan.FromSeconds(Configuration.Authentication.AccessDuration);
|
var userId = int.Parse(userIdClaim.Value);
|
||||||
configuration.ClientId = Configuration.Authentication.OAuth2.ClientId;
|
|
||||||
configuration.ClientSecret = Configuration.Authentication.OAuth2.ClientSecret;
|
var userRepository = provider.GetRequiredService<DatabaseRepository<User>>();
|
||||||
configuration.AuthorizeEndpoint = Configuration.PublicUrl + "/api/_auth/oauth2/authorize";
|
var user = await userRepository.Get().FirstAsync(x => x.Id == userId);
|
||||||
configuration.RedirectUri = Configuration.PublicUrl;
|
|
||||||
|
return user.TokenValidTimestamp;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IDataProvider<User>, LocalOAuth2Provider>();
|
WebApplicationBuilder.Services.AddAuthorization();
|
||||||
|
|
||||||
if (!Configuration.Authentication.UseLocalOAuth2)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
WebApplicationBuilder.Services.AddLocalOAuth2Provider<User>(Configuration.PublicUrl);
|
|
||||||
WebApplicationBuilder.Services.AddScoped<ILocalProviderImplementation<User>, LocalOAuth2Provider>();
|
|
||||||
WebApplicationBuilder.Services.AddScoped<IOAuth2Provider<User>, LocalOAuth2Provider<User>>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task UseOAuth2()
|
private Task UseAuth()
|
||||||
{
|
{
|
||||||
WebApplication.UseOAuth2Authentication<User>();
|
WebApplication.UseAuthentication();
|
||||||
WebApplication.UseMiddleware<PermissionLoaderMiddleware>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
WebApplication.UseJwtInvalidation();
|
||||||
}
|
|
||||||
|
|
||||||
private Task MapOAuth2()
|
WebApplication.UseAuthorization();
|
||||||
{
|
|
||||||
WebApplication.MapOAuth2Authentication<User>();
|
|
||||||
|
|
||||||
if (!Configuration.Authentication.UseLocalOAuth2)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
WebApplication.MapLocalOAuth2Provider<User>();
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Moonlight.Client.Interfaces;
|
|
||||||
using Moonlight.Client.Services;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Implementations;
|
|
||||||
|
|
||||||
public class AuthenticationUiHandler : IAppLoader, IAppScreen
|
|
||||||
{
|
|
||||||
public int Priority => 0;
|
|
||||||
|
|
||||||
public Task<bool> ShouldRender(IServiceProvider serviceProvider)
|
|
||||||
=> Task.FromResult(false);
|
|
||||||
|
|
||||||
public RenderFragment Render() => throw new NotImplementedException();
|
|
||||||
|
|
||||||
public async Task Load(IServiceProvider serviceProvider)
|
|
||||||
{
|
|
||||||
var identityService = serviceProvider.GetRequiredService<IdentityService>();
|
|
||||||
await identityService.Check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1" />
|
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all"/>
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all"/>
|
||||||
<PackageReference Include="MoonCore" Version="1.8.1" />
|
<PackageReference Include="MoonCore" Version="1.8.2" />
|
||||||
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
|
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
|
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
|
||||||
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.6" />
|
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.2.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using Microsoft.JSInterop;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Services;
|
|
||||||
|
|
||||||
public class DownloadService
|
|
||||||
{
|
|
||||||
private readonly IJSRuntime JsRuntime;
|
|
||||||
|
|
||||||
public DownloadService(IJSRuntime jsRuntime)
|
|
||||||
{
|
|
||||||
JsRuntime = jsRuntime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DownloadStream(string fileName, Stream stream)
|
|
||||||
{
|
|
||||||
using var streamRef = new DotNetStreamReference(stream);
|
|
||||||
|
|
||||||
await JsRuntime.InvokeVoidAsync("moonlight.utils.download", fileName, streamRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DownloadBytes(string fileName, byte[] bytes)
|
|
||||||
{
|
|
||||||
var ms = new MemoryStream(bytes);
|
|
||||||
|
|
||||||
await DownloadStream(fileName, ms);
|
|
||||||
|
|
||||||
ms.Close();
|
|
||||||
await ms.DisposeAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task DownloadString(string fileName, string content) =>
|
|
||||||
await DownloadBytes(fileName, Encoding.UTF8.GetBytes(content));
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using MoonCore.Attributes;
|
|
||||||
using MoonCore.Blazor.Services;
|
|
||||||
using MoonCore.Exceptions;
|
|
||||||
using MoonCore.Helpers;
|
|
||||||
using Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
namespace Moonlight.Client.Services;
|
|
||||||
|
|
||||||
[Scoped]
|
|
||||||
public class IdentityService
|
|
||||||
{
|
|
||||||
public string Username { get; private set; } = "";
|
|
||||||
public string Email { get; private set; } = "";
|
|
||||||
public string[] Permissions { get; private set; } = [];
|
|
||||||
public bool IsLoggedIn { get; private set; } = false;
|
|
||||||
|
|
||||||
private readonly HttpApiClient HttpApiClient;
|
|
||||||
private readonly LocalStorageService LocalStorageService;
|
|
||||||
|
|
||||||
public IdentityService(HttpApiClient httpApiClient, LocalStorageService localStorageService)
|
|
||||||
{
|
|
||||||
HttpApiClient = httpApiClient;
|
|
||||||
LocalStorageService = localStorageService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Check()
|
|
||||||
{
|
|
||||||
IsLoggedIn = false;
|
|
||||||
|
|
||||||
var response = await HttpApiClient.GetJson<CheckResponse>("api/auth/check");
|
|
||||||
|
|
||||||
Username = response.Username;
|
|
||||||
Email = response.Email;
|
|
||||||
Permissions = response.Permissions;
|
|
||||||
|
|
||||||
IsLoggedIn = true;
|
|
||||||
|
|
||||||
//await OnStateChanged?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task Logout()
|
|
||||||
{
|
|
||||||
await LocalStorageService.SetString("AccessToken", "unset");
|
|
||||||
await LocalStorageService.SetString("RefreshToken", "unset");
|
|
||||||
await LocalStorageService.Set("ExpiresAt", DateTime.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasPermission(string requiredPermission)
|
|
||||||
{
|
|
||||||
// Check for wildcard permission
|
|
||||||
if (Permissions.Contains("*"))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var requiredSegments = requiredPermission.Split('.');
|
|
||||||
|
|
||||||
// Check if the user has the exact permission or a wildcard match
|
|
||||||
foreach (var permission in Permissions)
|
|
||||||
{
|
|
||||||
var permissionSegments = permission.Split('.');
|
|
||||||
|
|
||||||
// Iterate over the segments of the required permission
|
|
||||||
for (var i = 0; i < requiredSegments.Length; i++)
|
|
||||||
{
|
|
||||||
// If the current segment matches or is a wildcard, continue to the next segment
|
|
||||||
if (i < permissionSegments.Length && requiredSegments[i] == permissionSegments[i] ||
|
|
||||||
permissionSegments[i] == "*")
|
|
||||||
{
|
|
||||||
// If we've reached the end of the permissionSegments array, it means we've found a match
|
|
||||||
if (i == permissionSegments.Length - 1)
|
|
||||||
return true; // Found an exact match or a wildcard match
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If we reach here, it means the segments don't match and we break out of the loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No matching permission found
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
124
Moonlight.Client/Services/RemoteAuthStateManager.cs
Normal file
124
Moonlight.Client/Services/RemoteAuthStateManager.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Web;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
using MoonCore.Blazor.Services;
|
||||||
|
using MoonCore.Blazor.Tailwind.Auth;
|
||||||
|
using MoonCore.Exceptions;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using Moonlight.Shared.Http.Requests.Auth;
|
||||||
|
using Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
|
namespace Moonlight.Client.Services;
|
||||||
|
|
||||||
|
public class RemoteAuthStateManager : AuthenticationStateManager
|
||||||
|
{
|
||||||
|
private readonly NavigationManager NavigationManager;
|
||||||
|
private readonly HttpApiClient HttpApiClient;
|
||||||
|
private readonly LocalStorageService LocalStorageService;
|
||||||
|
private readonly ILogger<RemoteAuthStateManager> Logger;
|
||||||
|
|
||||||
|
public RemoteAuthStateManager(
|
||||||
|
HttpApiClient httpApiClient,
|
||||||
|
LocalStorageService localStorageService,
|
||||||
|
NavigationManager navigationManager,
|
||||||
|
ILogger<RemoteAuthStateManager> logger
|
||||||
|
)
|
||||||
|
{
|
||||||
|
HttpApiClient = httpApiClient;
|
||||||
|
LocalStorageService = localStorageService;
|
||||||
|
NavigationManager = navigationManager;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
|
=> await LoadAuthState();
|
||||||
|
|
||||||
|
public override async Task HandleLogin()
|
||||||
|
{
|
||||||
|
var uri = new Uri(NavigationManager.Uri);
|
||||||
|
var codeParam = HttpUtility.ParseQueryString(uri.Query).Get("code");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(codeParam)) // If this is true, we need to log in the user
|
||||||
|
{
|
||||||
|
await StartLogin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loginCompleteData = await HttpApiClient.PostJson<LoginCompleteResponse>(
|
||||||
|
"api/auth/complete",
|
||||||
|
new LoginCompleteRequest()
|
||||||
|
{
|
||||||
|
Code = codeParam
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await LocalStorageService.SetString("AccessToken", loginCompleteData.AccessToken);
|
||||||
|
|
||||||
|
NavigationManager.NavigateTo("/");
|
||||||
|
NotifyAuthenticationStateChanged(LoadAuthState());
|
||||||
|
}
|
||||||
|
catch (HttpApiException e)
|
||||||
|
{
|
||||||
|
Logger.LogError("Unable to complete login: {e}", e);
|
||||||
|
|
||||||
|
await StartLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task Logout()
|
||||||
|
{
|
||||||
|
if (await LocalStorageService.ContainsKey("AccessToken"))
|
||||||
|
await LocalStorageService.SetString("AccessToken", "");
|
||||||
|
|
||||||
|
NotifyAuthenticationStateChanged(LoadAuthState());
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Utilities
|
||||||
|
|
||||||
|
private async Task StartLogin()
|
||||||
|
{
|
||||||
|
var loginStartData = await HttpApiClient.GetJson<LoginStartResponse>("api/auth/start");
|
||||||
|
|
||||||
|
var url = $"{loginStartData.Endpoint}" +
|
||||||
|
$"?client_id={loginStartData.ClientId}" +
|
||||||
|
$"&redirect_uri={loginStartData.RedirectUri}" +
|
||||||
|
$"&response_type=code";
|
||||||
|
|
||||||
|
NavigationManager.NavigateTo(url, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AuthenticationState> LoadAuthState()
|
||||||
|
{
|
||||||
|
AuthenticationState newState;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var checkData = await HttpApiClient.GetJson<CheckResponse>("api/auth/check");
|
||||||
|
|
||||||
|
newState = new(new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity(
|
||||||
|
[
|
||||||
|
new Claim("username", checkData.Username),
|
||||||
|
new Claim("email", checkData.Email),
|
||||||
|
new Claim("permissions", checkData.Permissions)
|
||||||
|
],
|
||||||
|
"RemoteAuthStateManager"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (HttpApiException)
|
||||||
|
{
|
||||||
|
newState = new(new ClaimsPrincipal(
|
||||||
|
new ClaimsIdentity()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -3,11 +3,9 @@ using System.Text.Json;
|
|||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using MoonCore.Blazor.Extensions;
|
|
||||||
using MoonCore.Blazor.Services;
|
using MoonCore.Blazor.Services;
|
||||||
using MoonCore.Blazor.Tailwind.Extensions;
|
using MoonCore.Blazor.Tailwind.Extensions;
|
||||||
using MoonCore.Blazor.Tailwind.Forms;
|
using MoonCore.Blazor.Tailwind.Auth;
|
||||||
using MoonCore.Blazor.Tailwind.Forms.Components;
|
|
||||||
using MoonCore.Extensions;
|
using MoonCore.Extensions;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
using MoonCore.PluginFramework.Extensions;
|
using MoonCore.PluginFramework.Extensions;
|
||||||
@@ -16,7 +14,6 @@ using Moonlight.Client.Implementations;
|
|||||||
using Moonlight.Client.Interfaces;
|
using Moonlight.Client.Interfaces;
|
||||||
using Moonlight.Client.Services;
|
using Moonlight.Client.Services;
|
||||||
using Moonlight.Client.UI;
|
using Moonlight.Client.UI;
|
||||||
using Moonlight.Client.UI.Forms;
|
|
||||||
using Moonlight.Shared.Misc;
|
using Moonlight.Shared.Misc;
|
||||||
|
|
||||||
namespace Moonlight.Client;
|
namespace Moonlight.Client;
|
||||||
@@ -64,8 +61,7 @@ public class Startup
|
|||||||
|
|
||||||
await RegisterLogging();
|
await RegisterLogging();
|
||||||
await RegisterBase();
|
await RegisterBase();
|
||||||
await RegisterOAuth2();
|
await RegisterAuthentication();
|
||||||
await RegisterFormComponents();
|
|
||||||
await RegisterInterfaces();
|
await RegisterInterfaces();
|
||||||
await HookPluginBuild();
|
await HookPluginBuild();
|
||||||
|
|
||||||
@@ -133,8 +129,27 @@ public class Startup
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
WebAssemblyHostBuilder.Services.AddScoped(sp =>
|
||||||
|
{
|
||||||
|
var httpClient = sp.GetRequiredService<HttpClient>();
|
||||||
|
var httpApiClient = new HttpApiClient(httpClient);
|
||||||
|
|
||||||
|
var localStorageService = sp.GetRequiredService<LocalStorageService>();
|
||||||
|
|
||||||
|
httpApiClient.OnConfigureRequest += async request =>
|
||||||
|
{
|
||||||
|
var accessToken = await localStorageService.GetString("AccessToken");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(accessToken))
|
||||||
|
return;
|
||||||
|
|
||||||
|
request.Headers.Add("Authorization", $"Bearer {accessToken}");
|
||||||
|
};
|
||||||
|
|
||||||
|
return httpApiClient;
|
||||||
|
});
|
||||||
|
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
|
WebAssemblyHostBuilder.Services.AddScoped<WindowService>();
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<DownloadService>();
|
|
||||||
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
|
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
|
||||||
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
|
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
|
||||||
|
|
||||||
@@ -143,23 +158,6 @@ public class Startup
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RegisterOAuth2()
|
|
||||||
{
|
|
||||||
WebAssemblyHostBuilder.AddTokenAuthentication();
|
|
||||||
WebAssemblyHostBuilder.AddOAuth2();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task RegisterFormComponents()
|
|
||||||
{
|
|
||||||
FormComponentRepository.Set<string, StringComponent>();
|
|
||||||
FormComponentRepository.Set<int, IntComponent>();
|
|
||||||
FormComponentRepository.Set<DateTime, DateComponent>();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Asset Loading
|
#region Asset Loading
|
||||||
|
|
||||||
private async Task LoadAssets()
|
private async Task LoadAssets()
|
||||||
@@ -339,4 +337,18 @@ public class Startup
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Authentication
|
||||||
|
|
||||||
|
private Task RegisterAuthentication()
|
||||||
|
{
|
||||||
|
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
|
||||||
|
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
|
||||||
|
|
||||||
|
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
"-m-3",
|
"-m-3",
|
||||||
"-mx-2",
|
"-mx-2",
|
||||||
"-mx-4",
|
"-mx-4",
|
||||||
|
"-rotate-90",
|
||||||
"-translate-x-1/2",
|
"-translate-x-1/2",
|
||||||
"-translate-x-full",
|
"-translate-x-full",
|
||||||
|
"-translate-y-1/2",
|
||||||
"absolute",
|
"absolute",
|
||||||
"align-middle",
|
"align-middle",
|
||||||
"animate-spin",
|
"animate-spin",
|
||||||
@@ -40,8 +42,12 @@
|
|||||||
"block",
|
"block",
|
||||||
"border",
|
"border",
|
||||||
"border-0",
|
"border-0",
|
||||||
|
"border-2",
|
||||||
|
"border-b",
|
||||||
"border-b-2",
|
"border-b-2",
|
||||||
|
"border-dashed",
|
||||||
"border-gray-100/10",
|
"border-gray-100/10",
|
||||||
|
"border-gray-600",
|
||||||
"border-gray-700",
|
"border-gray-700",
|
||||||
"border-gray-700/60",
|
"border-gray-700/60",
|
||||||
"border-none",
|
"border-none",
|
||||||
@@ -64,6 +70,7 @@
|
|||||||
"dark:disabled:text-gray-600",
|
"dark:disabled:text-gray-600",
|
||||||
"dark:group-hover:text-gray-400",
|
"dark:group-hover:text-gray-400",
|
||||||
"dark:text-gray-100",
|
"dark:text-gray-100",
|
||||||
|
"dark:text-gray-400",
|
||||||
"dark:text-gray-500",
|
"dark:text-gray-500",
|
||||||
"disabled:bg-gray-100",
|
"disabled:bg-gray-100",
|
||||||
"disabled:bg-gray-800",
|
"disabled:bg-gray-800",
|
||||||
@@ -90,6 +97,7 @@
|
|||||||
"flex",
|
"flex",
|
||||||
"flex-1",
|
"flex-1",
|
||||||
"flex-col",
|
"flex-col",
|
||||||
|
"flex-grow",
|
||||||
"flex-nowrap",
|
"flex-nowrap",
|
||||||
"flex-row",
|
"flex-row",
|
||||||
"flex-shrink-0",
|
"flex-shrink-0",
|
||||||
@@ -120,6 +128,7 @@
|
|||||||
"gap-x-5",
|
"gap-x-5",
|
||||||
"gap-x-6",
|
"gap-x-6",
|
||||||
"gap-y-2",
|
"gap-y-2",
|
||||||
|
"gap-y-3",
|
||||||
"gap-y-5",
|
"gap-y-5",
|
||||||
"gap-y-7",
|
"gap-y-7",
|
||||||
"gap-y-8",
|
"gap-y-8",
|
||||||
@@ -138,13 +147,16 @@
|
|||||||
"h-4",
|
"h-4",
|
||||||
"h-5",
|
"h-5",
|
||||||
"h-6",
|
"h-6",
|
||||||
|
"h-64",
|
||||||
"h-8",
|
"h-8",
|
||||||
"h-[20vh]",
|
"h-[20vh]",
|
||||||
"hidden",
|
"hidden",
|
||||||
|
"hover:bg-gray-600",
|
||||||
"hover:bg-gray-700",
|
"hover:bg-gray-700",
|
||||||
"hover:bg-gray-800",
|
"hover:bg-gray-800",
|
||||||
"hover:bg-primary-600",
|
"hover:bg-primary-600",
|
||||||
"hover:border-b-2",
|
"hover:border-b-2",
|
||||||
|
"hover:border-gray-500",
|
||||||
"hover:border-gray-600",
|
"hover:border-gray-600",
|
||||||
"hover:border-primary-500",
|
"hover:border-primary-500",
|
||||||
"hover:text-gray-100",
|
"hover:text-gray-100",
|
||||||
@@ -191,6 +203,7 @@
|
|||||||
"lg:z-50",
|
"lg:z-50",
|
||||||
"list-disc",
|
"list-disc",
|
||||||
"m-1",
|
"m-1",
|
||||||
|
"m-10",
|
||||||
"m-3",
|
"m-3",
|
||||||
"max-h-56",
|
"max-h-56",
|
||||||
"max-w-3xl",
|
"max-w-3xl",
|
||||||
@@ -211,10 +224,15 @@
|
|||||||
"md:grid-cols-3",
|
"md:grid-cols-3",
|
||||||
"md:h-[40vh]",
|
"md:h-[40vh]",
|
||||||
"md:items-center",
|
"md:items-center",
|
||||||
|
"md:ms-2",
|
||||||
|
"md:space-x-2",
|
||||||
"md:space-y-0",
|
"md:space-y-0",
|
||||||
|
"md:table-cell",
|
||||||
"md:text-3xl",
|
"md:text-3xl",
|
||||||
"me-1",
|
"me-1",
|
||||||
"me-2",
|
"me-2",
|
||||||
|
"me-2.5",
|
||||||
|
"me-3",
|
||||||
"min-h-full",
|
"min-h-full",
|
||||||
"min-w-60",
|
"min-w-60",
|
||||||
"ml-2",
|
"ml-2",
|
||||||
@@ -228,6 +246,7 @@
|
|||||||
"mr-6",
|
"mr-6",
|
||||||
"ms-0.5",
|
"ms-0.5",
|
||||||
"ms-1",
|
"ms-1",
|
||||||
|
"ms-2",
|
||||||
"ms-3",
|
"ms-3",
|
||||||
"mt-1",
|
"mt-1",
|
||||||
"mt-10",
|
"mt-10",
|
||||||
@@ -236,14 +255,14 @@
|
|||||||
"mt-3",
|
"mt-3",
|
||||||
"mt-4",
|
"mt-4",
|
||||||
"mt-5",
|
"mt-5",
|
||||||
|
"mt-6",
|
||||||
"mt-8",
|
"mt-8",
|
||||||
"mt-auto",
|
"mt-auto",
|
||||||
|
"mx-0.5",
|
||||||
"mx-2",
|
"mx-2",
|
||||||
"mx-5",
|
|
||||||
"mx-auto",
|
"mx-auto",
|
||||||
"my-1",
|
"my-1",
|
||||||
"my-3",
|
"my-3",
|
||||||
"my-4",
|
|
||||||
"my-8",
|
"my-8",
|
||||||
"opacity-0",
|
"opacity-0",
|
||||||
"opacity-100",
|
"opacity-100",
|
||||||
@@ -262,6 +281,7 @@
|
|||||||
"p-5",
|
"p-5",
|
||||||
"pb-3",
|
"pb-3",
|
||||||
"pb-4",
|
"pb-4",
|
||||||
|
"pb-6",
|
||||||
"pl-12",
|
"pl-12",
|
||||||
"pl-2",
|
"pl-2",
|
||||||
"pl-3",
|
"pl-3",
|
||||||
@@ -273,9 +293,11 @@
|
|||||||
"pr-1",
|
"pr-1",
|
||||||
"pr-3",
|
"pr-3",
|
||||||
"pr-8",
|
"pr-8",
|
||||||
|
"ps-4",
|
||||||
"pt-0.5",
|
"pt-0.5",
|
||||||
"pt-5",
|
"pt-5",
|
||||||
"pt-6",
|
"pt-6",
|
||||||
|
"px-1",
|
||||||
"px-2",
|
"px-2",
|
||||||
"px-3",
|
"px-3",
|
||||||
"px-4",
|
"px-4",
|
||||||
@@ -309,8 +331,11 @@
|
|||||||
"shadow-sm",
|
"shadow-sm",
|
||||||
"shadow-xl",
|
"shadow-xl",
|
||||||
"shrink-0",
|
"shrink-0",
|
||||||
|
"size-52",
|
||||||
|
"size-full",
|
||||||
"sm:-mx-6",
|
"sm:-mx-6",
|
||||||
"sm:auto-cols-max",
|
"sm:auto-cols-max",
|
||||||
|
"sm:block",
|
||||||
"sm:col-span-1",
|
"sm:col-span-1",
|
||||||
"sm:col-span-2",
|
"sm:col-span-2",
|
||||||
"sm:col-span-3",
|
"sm:col-span-3",
|
||||||
@@ -344,6 +369,7 @@
|
|||||||
"sm:pb-4",
|
"sm:pb-4",
|
||||||
"sm:px-6",
|
"sm:px-6",
|
||||||
"sm:text-sm",
|
"sm:text-sm",
|
||||||
|
"space-x-0.5",
|
||||||
"space-x-1",
|
"space-x-1",
|
||||||
"space-x-2",
|
"space-x-2",
|
||||||
"space-x-5",
|
"space-x-5",
|
||||||
@@ -353,8 +379,10 @@
|
|||||||
"space-y-4",
|
"space-y-4",
|
||||||
"space-y-8",
|
"space-y-8",
|
||||||
"sr-only",
|
"sr-only",
|
||||||
|
"start-1/2",
|
||||||
"static",
|
"static",
|
||||||
"sticky",
|
"sticky",
|
||||||
|
"stroke-current",
|
||||||
"table",
|
"table",
|
||||||
"table-auto",
|
"table-auto",
|
||||||
"text-2xl",
|
"text-2xl",
|
||||||
@@ -396,6 +424,7 @@
|
|||||||
"to-gray-800",
|
"to-gray-800",
|
||||||
"to-primary-600",
|
"to-primary-600",
|
||||||
"top-0",
|
"top-0",
|
||||||
|
"top-1/2",
|
||||||
"transform",
|
"transform",
|
||||||
"transition",
|
"transition",
|
||||||
"transition-all",
|
"transition-all",
|
||||||
@@ -413,6 +442,7 @@
|
|||||||
"w-24",
|
"w-24",
|
||||||
"w-32",
|
"w-32",
|
||||||
"w-4",
|
"w-4",
|
||||||
|
"w-40",
|
||||||
"w-5",
|
"w-5",
|
||||||
"w-6",
|
"w-6",
|
||||||
"w-8",
|
"w-8",
|
||||||
|
|||||||
@@ -4,27 +4,6 @@
|
|||||||
|
|
||||||
@inject ApplicationAssemblyService ApplicationAssemblyService
|
@inject ApplicationAssemblyService ApplicationAssemblyService
|
||||||
|
|
||||||
<ErrorLogger>
|
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
|
||||||
<OAuth2AuthenticationHandler>
|
AppAssembly="@typeof(App).Assembly"
|
||||||
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies">
|
AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies" />
|
||||||
<Found Context="routeData">
|
|
||||||
<CascadingValue Name="TargetPageType" Value="routeData.PageType">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
|
||||||
</CascadingValue>
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<PageTitle>Not found</PageTitle>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<div class="flex flex-col justify-center text-center">
|
|
||||||
<img class="h-48 mt-5 mb-3" src="/svg/notfound.svg" alt="Not found illustration"/>
|
|
||||||
|
|
||||||
<h3 class="mt-2 font-semibold text-white text-lg">Page not found</h3>
|
|
||||||
<p class="mt-1 text-gray-300">
|
|
||||||
The page you requested does not exist
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
</OAuth2AuthenticationHandler>
|
|
||||||
</ErrorLogger>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@inherits BaseFormComponent<DateTime>
|
|
||||||
|
|
||||||
<input @bind="Binder.Value" type="date" autocomplete="off" class="form-input w-full">
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
@inject IServiceProvider ServiceProvider
|
@inject IServiceProvider ServiceProvider
|
||||||
@inject ILogger<MainLayout> Logger
|
@inject ILogger<MainLayout> Logger
|
||||||
@inject IAppLoader[] AppLoaders
|
@inject IAppLoader[] AppLoaders
|
||||||
@@ -40,13 +39,9 @@ else
|
|||||||
|
|
||||||
<main class="py-10">
|
<main class="py-10">
|
||||||
<div class="px-4 sm:px-6 lg:px-8">
|
<div class="px-4 sm:px-6 lg:px-8">
|
||||||
<ErrorHandler>
|
<CascadingValue Value="this" IsFixed="true">
|
||||||
<PermissionHandler CheckFunction="CheckPermission">
|
@Body
|
||||||
<CascadingValue Value="this" IsFixed="true">
|
</CascadingValue>
|
||||||
@Body
|
|
||||||
</CascadingValue>
|
|
||||||
</PermissionHandler>
|
|
||||||
</ErrorHandler>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -142,6 +137,4 @@ else
|
|||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckPermission(string permission) => IdentityService.HasPermission(permission);
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
@using Moonlight.Client.Services
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using MoonCore.Blazor.Tailwind.Auth
|
||||||
@using Moonlight.Client.UI.Layouts
|
@using Moonlight.Client.UI.Layouts
|
||||||
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject AuthenticationStateManager AuthStateManager
|
||||||
|
|
||||||
<div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 bg-gray-800/60 backdrop-blur px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
<div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 bg-gray-800/60 backdrop-blur px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
|
||||||
@if (Layout.ShowMobileNavigation)
|
@if (Layout.ShowMobileNavigation)
|
||||||
@@ -26,26 +27,15 @@
|
|||||||
<div class="flex justify-between gap-x-4 lg:gap-x-6 w-full">
|
<div class="flex justify-between gap-x-4 lg:gap-x-6 w-full">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="flex items-center gap-x-4 lg:gap-x-6">
|
<div class="flex items-center gap-x-4 lg:gap-x-6">
|
||||||
@*
|
|
||||||
<button type="button" class="-m-2.5 p-2.5 text-gray-200 hover:text-gray-100">
|
|
||||||
<span class="sr-only">View notifications</span>
|
|
||||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
*@
|
|
||||||
|
|
||||||
<!-- Separator -->
|
|
||||||
<div class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10" aria-hidden="true"></div>
|
<div class="hidden lg:block lg:h-6 lg:w-px lg:bg-gray-900/10" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- Profile dropdown -->
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button @onclick="ToggleProfileNav" @onfocusout="ProfileNav_OnFocusOut" type="button" class="-m-1.5 flex items-center p-1.5" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
<button @onclick="ToggleProfileNav" @onfocusout="ProfileNav_OnFocusOut" type="button" class="-m-1.5 flex items-center p-1.5" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||||
<span class="sr-only">Open user menu</span>
|
<span class="sr-only">Open user menu</span>
|
||||||
<img class="h-8 w-8 rounded-full" src="https://masuowo.xyz/assets/images/avatar.png" alt="">
|
<img class="h-8 w-8 rounded-full" src="https://masuowo.xyz/assets/images/avatar.png" alt="">
|
||||||
<span class="hidden lg:flex lg:items-center">
|
<span class="hidden lg:flex lg:items-center">
|
||||||
<span class="ml-4 text-sm font-semibold leading-6 text-gray-100" aria-hidden="true">
|
<span class="ml-4 text-sm font-semibold leading-6 text-gray-100" aria-hidden="true">
|
||||||
@("@" + IdentityService.Username)
|
@("@" + Username)
|
||||||
</span>
|
</span>
|
||||||
<svg class="ml-2 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
<svg class="ml-2 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd"/>
|
||||||
@@ -53,18 +43,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!--
|
|
||||||
Dropdown menu, show/hide based on menu state.
|
|
||||||
|
|
||||||
Entering: ""
|
|
||||||
From: "transform scale-95"
|
|
||||||
To: "transform opacity-100 scale-100"
|
|
||||||
Leaving: "transition ease-in duration-75"
|
|
||||||
From: "transform opacity-100 scale-100"
|
|
||||||
To: "transform opacity-0 scale-95"
|
|
||||||
-->
|
|
||||||
<div class="@(ShowProfileNav ? "opacity-100" : "opacity-0 hidden") transition ease-out duration-100 absolute right-0 z-10 mt-2.5 w-44 origin-top-right rounded-md bg-gray-750 py-2 shadow-lg ring-1 ring-gray-100/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
|
<div class="@(ShowProfileNav ? "opacity-100" : "opacity-0 hidden") transition ease-out duration-100 absolute right-0 z-10 mt-2.5 w-44 origin-top-right rounded-md bg-gray-750 py-2 shadow-lg ring-1 ring-gray-100/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
|
||||||
<!-- Active: "bg-gray-50", Not Active: "" -->
|
|
||||||
<a href="/admin" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-0">Your profile</a>
|
<a href="/admin" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-0">Your profile</a>
|
||||||
<a @onclick="Logout" @onclick:preventDefault href="#" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-1">Sign out</a>
|
<a @onclick="Logout" @onclick:preventDefault href="#" class="block px-3 py-1 text-sm leading-6 text-gray-100 hover:text-primary-500" role="menuitem" tabindex="-1" id="user-menu-item-1">Sign out</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,8 +55,17 @@
|
|||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
[Parameter] public MainLayout Layout { get; set; }
|
[Parameter] public MainLayout Layout { get; set; }
|
||||||
|
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||||
|
|
||||||
private bool ShowProfileNav = false;
|
private bool ShowProfileNav = false;
|
||||||
|
private string Username;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var identity = await AuthState;
|
||||||
|
var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username");
|
||||||
|
Username = usernameClaim.Value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -109,11 +97,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task Logout()
|
private async Task Logout()
|
||||||
{
|
=> await AuthStateManager.Logout();
|
||||||
await IdentityService.Logout();
|
|
||||||
await ToastService.Info("Successfully logged out");
|
|
||||||
|
|
||||||
//await Layout.Load();
|
|
||||||
Navigation.NavigateTo(Navigation.Uri, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,40 +4,19 @@
|
|||||||
@using Moonlight.Client.UI.Layouts
|
@using Moonlight.Client.UI.Layouts
|
||||||
|
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IdentityService IdentityService
|
|
||||||
@inject ISidebarItemProvider[] SidebarItemProviders
|
@inject ISidebarItemProvider[] SidebarItemProviders
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var url = new Uri(Navigation.Uri);
|
var url = new Uri(Navigation.Uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="relative z-40 lg:hidden transition-opacity @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 hidden")" role="dialog" aria-modal="true">
|
<div
|
||||||
|
class="relative z-40 lg:hidden transition-opacity @(Layout.ShowMobileNavigation ? "opacity-100" : "opacity-0 hidden")"
|
||||||
|
role="dialog" aria-modal="true">
|
||||||
<div class="fixed inset-0 bg-gray-800/80"></div>
|
<div class="fixed inset-0 bg-gray-800/80"></div>
|
||||||
|
|
||||||
<div class="fixed inset-0 flex justify-center bg-gray-900">
|
<div class="fixed inset-0 flex justify-center bg-gray-900">
|
||||||
<!--
|
|
||||||
Off-canvas menu, show/hide based on off-canvas menu state.
|
|
||||||
|
|
||||||
Entering: "transition ease-in-out duration-300 transform"
|
|
||||||
From: "-translate-x-full"
|
|
||||||
To: "translate-x-0"
|
|
||||||
Leaving: "transition ease-in-out duration-300 transform"
|
|
||||||
From: "translate-x-0"
|
|
||||||
To: "-translate-x-full"
|
|
||||||
-->
|
|
||||||
<div class="relative flex w-full max-w-xs flex-1">
|
<div class="relative flex w-full max-w-xs flex-1">
|
||||||
<!--
|
|
||||||
Close button, show/hide based on off-canvas menu state.
|
|
||||||
|
|
||||||
Entering: "ease-in-out duration-300"
|
|
||||||
From: "opacity-0"
|
|
||||||
To: "opacity-100"
|
|
||||||
Leaving: "ease-in-out duration-300"
|
|
||||||
From: "opacity-100"
|
|
||||||
To: "opacity-0"
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
|
||||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6 pb-4">
|
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6 pb-4">
|
||||||
<div class="flex h-16 shrink-0 items-center">a
|
<div class="flex h-16 shrink-0 items-center">a
|
||||||
<img class="h-8 w-auto" src="/logo.svg" alt="Moonlight">
|
<img class="h-8 w-auto" src="/logo.svg" alt="Moonlight">
|
||||||
@@ -46,40 +25,42 @@
|
|||||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||||
@foreach (var group in Items)
|
@foreach (var group in Items)
|
||||||
{
|
{
|
||||||
<li>
|
<li>
|
||||||
@if (!string.IsNullOrEmpty(group.Key))
|
@if (!string.IsNullOrEmpty(group.Key))
|
||||||
{
|
|
||||||
<div class="text-xs font-semibold leading-6 text-gray-400">
|
|
||||||
@group.Key
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ul role="list" class="-mx-2 space-y-1">
|
|
||||||
@foreach (var item in group.Value)
|
|
||||||
{
|
{
|
||||||
var isMatch = item.RequiresExactMatch
|
<div class="text-xs font-semibold leading-6 text-gray-400">
|
||||||
? url.LocalPath == item.Path
|
@group.Key
|
||||||
: url.LocalPath.StartsWith(item.Path);
|
</div>
|
||||||
|
|
||||||
<li>
|
|
||||||
@if (isMatch)
|
|
||||||
{
|
|
||||||
<a href="@item.Path" class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
|
||||||
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
|
||||||
@item.Name
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<a href="@item.Path" class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
|
||||||
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
|
||||||
@item.Name
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
}
|
}
|
||||||
</ul>
|
|
||||||
</li>
|
<ul role="list" class="-mx-2 space-y-1">
|
||||||
|
@foreach (var item in group.Value)
|
||||||
|
{
|
||||||
|
var isMatch = item.RequiresExactMatch
|
||||||
|
? url.LocalPath == item.Path
|
||||||
|
: url.LocalPath.StartsWith(item.Path);
|
||||||
|
|
||||||
|
<li>
|
||||||
|
@if (isMatch)
|
||||||
|
{
|
||||||
|
<a href="@item.Path"
|
||||||
|
class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
||||||
|
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
||||||
|
@item.Name
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="@item.Path"
|
||||||
|
class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
||||||
|
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
||||||
|
@item.Name
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -88,12 +69,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Static sidebar for desktop -->
|
|
||||||
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
||||||
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
|
||||||
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
|
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-800/60 px-6 pb-4">
|
||||||
<div class="flex h-16 shrink-0 items-center">
|
<div class="flex h-16 shrink-0 items-center">
|
||||||
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg" alt="Your Company">
|
<img class="h-8 w-auto" src="https://gamecp.masuowo.xyz/api/core/asset/Core/svg/logo.svg"
|
||||||
|
alt="Your Company">
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex flex-1 flex-col">
|
<nav class="flex flex-1 flex-col">
|
||||||
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
<ul role="list" class="flex flex-1 flex-col gap-y-7">
|
||||||
@@ -117,14 +97,16 @@
|
|||||||
<li>
|
<li>
|
||||||
@if (isMatch)
|
@if (isMatch)
|
||||||
{
|
{
|
||||||
<a href="@item.Path" class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
<a href="@item.Path"
|
||||||
|
class="bg-gray-800 text-white group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
||||||
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
||||||
@item.Name
|
@item.Name
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<a href="@item.Path" class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
<a href="@item.Path"
|
||||||
|
class="text-gray-300 hover:text-white hover:bg-gray-800 group flex gap-x-3 rounded-md p-2 text-sm leading-6 items-center">
|
||||||
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
<i class="ms-1 text-lg shrink-0 @item.Icon"></i>
|
||||||
@item.Name
|
@item.Name
|
||||||
</a>
|
</a>
|
||||||
@@ -149,7 +131,7 @@
|
|||||||
{
|
{
|
||||||
Items = SidebarItemProviders
|
Items = SidebarItemProviders
|
||||||
.SelectMany(x => x.Get())
|
.SelectMany(x => x.Get())
|
||||||
.Where(x => x.Permission == null || (x.Permission != null && IdentityService.HasPermission(x.Permission)))
|
//.Where(x => x.Permission == null || (x.Permission != null && IdentityService.HasPermission(x.Permission)))
|
||||||
.GroupBy(x => x.Group ?? "")
|
.GroupBy(x => x.Group ?? "")
|
||||||
.OrderByDescending(x => string.IsNullOrEmpty(x.Key))
|
.OrderByDescending(x => string.IsNullOrEmpty(x.Key))
|
||||||
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Priority).ToArray());
|
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Priority).ToArray());
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@page "/admin/api"
|
@page "/admin/api"
|
||||||
|
@*
|
||||||
@using MoonCore.Attributes
|
@using MoonCore.Attributes
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Models
|
@using MoonCore.Models
|
||||||
@@ -108,4 +108,4 @@
|
|||||||
configuration.WithField(x => x.ExpiresAt);
|
configuration.WithField(x => x.ExpiresAt);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}*@
|
||||||
69
Moonlight.Client/UI/Views/Admin/Users/Create.razor
Normal file
69
Moonlight.Client/UI/Views/Admin/Users/Create.razor
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@page "/admin/users/create"
|
||||||
|
|
||||||
|
@using MoonCore.Helpers
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||||
|
|
||||||
|
@inject HttpApiClient ApiClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<PageHeader Title="Create User">
|
||||||
|
<a href="/admin/users" class="btn btn-secondary">
|
||||||
|
<i class="icon-chevron-left mr-1"></i>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
|
||||||
|
<i class="icon-check mr-1"></i>
|
||||||
|
Create
|
||||||
|
</WButton>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
|
||||||
|
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Name</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Version</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Author</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Donate Url</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.PermissionsJson" type="text" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HandleForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private HandleForm Form;
|
||||||
|
private CreateUserRequest Request;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Request = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSubmit()
|
||||||
|
{
|
||||||
|
await ApiClient.Post("api/users", Request);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully created User");
|
||||||
|
Navigation.NavigateTo("/admin/users");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,71 +1,55 @@
|
|||||||
@page "/admin/users"
|
@page "/admin/users"
|
||||||
|
|
||||||
@using MoonCore.Attributes
|
|
||||||
@using MoonCore.Blazor.Tailwind.Forms.Components
|
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
@using MoonCore.Models
|
@using MoonCore.Models
|
||||||
@using Moonlight.Shared.Http.Requests.Admin.Users
|
@using MoonCore.Blazor.Tailwind.Dt
|
||||||
@using Moonlight.Shared.Http.Responses.Admin.Users
|
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||||
|
|
||||||
@attribute [RequirePermission("admin.users.read")]
|
@inject HttpApiClient ApiClient
|
||||||
|
@inject AlertService AlertService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
@inject HttpApiClient HttpApiClient
|
<PageHeader Title="Users" />
|
||||||
|
|
||||||
<Crud TItem="UserDetailResponse"
|
<DataTable @ref="Table" TItem="UserDetailResponse" PageSize="15" LoadItemsPaginatedAsync="LoadData">
|
||||||
TCreateForm="CreateUserRequest"
|
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Id)" Name="Id" />
|
||||||
TUpdateForm="UpdateUserRequest"
|
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Username)" Name="Username" />
|
||||||
OnConfigure="OnConfigure">
|
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Email)" Name="Email" />
|
||||||
<View>
|
<DataTableColumn TItem="UserDetailResponse">
|
||||||
<Column TItem="UserDetailResponse" Field="@(x => x.Id)" Title="Id" />
|
<ColumnTemplate>
|
||||||
<Column TItem="UserDetailResponse" Field="@(x => x.Username)" Title="Username" />
|
<div class="flex justify-end">
|
||||||
<Column TItem="UserDetailResponse" Field="@(x => x.Email)" Title="Email" />
|
<a href="/admin/users/update/@(context.Id)" class="text-primary-500 mr-2 sm:mr-3">
|
||||||
</View>
|
<i class="icon-pencil text-base"></i>
|
||||||
</Crud>
|
</a>
|
||||||
|
|
||||||
|
<a href="#" @onclick="() => Delete(context)" @onclick:preventDefault
|
||||||
|
class="text-danger-500">
|
||||||
|
<i class="icon-trash text-base"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ColumnTemplate>
|
||||||
|
</DataTableColumn>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
{
|
{
|
||||||
private void OnConfigure(CrudOptions<UserDetailResponse, CreateUserRequest, UpdateUserRequest> crudOptions)
|
private DataTable<UserDetailResponse> Table;
|
||||||
|
|
||||||
|
private async Task<IPagedData<UserDetailResponse>> LoadData(PaginationOptions options)
|
||||||
|
=> await ApiClient.GetJson<PagedData<UserDetailResponse>>($"api/admin/users?page={options.Page}&pageSize={options.PerPage}");
|
||||||
|
|
||||||
|
private async Task Delete(UserDetailResponse detailResponse)
|
||||||
{
|
{
|
||||||
crudOptions.ItemName = "User";
|
await AlertService.ConfirmDanger(
|
||||||
|
"User deletion",
|
||||||
|
$"Do you really want to delete the user '{detailResponse.Username}'",
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
await ApiClient.Delete($"api/admin/users/{detailResponse.Id}");
|
||||||
|
await ToastService.Success("Successfully deleted user");
|
||||||
|
|
||||||
crudOptions.ItemLoader = async (page, pageSize)
|
await Table.Refresh();
|
||||||
=> await HttpApiClient.GetJson<PagedData<UserDetailResponse>>($"api/admin/users?page={page}&pageSize={pageSize}");
|
}
|
||||||
|
);
|
||||||
crudOptions.SingleItemLoader = async id
|
|
||||||
=> await HttpApiClient.GetJson<UserDetailResponse>($"api/admin/users/{id}");
|
|
||||||
|
|
||||||
crudOptions.QueryIdentifier = response => response.Id.ToString();
|
|
||||||
|
|
||||||
crudOptions.OnCreate = async request
|
|
||||||
=> await HttpApiClient.Post("api/admin/users", request);
|
|
||||||
|
|
||||||
crudOptions.OnUpdate = async (item, request)
|
|
||||||
=> await HttpApiClient.Patch($"api/admin/users/{item.Id}", request);
|
|
||||||
|
|
||||||
crudOptions.OnDelete = async item
|
|
||||||
=> await HttpApiClient.Delete($"api/admin/users/{item.Id}");
|
|
||||||
|
|
||||||
crudOptions.OnConfigureCreate = configuration =>
|
|
||||||
{
|
|
||||||
configuration.WithField(x => x.Username);
|
|
||||||
configuration.WithField(x => x.Email);
|
|
||||||
configuration.WithField(x => x.Password)
|
|
||||||
.WithComponent<StringComponent>(component => component.Type = "password");
|
|
||||||
|
|
||||||
configuration.WithField(x => x.PermissionsJson)
|
|
||||||
.WithComponent<TagComponent>();
|
|
||||||
};
|
|
||||||
|
|
||||||
crudOptions.OnConfigureUpdate = (_, configuration) =>
|
|
||||||
{
|
|
||||||
configuration.WithField(x => x.Username);
|
|
||||||
configuration.WithField(x => x.Email);
|
|
||||||
|
|
||||||
configuration.WithField(x => x.Password, fieldConfiguration =>
|
|
||||||
{
|
|
||||||
fieldConfiguration.Description = "Optional. Specify if you want to change this accounts password";
|
|
||||||
})
|
|
||||||
.WithComponent<StringComponent>(component => component.Type = "password");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
69
Moonlight.Client/UI/Views/Admin/Users/Update.razor
Normal file
69
Moonlight.Client/UI/Views/Admin/Users/Update.razor
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@page "/users/update/{Id:int}"
|
||||||
|
|
||||||
|
@using MoonCore.Helpers
|
||||||
|
@using Moonlight.Shared.Http.Requests.Admin.Users
|
||||||
|
@using Moonlight.Shared.Http.Responses.Admin.Users
|
||||||
|
|
||||||
|
@inject HttpApiClient ApiClient
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<PageHeader Title="Update User">
|
||||||
|
<a href="/admin/users" class="btn btn-secondary">
|
||||||
|
<i class="icon-chevron-left mr-1"></i>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
|
||||||
|
<i class="icon-check mr-1"></i>
|
||||||
|
Update
|
||||||
|
</WButton>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
|
||||||
|
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Name</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Username" type="text" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Version</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Email" type="email" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-white">Author</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input @bind="Request.Password" type="password" autocomplete="off" class="form-input w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HandleForm>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter] public int Id { get; set; }
|
||||||
|
|
||||||
|
private HandleForm Form;
|
||||||
|
private UpdateUserRequest Request;
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader _)
|
||||||
|
{
|
||||||
|
var detail = await ApiClient.GetJson<UserDetailResponse>($"api/users/{Id}");
|
||||||
|
Request = Mapper.Map<UpdateUserRequest>(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSubmit()
|
||||||
|
{
|
||||||
|
await ApiClient.Patch($"api/admin/users/{Id}", Request);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully updated User");
|
||||||
|
Navigation.NavigateTo("/admin/users");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,26 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
|
||||||
@using Moonlight.Client.Services
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
|
||||||
@inject IdentityService IdentityService
|
|
||||||
|
|
||||||
<div class="font-medium leading-[1.1] tracking-tight">
|
<div class="font-medium leading-[1.1] tracking-tight">
|
||||||
<div class="animate-shimmer bg-gradient-to-r from-violet-400 via-sky-400 to-purple-400 bg-clip-text font-semibold text-transparent text-3xl" style="animation-duration: 5s; background-size: 200% 100%">
|
<div class="animate-shimmer bg-gradient-to-r from-violet-400 via-sky-400 to-purple-400 bg-clip-text font-semibold text-transparent text-3xl" style="animation-duration: 5s; background-size: 200% 100%">
|
||||||
Welcome, @(IdentityService.Username)
|
Welcome, @(Username)
|
||||||
</div>
|
</div>
|
||||||
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
|
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-primary-500/10"></div>
|
<div class="text-primary-500/10"></div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||||
|
|
||||||
|
private string Username;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var identity = await AuthState;
|
||||||
|
var usernameClaim = identity.User.Claims.ToArray().First(x => x.Type == "username");
|
||||||
|
Username = usernameClaim.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
|
|
||||||
@using MoonCore.Blazor.Tailwind.Components
|
@using MoonCore.Blazor.Tailwind.Components
|
||||||
@using MoonCore.Blazor.Tailwind.Alerts
|
@using MoonCore.Blazor.Tailwind.Alerts
|
||||||
@using MoonCore.Blazor.Tailwind.Crud
|
|
||||||
@using MoonCore.Blazor.Tailwind.Forms
|
|
||||||
@using MoonCore.Blazor.Tailwind.Helpers
|
@using MoonCore.Blazor.Tailwind.Helpers
|
||||||
@using MoonCore.Blazor.Tailwind.Modals
|
@using MoonCore.Blazor.Tailwind.Modals
|
||||||
@using MoonCore.Blazor.Tailwind.Services
|
@using MoonCore.Blazor.Tailwind.Services
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Moonlight.Shared.Http.Requests.Auth;
|
namespace Moonlight.Shared.Http.Requests.Auth;
|
||||||
|
|
||||||
public class OAuth2HandleRequest
|
public class LoginCompleteRequest
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "You need to provide the oauth2 code")]
|
[Required(ErrorMessage = "You need to provide a code")]
|
||||||
public string Code { get; set; }
|
public string Code { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Moonlight.Shared.Http.Requests.Auth;
|
|
||||||
|
|
||||||
public class RefreshRequest
|
|
||||||
{
|
|
||||||
[Required(ErrorMessage = "You need to provide a refresh token")]
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
namespace Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
public class CheckResponse
|
public class CheckResponse
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public string[] Permissions { get; set; }
|
public string Permissions { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
|
public class LoginCompleteResponse
|
||||||
|
{
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
namespace Moonlight.Shared.Http.Responses.Auth;
|
||||||
|
|
||||||
public class OAuth2StartResponse
|
public class LoginStartResponse
|
||||||
{
|
{
|
||||||
public string Endpoint { get; set; }
|
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
|
public string Endpoint { get; set; }
|
||||||
public string RedirectUri { get; set; }
|
public string RedirectUri { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
public class OAuth2HandleResponse
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
public DateTime ExpiresAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Moonlight.Shared.Http.Responses.Auth;
|
|
||||||
|
|
||||||
public class RefreshResponse
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
public DateTime ExpiresAt { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Moonlight.Shared.Http.Responses.OAuth2;
|
|
||||||
|
|
||||||
public class InfoResponse
|
|
||||||
{
|
|
||||||
public string Username { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Shared.Http.Responses.OAuth2;
|
||||||
|
|
||||||
|
public class OAuth2HandleResponse
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user