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

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