Starting updating mooncore dependency usage

This commit is contained in:
2025-02-04 17:09:07 +01:00
parent 1a4864ba00
commit bf5a744499
38 changed files with 1099 additions and 748 deletions

View File

@@ -29,34 +29,18 @@ public class AppConfiguration
public class AuthenticationConfig
{
public string AccessSecret { get; set; } = Formatter.GenerateString(32);
public string RefreshSecret { get; set; } = Formatter.GenerateString(32);
public int AccessDuration { get; set; } = 60;
public int RefreshDuration { get; set; } = 3600;
public string Secret { get; set; } = Formatter.GenerateString(32);
public int TokenDuration { get; set; } = 3600;
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 string Secret { get; set; } = Formatter.GenerateString(32);
public string ClientId { get; set; } = Formatter.GenerateString(8);
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? AccessEndpoint { get; set; }
public string? RefreshEndpoint { get; set; }
}
}

View File

@@ -1,8 +1,6 @@
using MoonCore.Extended.OAuth2.Consumer;
namespace Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Database.Entities;
public class User : IUserModel
public class User
{
public int Id { get; set; }
@@ -10,10 +8,6 @@ public class User : IUserModel
public string Email { 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 AccessToken { get; set; } = "";
public string RefreshToken { get; set; } = "";
public DateTime RefreshTimestamp { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Attributes;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.PermFilter;
using MoonCore.Helpers;
using MoonCore.Models;
using Moonlight.ApiServer.Database.Entities;

View File

@@ -1,8 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using MoonCore.Attributes;
using MoonCore.Authentication;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
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.Shared.Http.Requests.Auth;
using Moonlight.Shared.Http.Responses.Auth;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Http.Controllers.Auth;
@@ -10,206 +20,138 @@ namespace Moonlight.ApiServer.Http.Controllers.Auth;
[Route("api/auth")]
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 IOAuth2Provider[] OAuth2Providers;
private readonly IAuthInterceptor[] AuthInterceptors;
private readonly ILogger<AuthController> Logger;
private readonly DatabaseRepository<User> UserRepository;
public AuthController(
OAuth2Service oAuth2Service,
TokenHelper tokenHelper,
DatabaseRepository<User> userRepository,
AppConfiguration configuration,
ILogger<AuthController> logger,
IOAuth2Provider[] oAuth2Providers,
IAuthInterceptor[] authInterceptors,
AppConfiguration configuration)
DatabaseRepository<User> userRepository
)
{
OAuth2Service = oAuth2Service;
TokenHelper = tokenHelper;
UserRepository = userRepository;
Logger = logger;
OAuth2Providers = oAuth2Providers;
AuthInterceptors = authInterceptors;
Configuration = configuration;
Logger = logger;
UserRepository = userRepository;
}
[HttpGet]
public async Task<OAuth2StartResponse> Start()
[AllowAnonymous]
[HttpGet("start")]
public Task<LoginStartResponse> Start()
{
var data = await OAuth2Service.StartAuthorizing();
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()
var response = new LoginStartResponse()
{
AccessToken = tokenPair.AccessToken,
RefreshToken = tokenPair.RefreshToken,
ExpiresAt = DateTime.UtcNow.AddSeconds(authConfig.AccessDuration)
};
}
[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
ClientId = Configuration.Authentication.OAuth2.ClientId,
RedirectUri = Configuration.Authentication.OAuth2.AuthorizationRedirect ?? Configuration.PublicUrl,
Endpoint = Configuration.Authentication.OAuth2.AuthorizationEndpoint ?? Configuration.PublicUrl + "/oauth2/authorize"
};
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)
};
}
}

View File

@@ -25,7 +25,7 @@ public class FrontendController : Controller
}
[HttpGet("frontend.json")]
public async Task<FrontendConfiguration> GetConfiguration()
public Task<FrontendConfiguration> GetConfiguration()
{
var configuration = new FrontendConfiguration()
{
@@ -39,7 +39,7 @@ public class FrontendController : Controller
configuration.Scripts = AssetService.GetJavascriptAssets();
return configuration;
return Task.FromResult(configuration);
}
[HttpGet("plugins/{assemblyName}")] // TODO: Test this

View 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; }
}

View 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;
}
}

View 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; }
}

View File

@@ -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;
}
}

View File

@@ -24,8 +24,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore.Extended" Version="1.2.6" />
<PackageReference Include="MoonCore" Version="1.8.2" />
<PackageReference Include="MoonCore.Extended" Version="1.2.7" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>

View File

@@ -1,14 +1,14 @@
using System.Reflection;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using MoonCore.Configuration;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.OAuth2.Consumer;
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.Extended.JwtInvalidation;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.PluginFramework.Extensions;
@@ -17,8 +17,6 @@ using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Http.Middleware;
using Moonlight.ApiServer.Implementations.OAuth2;
using Moonlight.ApiServer.Interfaces.Auth;
using Moonlight.ApiServer.Interfaces.OAuth2;
using Moonlight.ApiServer.Interfaces.Startup;
@@ -78,7 +76,7 @@ public class Startup
await RegisterLogging();
await RegisterBase();
await RegisterDatabase();
await RegisterOAuth2();
await RegisterAuth();
await RegisterCaching();
await HookPluginBuild();
await HandleConfigureArguments();
@@ -90,13 +88,11 @@ public class Startup
await PrepareDatabase();
await UseBase();
await UseOAuth2();
await UseBaseMiddleware();
await UseAuth();
await HookPluginConfigure();
await UsePluginAssets();
await MapBase();
await MapOAuth2();
await HookPluginEndpoints();
await WebApplication.RunAsync();
@@ -240,14 +236,6 @@ public class Startup
return Task.CompletedTask;
}
private Task UseBaseMiddleware()
{
WebApplication.UseMiddleware<AuthorizationMiddleware>();
WebApplication.UseMiddleware<ApiAuthenticationMiddleware>();
return Task.CompletedTask;
}
private Task MapBase()
{
WebApplication.MapControllers();
@@ -593,50 +581,56 @@ public class Startup
#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;
configuration.RefreshSecret = Configuration.Authentication.RefreshSecret;
configuration.RefreshDuration = TimeSpan.FromSeconds(Configuration.Authentication.RefreshDuration);
configuration.RefreshInterval = TimeSpan.FromSeconds(Configuration.Authentication.AccessDuration);
configuration.ClientId = Configuration.Authentication.OAuth2.ClientId;
configuration.ClientSecret = Configuration.Authentication.OAuth2.ClientSecret;
configuration.AuthorizeEndpoint = Configuration.PublicUrl + "/api/_auth/oauth2/authorize";
configuration.RedirectUri = Configuration.PublicUrl;
options.InvalidateTimeProvider = async (provider, principal) =>
{
var userIdClaim = principal.Claims.First(x => x.Type == "userId");
var userId = int.Parse(userIdClaim.Value);
var userRepository = provider.GetRequiredService<DatabaseRepository<User>>();
var user = await userRepository.Get().FirstAsync(x => x.Id == userId);
return user.TokenValidTimestamp;
};
});
WebApplicationBuilder.Services.AddScoped<IDataProvider<User>, LocalOAuth2Provider>();
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>>();
WebApplicationBuilder.Services.AddAuthorization();
return Task.CompletedTask;
}
private Task UseOAuth2()
private Task UseAuth()
{
WebApplication.UseOAuth2Authentication<User>();
WebApplication.UseMiddleware<PermissionLoaderMiddleware>();
WebApplication.UseAuthentication();
return Task.CompletedTask;
}
WebApplication.UseJwtInvalidation();
private Task MapOAuth2()
{
WebApplication.MapOAuth2Authentication<User>();
WebApplication.UseAuthorization();
if (!Configuration.Authentication.UseLocalOAuth2)
return Task.CompletedTask;
WebApplication.MapLocalOAuth2Provider<User>();
return Task.CompletedTask;
}

View File

@@ -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();
}
}

View File

@@ -24,10 +24,10 @@
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1" />
<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="MoonCore" Version="1.8.1" />
<PackageReference Include="MoonCore" Version="1.8.2" />
<PackageReference Include="MoonCore.Blazor" Version="1.2.8" />
<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>
<!--

View File

@@ -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));
}

View File

@@ -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;
}
}

View 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
}

View File

@@ -3,11 +3,9 @@ using System.Text.Json;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop;
using MoonCore.Blazor.Extensions;
using MoonCore.Blazor.Services;
using MoonCore.Blazor.Tailwind.Extensions;
using MoonCore.Blazor.Tailwind.Forms;
using MoonCore.Blazor.Tailwind.Forms.Components;
using MoonCore.Blazor.Tailwind.Auth;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.PluginFramework.Extensions;
@@ -16,7 +14,6 @@ using Moonlight.Client.Implementations;
using Moonlight.Client.Interfaces;
using Moonlight.Client.Services;
using Moonlight.Client.UI;
using Moonlight.Client.UI.Forms;
using Moonlight.Shared.Misc;
namespace Moonlight.Client;
@@ -64,8 +61,7 @@ public class Startup
await RegisterLogging();
await RegisterBase();
await RegisterOAuth2();
await RegisterFormComponents();
await RegisterAuthentication();
await RegisterInterfaces();
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<DownloadService>();
WebAssemblyHostBuilder.Services.AddMoonCoreBlazorTailwind();
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
@@ -143,23 +158,6 @@ public class Startup
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
private async Task LoadAssets()
@@ -339,4 +337,18 @@ public class Startup
}
#endregion
#region Authentication
private Task RegisterAuthentication()
{
WebAssemblyHostBuilder.Services.AddAuthorizationCore();
WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState();
WebAssemblyHostBuilder.Services.AddAuthenticationStateManager<RemoteAuthStateManager>();
return Task.CompletedTask;
}
#endregion
}

View File

@@ -7,8 +7,10 @@
"-m-3",
"-mx-2",
"-mx-4",
"-rotate-90",
"-translate-x-1/2",
"-translate-x-full",
"-translate-y-1/2",
"absolute",
"align-middle",
"animate-spin",
@@ -40,8 +42,12 @@
"block",
"border",
"border-0",
"border-2",
"border-b",
"border-b-2",
"border-dashed",
"border-gray-100/10",
"border-gray-600",
"border-gray-700",
"border-gray-700/60",
"border-none",
@@ -64,6 +70,7 @@
"dark:disabled:text-gray-600",
"dark:group-hover:text-gray-400",
"dark:text-gray-100",
"dark:text-gray-400",
"dark:text-gray-500",
"disabled:bg-gray-100",
"disabled:bg-gray-800",
@@ -90,6 +97,7 @@
"flex",
"flex-1",
"flex-col",
"flex-grow",
"flex-nowrap",
"flex-row",
"flex-shrink-0",
@@ -120,6 +128,7 @@
"gap-x-5",
"gap-x-6",
"gap-y-2",
"gap-y-3",
"gap-y-5",
"gap-y-7",
"gap-y-8",
@@ -138,13 +147,16 @@
"h-4",
"h-5",
"h-6",
"h-64",
"h-8",
"h-[20vh]",
"hidden",
"hover:bg-gray-600",
"hover:bg-gray-700",
"hover:bg-gray-800",
"hover:bg-primary-600",
"hover:border-b-2",
"hover:border-gray-500",
"hover:border-gray-600",
"hover:border-primary-500",
"hover:text-gray-100",
@@ -191,6 +203,7 @@
"lg:z-50",
"list-disc",
"m-1",
"m-10",
"m-3",
"max-h-56",
"max-w-3xl",
@@ -211,10 +224,15 @@
"md:grid-cols-3",
"md:h-[40vh]",
"md:items-center",
"md:ms-2",
"md:space-x-2",
"md:space-y-0",
"md:table-cell",
"md:text-3xl",
"me-1",
"me-2",
"me-2.5",
"me-3",
"min-h-full",
"min-w-60",
"ml-2",
@@ -228,6 +246,7 @@
"mr-6",
"ms-0.5",
"ms-1",
"ms-2",
"ms-3",
"mt-1",
"mt-10",
@@ -236,14 +255,14 @@
"mt-3",
"mt-4",
"mt-5",
"mt-6",
"mt-8",
"mt-auto",
"mx-0.5",
"mx-2",
"mx-5",
"mx-auto",
"my-1",
"my-3",
"my-4",
"my-8",
"opacity-0",
"opacity-100",
@@ -262,6 +281,7 @@
"p-5",
"pb-3",
"pb-4",
"pb-6",
"pl-12",
"pl-2",
"pl-3",
@@ -273,9 +293,11 @@
"pr-1",
"pr-3",
"pr-8",
"ps-4",
"pt-0.5",
"pt-5",
"pt-6",
"px-1",
"px-2",
"px-3",
"px-4",
@@ -309,8 +331,11 @@
"shadow-sm",
"shadow-xl",
"shrink-0",
"size-52",
"size-full",
"sm:-mx-6",
"sm:auto-cols-max",
"sm:block",
"sm:col-span-1",
"sm:col-span-2",
"sm:col-span-3",
@@ -344,6 +369,7 @@
"sm:pb-4",
"sm:px-6",
"sm:text-sm",
"space-x-0.5",
"space-x-1",
"space-x-2",
"space-x-5",
@@ -353,8 +379,10 @@
"space-y-4",
"space-y-8",
"sr-only",
"start-1/2",
"static",
"sticky",
"stroke-current",
"table",
"table-auto",
"text-2xl",
@@ -396,6 +424,7 @@
"to-gray-800",
"to-primary-600",
"top-0",
"top-1/2",
"transform",
"transition",
"transition-all",
@@ -413,6 +442,7 @@
"w-24",
"w-32",
"w-4",
"w-40",
"w-5",
"w-6",
"w-8",

View File

@@ -4,27 +4,6 @@
@inject ApplicationAssemblyService ApplicationAssemblyService
<ErrorLogger>
<OAuth2AuthenticationHandler>
<Router AppAssembly="@typeof(App).Assembly" 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>
<ApplicationRouter DefaultLayout="@typeof(MainLayout)"
AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="ApplicationAssemblyService.NavigationAssemblies" />

View File

@@ -1,3 +0,0 @@
@inherits BaseFormComponent<DateTime>
<input @bind="Binder.Value" type="date" autocomplete="off" class="form-input w-full">

View File

@@ -7,7 +7,6 @@
@inherits LayoutComponentBase
@inject IdentityService IdentityService
@inject IServiceProvider ServiceProvider
@inject ILogger<MainLayout> Logger
@inject IAppLoader[] AppLoaders
@@ -40,13 +39,9 @@ else
<main class="py-10">
<div class="px-4 sm:px-6 lg:px-8">
<ErrorHandler>
<PermissionHandler CheckFunction="CheckPermission">
<CascadingValue Value="this" IsFixed="true">
@Body
</CascadingValue>
</PermissionHandler>
</ErrorHandler>
<CascadingValue Value="this" IsFixed="true">
@Body
</CascadingValue>
</div>
</main>
@@ -142,6 +137,4 @@ else
await InvokeAsync(StateHasChanged);
}
private bool CheckPermission(string permission) => IdentityService.HasPermission(permission);
}

View File

@@ -1,9 +1,10 @@
@using Moonlight.Client.Services
@using Microsoft.AspNetCore.Components.Authorization
@using MoonCore.Blazor.Tailwind.Auth
@using Moonlight.Client.UI.Layouts
@inject IdentityService IdentityService
@inject ToastService ToastService
@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">
@if (Layout.ShowMobileNavigation)
@@ -26,26 +27,15 @@
<div class="flex justify-between gap-x-4 lg:gap-x-6 w-full">
<div></div>
<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>
<!-- Profile dropdown -->
<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">
<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="">
<span class="hidden lg:flex lg:items-center">
<span class="ml-4 text-sm font-semibold leading-6 text-gray-100" aria-hidden="true">
@("@" + IdentityService.Username)
@("@" + Username)
</span>
<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"/>
@@ -53,18 +43,7 @@
</span>
</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">
<!-- 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 @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>
@@ -76,8 +55,17 @@
@code
{
[Parameter] public MainLayout Layout { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
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)
{
@@ -109,11 +97,5 @@
}
private async Task Logout()
{
await IdentityService.Logout();
await ToastService.Info("Successfully logged out");
//await Layout.Load();
Navigation.NavigateTo(Navigation.Uri, true);
}
=> await AuthStateManager.Logout();
}

View File

@@ -4,40 +4,19 @@
@using Moonlight.Client.UI.Layouts
@inject NavigationManager Navigation
@inject IdentityService IdentityService
@inject ISidebarItemProvider[] SidebarItemProviders
@{
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 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">
<!--
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 h-16 shrink-0 items-center">a
<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">
@foreach (var group in Items)
{
<li>
@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)
<li>
@if (!string.IsNullOrEmpty(group.Key))
{
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>
<div class="text-xs font-semibold leading-6 text-gray-400">
@group.Key
</div>
}
</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>
</nav>
@@ -88,12 +69,11 @@
</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">
<!-- 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 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>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
@@ -117,14 +97,16 @@
<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">
<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">
<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>
@@ -149,7 +131,7 @@
{
Items = SidebarItemProviders
.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 ?? "")
.OrderByDescending(x => string.IsNullOrEmpty(x.Key))
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Priority).ToArray());

View File

@@ -1,5 +1,5 @@
@page "/admin/api"
@*
@using MoonCore.Attributes
@using MoonCore.Helpers
@using MoonCore.Models
@@ -108,4 +108,4 @@
configuration.WithField(x => x.ExpiresAt);
};
}
}
}*@

View 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");
}
}

View File

@@ -1,71 +1,55 @@
@page "/admin/users"
@page "/admin/users"
@using MoonCore.Attributes
@using MoonCore.Blazor.Tailwind.Forms.Components
@using MoonCore.Helpers
@using MoonCore.Models
@using Moonlight.Shared.Http.Requests.Admin.Users
@using MoonCore.Blazor.Tailwind.Dt
@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"
TCreateForm="CreateUserRequest"
TUpdateForm="UpdateUserRequest"
OnConfigure="OnConfigure">
<View>
<Column TItem="UserDetailResponse" Field="@(x => x.Id)" Title="Id" />
<Column TItem="UserDetailResponse" Field="@(x => x.Username)" Title="Username" />
<Column TItem="UserDetailResponse" Field="@(x => x.Email)" Title="Email" />
</View>
</Crud>
<DataTable @ref="Table" TItem="UserDetailResponse" PageSize="15" LoadItemsPaginatedAsync="LoadData">
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Id)" Name="Id" />
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Username)" Name="Username" />
<DataTableColumn TItem="UserDetailResponse" Field="@(x => x.Email)" Name="Email" />
<DataTableColumn TItem="UserDetailResponse">
<ColumnTemplate>
<div class="flex justify-end">
<a href="/admin/users/update/@(context.Id)" class="text-primary-500 mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</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
{
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 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");
};
await Table.Refresh();
}
);
}
}

View 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");
}
}

View File

@@ -1,14 +1,26 @@
@page "/"
@using Moonlight.Client.Services
@inject IdentityService IdentityService
@using Microsoft.AspNetCore.Components.Authorization
<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%">
Welcome, @(IdentityService.Username)
Welcome, @(Username)
</div>
<div class="text-gray-200 text-2xl">What do you want to do today?</div>
</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;
}
}

View File

@@ -10,8 +10,6 @@
@using MoonCore.Blazor.Tailwind.Components
@using MoonCore.Blazor.Tailwind.Alerts
@using MoonCore.Blazor.Tailwind.Crud
@using MoonCore.Blazor.Tailwind.Forms
@using MoonCore.Blazor.Tailwind.Helpers
@using MoonCore.Blazor.Tailwind.Modals
@using MoonCore.Blazor.Tailwind.Services

View File

@@ -2,8 +2,8 @@ using System.ComponentModel.DataAnnotations;
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; }
}

View File

@@ -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; }
}

View File

@@ -1,8 +1,8 @@
namespace Moonlight.Shared.Http.Responses.Auth;
namespace Moonlight.Shared.Http.Responses.Auth;
public class CheckResponse
{
public string Username { get; set; }
public string Email { get; set; }
public string[] Permissions { get; set; }
public string Permissions { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public class LoginCompleteResponse
{
public string AccessToken { get; set; }
}

View File

@@ -1,8 +1,8 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public class OAuth2StartResponse
public class LoginStartResponse
{
public string Endpoint { get; set; }
public string ClientId { get; set; }
public string Endpoint { get; set; }
public string RedirectUri { get; set; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -1,7 +0,0 @@
namespace Moonlight.Shared.Http.Responses.OAuth2;
public class InfoResponse
{
public string Username { get; set; }
public string Email { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.OAuth2;
public class OAuth2HandleResponse
{
public int UserId { get; set; }
}