Implemented OIDC

This commit is contained in:
Mia Rose Winter 2024-03-11 15:26:03 +01:00
parent 116dd93da2
commit a9269ece30
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
28 changed files with 1842 additions and 497 deletions

View file

@ -19,6 +19,7 @@
<BoardComponent CenterContent="true"> <BoardComponent CenterContent="true">
<BoardCardComponent Heading="@Localizer["Title"]"> <BoardCardComponent Heading="@Localizer["Title"]">
<StatusMessage Message="@Message" /> <StatusMessage Message="@Message" />
<a class="btn btn-primary w-full mt-3" href="/Account/Login">@Localizer["Login_Label"]</a>
</BoardCardComponent> </BoardCardComponent>
</BoardComponent> </BoardComponent>

View file

@ -4,45 +4,45 @@
@using System.Security.Claims @using System.Security.Claims
@using System.Text @using System.Text
@using System.Text.Encodings.Web @using System.Text.Encodings.Web
@using Humanizer
@using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.WebUtilities
@using Wave.Data @using Wave.Data
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject RoleManager<IdentityRole> RoleManager
@inject IUserStore<ApplicationUser> UserStore @inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender<ApplicationUser> EmailSender @inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger @inject ILogger<ExternalLogin> Logger
@inject IStringLocalizer<ExternalLogin> Localizer
<PageTitle>Register</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<StatusMessage Message="@message" /> <StatusMessage Message="@message" />
<h1>Register</h1>
<h2>Associate your @ProviderDisplayName account.</h2>
<hr />
<div class="alert alert-info"> <BoardComponent CenterContent="true">
You've successfully authenticated with <strong>@ProviderDisplayName</strong>. <BoardCardComponent Heading="@Localizer["Title"]">
Please enter an email address for this site below and click the Register button to finish <Alert CanRemove="false" Type="Alert.MessageType.Information">
logging in. <p>
</div> @string.Format(Localizer["Message"], ProviderDisplayName)
</p>
<div class="row"> </Alert>
<div class="col-md-4">
<EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post"> <EditForm Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
<DataAnnotationsValidator/> <DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert" /> <InputLabelComponent LabelText="@Localizer["Email_Label"]" For="() => Input.Email">
<div class="form-floating mb-3"> <InputText @bind-Value="Input.Email" class="input input-bordered w-full"
<InputText @bind-Value="Input.Email" class="form-control" autocomplete="email" placeholder="Please enter your email." /> autocomplete="email" placeholder="@Localizer["Email_Placeholder"]" />
<label for="email" class="form-label">Email</label> </InputLabelComponent>
<ValidationMessage For="() => Input.Email" />
</div> <button type="submit" class="btn btn-primary w-full">
<button type="submit" class="w-100 btn btn-lg btn-primary">Register</button> @Localizer["Submit"]
</button>
</EditForm> </EditForm>
</div> </BoardCardComponent>
</div> </BoardComponent>
@code { @code {
public const string LoginCallbackAction = "LoginCallback"; public const string LoginCallbackAction = "LoginCallback";
@ -67,25 +67,20 @@
private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName; private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() {
{ if (RemoteError is not null) {
if (RemoteError is not null)
{
RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext); RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext);
} }
var info = await SignInManager.GetExternalLoginInfoAsync(); var info = await SignInManager.GetExternalLoginInfoAsync();
if (info is null) if (info is null) {
{
RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext);
} }
externalLoginInfo = info; externalLoginInfo = info;
if (HttpMethods.IsGet(HttpContext.Request.Method)) if (HttpMethods.IsGet(HttpContext.Request.Method)) {
{ if (Action == LoginCallbackAction) {
if (Action == LoginCallbackAction)
{
await OnLoginCallbackAsync(); await OnLoginCallbackAsync();
return; return;
} }
@ -96,8 +91,7 @@
} }
} }
private async Task OnLoginCallbackAsync() private async Task OnLoginCallbackAsync() {
{
// Sign in the user with this external login provider if the user already has a login. // Sign in the user with this external login provider if the user already has a login.
var result = await SignInManager.ExternalLoginSignInAsync( var result = await SignInManager.ExternalLoginSignInAsync(
externalLoginInfo.LoginProvider, externalLoginInfo.LoginProvider,
@ -105,54 +99,71 @@
isPersistent: false, isPersistent: false,
bypassTwoFactor: true); bypassTwoFactor: true);
if (result.Succeeded) if (result.Succeeded) {
{
Logger.LogInformation( Logger.LogInformation(
"{Name} logged in with {LoginProvider} provider.", "{Name} logged in with {LoginProvider} provider.",
externalLoginInfo.Principal.Identity?.Name, externalLoginInfo.Principal.Identity?.Name,
externalLoginInfo.LoginProvider); externalLoginInfo.LoginProvider);
RedirectManager.RedirectTo(ReturnUrl); RedirectManager.RedirectTo(ReturnUrl);
} } else if (result.IsLockedOut) {
else if (result.IsLockedOut)
{
RedirectManager.RedirectTo("Account/Lockout"); RedirectManager.RedirectTo("Account/Lockout");
} }
// If the user does not have an account, then ask the user to create an account. // If the user does not have an account, then ask the user to create an account.
if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) if (externalLoginInfo.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Email or "email") is {} claim) {
{ Input.Email = claim.Value;
Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? "";
} }
} }
private async Task OnValidSubmitAsync() private async Task OnValidSubmitAsync() {
{
var emailStore = GetEmailStore(); var emailStore = GetEmailStore();
var user = CreateUser(); var user = CreateUser();
await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
var info = await SignInManager.GetExternalLoginInfoAsync();
if (info?.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Name or ClaimTypes.GivenName or "name") is {} nameClaim) {
user.FullName = nameClaim.Value;
}
var result = await UserManager.CreateAsync(user); var result = await UserManager.CreateAsync(user);
if (result.Succeeded) if (result.Succeeded) {
{
result = await UserManager.AddLoginAsync(user, externalLoginInfo); result = await UserManager.AddLoginAsync(user, externalLoginInfo);
if (result.Succeeded) if (result.Succeeded) {
{
Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider); Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider);
var userId = await UserManager.GetUserIdAsync(user); string userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); string code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
var callbackUrl = NavigationManager.GetUriWithQueryParameters( string callbackUrl = NavigationManager.GetUriWithQueryParameters(
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> {["userId"] = userId, ["code"] = code}); new Dictionary<string, object?> {["userId"] = userId, ["code"] = code});
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
if (info?.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.Role or "roles") is { } roleClaim) {
var roles = roleClaim.Value.Split(",").Select(s => s.Trim());
foreach (string role in roles) {
try {
string r = role.Titleize();
if (r is "Author" or "Reviewer" or "Moderator" or "Admin" &&
!await RoleManager.RoleExistsAsync(r) &&
!(await RoleManager.CreateAsync(new IdentityRole(r))).Succeeded) {
Logger.LogError("Failed to create role {role}. Failed to assign it to user {name}.", r, user.UserName);
continue;
}
await UserManager.AddToRoleAsync(user, r);
} catch (Exception ex) {
Logger.LogWarning(ex, "Failed to add newly created user {name} to role {role}.", user.UserName, role);
}
}
}
// If account confirmation is required, we need to show the link if we don't have a real email sender // If account confirmation is required, we need to show the link if we don't have a real email sender
if (UserManager.Options.SignIn.RequireConfirmedAccount) if (UserManager.Options.SignIn.RequireConfirmedAccount) {
{
RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email });
} }
@ -164,32 +175,26 @@
message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
} }
private ApplicationUser CreateUser() private ApplicationUser CreateUser() {
{ try {
try
{
return Activator.CreateInstance<ApplicationUser>(); return Activator.CreateInstance<ApplicationUser>();
} } catch {
catch
{
throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
$"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor"); $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
} }
} }
private IUserEmailStore<ApplicationUser> GetEmailStore() private IUserEmailStore<ApplicationUser> GetEmailStore() {
{ if (!UserManager.SupportsUserEmail) {
if (!UserManager.SupportsUserEmail)
{
throw new NotSupportedException("The default UI requires a user store with email support."); throw new NotSupportedException("The default UI requires a user store with email support.");
} }
return (IUserEmailStore<ApplicationUser>) UserStore; return (IUserEmailStore<ApplicationUser>) UserStore;
} }
private sealed class InputModel private sealed class InputModel {
{ [Required] [EmailAddress]
[Required]
[EmailAddress]
public string Email { get; set; } = ""; public string Email { get; set; } = "";
} }
} }

View file

@ -61,13 +61,8 @@
</li> </li>
</ul> </ul>
</BoardCardComponent> </BoardCardComponent>
<!--
<section class="w-80 max-w-xs">
<h2 class="text-2xl lg:text-4xl mb-3">Use another service to log in.</h2>
<hr />
<ExternalLoginPicker /> <ExternalLoginPicker />
</section>
-->
</BoardComponent> </BoardComponent>
@code { @code {

View file

@ -3,6 +3,7 @@
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Identity
@using Wave.Data @using Wave.Data
@using Wave.Utilities
@inject UserManager<ApplicationUser> UserManager @inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@ -10,25 +11,26 @@
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject ILogger<DeletePersonalData> Logger @inject ILogger<DeletePersonalData> Logger
@inject IStringLocalizer<DeletePersonalData> Localizer @inject IStringLocalizer<DeletePersonalData> Localizer
@inject IMessageDisplay Message
<PageTitle>@Localizer["Title"]</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<StatusMessage Message="@message" />
<BoardComponent> <BoardComponent>
<BoardCardComponent Heading="@Localizer["Title"]"> <BoardCardComponent Heading="@Localizer["Title"]">
<Alert Type="Alert.MessageType.Warning" CanRemove="false"> <Alert Type="Alert.MessageType.Warning" CanRemove="false">
<strong class="font-bold">@Localizer["Delete_FinalWarning"]</strong> <strong class="font-bold">@Localizer["Delete_FinalWarning"]</strong>
</Alert> </Alert>
<EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post" class="w-full"> <EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post" class="w-full mt-3">
<DataAnnotationsValidator/> <DataAnnotationsValidator/>
<ValidationSummary class="text-error" role="alert" /> @if (RequirePassword) {
@if (requirePassword) {
<InputLabelComponent LabelText="@Localizer["Delete_Password_Label"]" For="() => Input.Password"> <InputLabelComponent LabelText="@Localizer["Delete_Password_Label"]" For="() => Input.Password">
<InputText type="password" @bind-Value="Input.Password" class="input input-bordered w-full" required <InputText type="password" @bind-Value="Input.Password" class="input input-bordered w-full" required
autocomplete="current-password" placeholder="@Localizer["Delete_Password_Placeholder"]"/> autocomplete="current-password" placeholder="@Localizer["Delete_Password_Placeholder"]"/>
</InputLabelComponent> </InputLabelComponent>
} else {
<!-- our model binding fails without zero bound properties -->
<InputText type="hidden" @bind-Value="Input.Password" />
} }
<button class="btn btn-lg btn-error w-full" type="submit">@Localizer["Delete_FinalSubmit"]</button> <button class="btn btn-lg btn-error w-full" type="submit">@Localizer["Delete_FinalSubmit"]</button>
</EditForm> </EditForm>
@ -36,40 +38,42 @@
</BoardComponent> </BoardComponent>
@code { @code {
private string? message; private ApplicationUser User { get; set; } = default!;
private ApplicationUser user = default!; private bool RequirePassword { get; set; }
private bool requirePassword;
[CascadingParameter] [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm] [SupplyParameterFromForm(FormName = "delete-user")]
private InputModel Input { get; set; } = new(); private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
user = await UserAccessor.GetRequiredUserAsync(HttpContext); User = await UserAccessor.GetRequiredUserAsync(HttpContext);
requirePassword = await UserManager.HasPasswordAsync(user); RequirePassword = await UserManager.HasPasswordAsync(User);
} }
private async Task OnValidSubmitAsync() { private async Task OnValidSubmitAsync() {
if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) { if (RequirePassword && !await UserManager.CheckPasswordAsync(User, Input.Password)) {
message = Localizer["Delete_ErrorWrongPassword"]; Message.ShowError(Localizer["Delete_ErrorWrongPassword"]);
return; return;
} }
var result = await UserManager.DeleteAsync(user); var result = await UserManager.DeleteAsync(User);
if (!result.Succeeded) { if (!result.Succeeded) {
throw new InvalidOperationException(Localizer["Delete_ErrorUnknown"]); throw new InvalidOperationException(Localizer["Delete_ErrorUnknown"]);
} }
await SignInManager.SignOutAsync(); await SignInManager.SignOutAsync();
string userId = await UserManager.GetUserIdAsync(user); string userId = await UserManager.GetUserIdAsync(User);
Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
RedirectManager.RedirectToWithStatus("/", Localizer["Delete_Success"], HttpContext); Message.ShowSuccess(Localizer["Delete_Success"]);
RedirectManager.RedirectTo("/");
} }
private sealed class InputModel { private sealed class InputModel {
[DataType(DataType.Password)] public string Password { get; set; } = ""; [DataType(DataType.Password)]
public string Password { get; set; } = "";
} }
} }

View file

@ -9,22 +9,29 @@
@inject IdentityUserAccessor UserAccessor @inject IdentityUserAccessor UserAccessor
@inject IUserStore<ApplicationUser> UserStore @inject IUserStore<ApplicationUser> UserStore
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<ExternalLogin> Localizer
<PageTitle>Manage your external logins</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<StatusMessage/> <StatusMessage/>
@if (currentLogins?.Count > 0)
{ @if (CurrentLogins?.Count > 0) {
<h3>Registered Logins</h3> <BoardComponent>
<BoardCardComponent Heading="@Localizer["Title"]">
<div class="overflow-x-auto">
<table class="table"> <table class="table">
<thead>
<tr>
<th>Provider</th>
<th></th>
</tr>
</thead>
<tbody> <tbody>
@foreach (var login in currentLogins) @foreach (var login in CurrentLogins) {
{
<tr> <tr>
<td>@login.ProviderDisplayName</td> <td>@login.ProviderDisplayName</td>
<td> <td>
@if (showRemoveButton) @if (ShowRemoveButton) {
{
<form @formname="@($"remove-login-{login.LoginProvider}")" @onsubmit="OnSubmitAsync" method="post"> <form @formname="@($"remove-login-{login.LoginProvider}")" @onsubmit="OnSubmitAsync" method="post">
<AntiforgeryToken/> <AntiforgeryToken/>
<div> <div>
@ -33,9 +40,7 @@
<button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button> <button type="submit" class="btn btn-primary" title="Remove this @login.ProviderDisplayName login from your account">Remove</button>
</div> </div>
</form> </form>
} } else {
else
{
@: &nbsp; @: &nbsp;
} }
</td> </td>
@ -43,17 +48,15 @@
} }
</tbody> </tbody>
</table> </table>
} </div>
@if (otherLogins?.Count > 0) </BoardCardComponent>
{ @if (OtherLogins?.Count > 0) {
<h4>Add another service to log in.</h4> <BoardCardComponent Heading="Add another service to log in.">
<hr />
<form class="form-horizontal" action="Account/Manage/LinkExternalLogin" method="post"> <form class="form-horizontal" action="Account/Manage/LinkExternalLogin" method="post">
<AntiforgeryToken/> <AntiforgeryToken/>
<div> <div>
<p> <p>
@foreach (var provider in otherLogins) @foreach (var provider in OtherLogins) {
{
<button type="submit" class="btn btn-primary" name="Provider" value="@provider.Name" title="Log in using your @provider.DisplayName account"> <button type="submit" class="btn btn-primary" name="Provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">
@provider.DisplayName @provider.DisplayName
</button> </button>
@ -61,74 +64,67 @@
</p> </p>
</div> </div>
</form> </form>
</BoardCardComponent>
}
</BoardComponent>
} }
@code { @code {
public const string LinkLoginCallbackAction = "LinkLoginCallback"; public const string LinkLoginCallbackAction = "LinkLoginCallback";
private ApplicationUser user = default!; private ApplicationUser User { get; set; } = default!;
private IList<UserLoginInfo>? currentLogins; private IList<UserLoginInfo>? CurrentLogins { get; set; }
private IList<AuthenticationScheme>? otherLogins; private IList<AuthenticationScheme>? OtherLogins { get; set; }
private bool showRemoveButton; private bool ShowRemoveButton { get; set; }
[CascadingParameter] [CascadingParameter]
private HttpContext HttpContext { get; set; } = default!; private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm] [SupplyParameterFromForm]
private string? LoginProvider { get; set; } private string? LoginProvider { get; set; }
[SupplyParameterFromForm] [SupplyParameterFromForm]
private string? ProviderKey { get; set; } private string? ProviderKey { get; set; }
[SupplyParameterFromQuery] [SupplyParameterFromQuery]
private string? Action { get; set; } private string? Action { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() {
{ User = await UserAccessor.GetRequiredUserAsync(HttpContext);
user = await UserAccessor.GetRequiredUserAsync(HttpContext); CurrentLogins = await UserManager.GetLoginsAsync(User);
currentLogins = await UserManager.GetLoginsAsync(user); OtherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync())
otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
.Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList(); .ToList();
string? passwordHash = null; string? passwordHash = null;
if (UserStore is IUserPasswordStore<ApplicationUser> userPasswordStore) if (UserStore is IUserPasswordStore<ApplicationUser> userPasswordStore) {
{ passwordHash = await userPasswordStore.GetPasswordHashAsync(User, HttpContext.RequestAborted);
passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted);
} }
showRemoveButton = passwordHash is not null || currentLogins.Count > 1; ShowRemoveButton = passwordHash is not null || CurrentLogins.Count > 1;
if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) {
{
await OnGetLinkLoginCallbackAsync(); await OnGetLinkLoginCallbackAsync();
} }
} }
private async Task OnSubmitAsync() private async Task OnSubmitAsync() {
{ var result = await UserManager.RemoveLoginAsync(User, LoginProvider!, ProviderKey!);
var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); if (!result.Succeeded) {
if (!result.Succeeded)
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext);
} }
await SignInManager.RefreshSignInAsync(user); await SignInManager.RefreshSignInAsync(User);
RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext);
} }
private async Task OnGetLinkLoginCallbackAsync() private async Task OnGetLinkLoginCallbackAsync() {
{ var userId = await UserManager.GetUserIdAsync(User);
var userId = await UserManager.GetUserIdAsync(user);
var info = await SignInManager.GetExternalLoginInfoAsync(userId); var info = await SignInManager.GetExternalLoginInfoAsync(userId);
if (info is null) if (info is null) {
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext);
} }
var result = await UserManager.AddLoginAsync(user, info); var result = await UserManager.AddLoginAsync(User, info);
if (!result.Succeeded) if (!result.Succeeded) {
{
RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext);
} }
@ -137,4 +133,5 @@
RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext);
} }
} }

View file

@ -8,71 +8,62 @@
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor @inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<SetPassword> Localizer
<PageTitle>Set password</PageTitle> <PageTitle>@Localizer["Title"]</PageTitle>
<h3>Set your password</h3> <StatusMessage Message="@Message"/>
<StatusMessage Message="@message" /> <BoardComponent>
<p class="text-info"> <BoardCardComponent Heading="@Localizer["Title"]">
You do not have a local username/password for this site. Add a local <Alert CanRemove="false" Type="Alert.MessageType.Information">
account so you can log in without an external login. <p>@Localizer["Message"]</p>
</p> </Alert>
<div class="row">
<div class="col-md-6">
<EditForm Model="Input" FormName="set-password" OnValidSubmit="OnValidSubmitAsync" method="post"> <EditForm Model="Input" FormName="set-password" OnValidSubmit="OnValidSubmitAsync" method="post">
<DataAnnotationsValidator/> <DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert" /> <InputLabelComponent LabelText="@Localizer["NewPassword_Label"]" For="() => Input.NewPassword">
<div class="form-floating mb-3"> <InputText type="password" @bind-Value="Input.NewPassword" class="input input-bordered w-full" autocomplete="new-password"
<InputText type="password" @bind-Value="Input.NewPassword" class="form-control" autocomplete="new-password" placeholder="Please enter your new password." /> required aria-required="true" placeholder="@Localizer["NewPassword_Placeholder"]"/>
<label for="new-password" class="form-label">New password</label> </InputLabelComponent>
<ValidationMessage For="() => Input.NewPassword" class="text-danger" /> <InputLabelComponent LabelText="@Localizer["ConfirmPassword_Label"]" For="() => Input.ConfirmPassword">
</div> <InputText type="password" @bind-Value="Input.ConfirmPassword" class="input input-bordered w-full" autocomplete="new-password"
<div class="form-floating mb-3"> required aria-required="true" placeholder="@Localizer["ConfirmPassword_Placeholder"]"/>
<InputText type="password" @bind-Value="Input.ConfirmPassword" class="form-control" autocomplete="new-password" placeholder="Please confirm your new password." /> </InputLabelComponent>
<label for="confirm-password" class="form-label">Confirm password</label> <button type="submit" class="btn btn-primary w-full">
<ValidationMessage For="() => Input.ConfirmPassword" class="text-danger" /> @Localizer["ChangePassword_Submit"]
</div> </button>
<button type="submit" class="w-100 btn btn-lg btn-primary">Set password</button>
</EditForm> </EditForm>
</div> </BoardCardComponent>
</div> </BoardComponent>
@code { @code {
private string? message; private string? Message { get; set; }
private ApplicationUser user = default!; private ApplicationUser User { get; set; } = default!;
[CascadingParameter] [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromForm] [SupplyParameterFromForm] private InputModel Input { get; set; } = new();
private InputModel Input { get; set; } = new();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() {
{ User = await UserAccessor.GetRequiredUserAsync(HttpContext);
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
var hasPassword = await UserManager.HasPasswordAsync(user); bool hasPassword = await UserManager.HasPasswordAsync(User);
if (hasPassword) if (hasPassword) {
{
RedirectManager.RedirectTo("Account/Manage/ChangePassword"); RedirectManager.RedirectTo("Account/Manage/ChangePassword");
} }
} }
private async Task OnValidSubmitAsync() private async Task OnValidSubmitAsync() {
{ var addPasswordResult = await UserManager.AddPasswordAsync(User, Input.NewPassword!);
var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); if (!addPasswordResult.Succeeded) {
if (!addPasswordResult.Succeeded) Message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
{
message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}";
return; return;
} }
await SignInManager.RefreshSignInAsync(user); await SignInManager.RefreshSignInAsync(User);
RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext);
} }
private sealed class InputModel private sealed class InputModel {
{
[Required] [Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)] [DataType(DataType.Password)]
@ -84,4 +75,5 @@
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string? ConfirmPassword { get; set; } public string? ConfirmPassword { get; set; }
} }
} }

View file

@ -9,57 +9,51 @@
@inject IEmailSender<ApplicationUser> EmailSender @inject IEmailSender<ApplicationUser> EmailSender
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<RegisterConfirmation> Localizer
<HeadContent> <HeadContent>
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow">
</HeadContent> </HeadContent>
<PageTitle>Register confirmation</PageTitle> <PageTitle>@Localizer["Title"] </PageTitle>
<h1>Register confirmation</h1>
<StatusMessage Message="@statusMessage"/> <StatusMessage Message="@statusMessage"/>
@if (emailConfirmationLink is not null) <BoardComponent CenterContent="true">
{ <BoardCardComponent Heading="@Localizer["Title"]">
<Alert CanRemove="false" Type="Alert.MessageType.Success">
@if (emailConfirmationLink is not null) {
<p> <p>
This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender. This app does not currently have a real email sender registered, see <a href="https://aka.ms/aspaccountconf">these docs</a> for how to configure a real email sender.
Normally this would be emailed: <a href="@emailConfirmationLink">Click here to confirm your account</a> Normally this would be emailed: <a href="@emailConfirmationLink">Click here to confirm your account</a>
</p> </p>
} else {
<p>@Localizer["Message"]</p>
} }
else </Alert>
{ </BoardCardComponent>
<p>Please check your email to confirm your account.</p> </BoardComponent>
}
@code { @code {
private string? emailConfirmationLink; private string? emailConfirmationLink;
private string? statusMessage; private string? statusMessage;
[CascadingParameter] [CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
private HttpContext HttpContext { get; set; } = default!;
[SupplyParameterFromQuery] [SupplyParameterFromQuery] private string? Email { get; set; }
private string? Email { get; set; }
[SupplyParameterFromQuery] [SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() {
{ if (Email is null) {
if (Email is null)
{
RedirectManager.RedirectTo(""); RedirectManager.RedirectTo("");
} }
var user = await UserManager.FindByEmailAsync(Email); var user = await UserManager.FindByEmailAsync(Email);
if (user is null) if (user is null) {
{
HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
statusMessage = "Error finding user for unspecified email"; statusMessage = "Error finding user for unspecified email";
} } else if (EmailSender is IdentityNoOpEmailSender) {
else if (EmailSender is IdentityNoOpEmailSender)
{
// Once you add a real email sender, you should remove this code that lets you confirm the account // Once you add a real email sender, you should remove this code that lets you confirm the account
var userId = await UserManager.GetUserIdAsync(user); var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
@ -69,4 +63,5 @@ else
new Dictionary<string, object?> {["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl}); new Dictionary<string, object?> {["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl});
} }
} }
} }

View file

@ -4,40 +4,42 @@
@inject SignInManager<ApplicationUser> SignInManager @inject SignInManager<ApplicationUser> SignInManager
@inject IdentityRedirectManager RedirectManager @inject IdentityRedirectManager RedirectManager
@inject IStringLocalizer<ExternalLoginPicker> Localizer
@if (externalLogins.Length == 0) @if (externalLogins.Length > 0) {
{ <BoardCardComponent Heading="@Localizer["Title"]">
<div>
<p>
There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article
about setting up this ASP.NET application to support logging in via external services</a>.
</p>
</div>
}
else
{
<form class="form-horizontal" action="Account/PerformExternalLogin" method="post"> <form class="form-horizontal" action="Account/PerformExternalLogin" method="post">
<div> <div class="flex flex-col space-y-3">
<AntiforgeryToken/> <AntiforgeryToken/>
<input type="hidden" name="ReturnUrl" value="@ReturnUrl"/> <input type="hidden" name="ReturnUrl" value="@ReturnUrl"/>
<p> <p>
@foreach (var provider in externalLogins) @foreach (var provider in externalLogins) {
{ if (provider.Name is "OpenIdConnect") {
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> <button type="submit" class="btn btn-wide btn-primary" name="provider" value="@provider.Name" title="@Localizer["OpenId_Tooltip"]">
@Localizer["OpenId_Label"]
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
</svg>
</button>
} else {
<button type="submit" class="btn btn-wide btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">
@provider.DisplayName
</button>
}
} }
</p> </p>
</div> </div>
</form> </form>
</BoardCardComponent>
} }
@code { @code {
private AuthenticationScheme[] externalLogins = []; private AuthenticationScheme[] externalLogins = [];
[SupplyParameterFromQuery] [SupplyParameterFromQuery] private string? ReturnUrl { get; set; }
private string? ReturnUrl { get; set; }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync() {
{
externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray();
} }
} }

View file

@ -0,0 +1,7 @@
namespace Wave.Data;
public class OidcConfiguration {
public string Authority { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string ClientSecret { get; set; } = string.Empty;
}

View file

@ -11,7 +11,7 @@ public class UserClaimsFactory(
: UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>(userManager, roleManager, options) { : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>(userManager, roleManager, options) {
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user) { protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user) {
var principal = await base.GenerateClaimsAsync(user); var principal = await base.GenerateClaimsAsync(user);
principal.AddClaim(new Claim("Id", user.Id)); // principal.AddClaim(new Claim("Id", user.Id));
principal.AddClaim(new Claim("FullName", user.Name)); principal.AddClaim(new Claim("FullName", user.Name));
return principal; return principal;
} }

View file

@ -5,7 +5,6 @@
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption;
using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
@ -19,6 +18,7 @@
using Wave.Data; using Wave.Data;
using Wave.Services; using Wave.Services;
using Wave.Utilities; using Wave.Utilities;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
string humanReadableVersion = Assembly.GetEntryAssembly()? string humanReadableVersion = Assembly.GetEntryAssembly()?
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()? .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
@ -103,6 +103,30 @@
options.Realm = "Wave API"; options.Realm = "Wave API";
}) })
.AddIdentityCookies(); .AddIdentityCookies();
if (builder.Configuration.GetSection("Oidc").Get<OidcConfiguration>() is {} oidc && !string.IsNullOrWhiteSpace(oidc.Authority)) {
builder.Services.AddAuthentication(options => {
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
}).AddOpenIdConnect(options => {
options.SignInScheme = IdentityConstants.ExternalScheme;
options.Scope.Add(OpenIdConnectScope.OpenIdProfile);
options.Scope.Add(OpenIdConnectScope.OfflineAccess);
options.Authority = oidc.Authority;
options.ClientId = oidc.ClientId;
options.ClientSecret = oidc.ClientSecret;
options.ResponseType = OpenIdConnectResponseType.Code;
options.MapInboundClaims = false;
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.CallbackPath = new PathString("/signin-oidc");
options.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
options.RemoteSignOutPath = new PathString("/signout-oidc");
});
}
#endregion #endregion
@ -114,7 +138,10 @@
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) builder.Services.AddIdentityCore<ApplicationUser>(options => {
options.SignIn.RequireConfirmedAccount = true;
options.ClaimsIdentity.UserIdClaimType = "Id";
})
.AddRoles<IdentityRole>() .AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager() .AddSignInManager()

View file

@ -110,4 +110,7 @@
<data name="Success" xml:space="preserve"> <data name="Success" xml:space="preserve">
<value>Vielen Dank fürs Bestätigen ihrer E-Mail Adresse.</value> <value>Vielen Dank fürs Bestätigen ihrer E-Mail Adresse.</value>
</data> </data>
<data name="Login_Label" xml:space="preserve">
<value>Anmelden</value>
</data>
</root> </root>

View file

@ -110,4 +110,7 @@
<data name="Success" xml:space="preserve"> <data name="Success" xml:space="preserve">
<value>Thank you for confirming your email.</value> <value>Thank you for confirming your email.</value>
</data> </data>
<data name="Login_Label" xml:space="preserve">
<value>Log In</value>
</data>
</root> </root>

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Message" xml:space="preserve">
<value>Sie haben sich erfolgreich mit {0} angemeldet.
Bitte geben Sie nun eine E-Mail Addresse für diese Webseite ein.</value>
</data>
<data name="Email_Label" xml:space="preserve">
<value>E-Mail</value>
</data>
<data name="Email_Placeholder" xml:space="preserve">
<value>name@example.de</value>
</data>
<data name="Submit" xml:space="preserve">
<value>Registrieren</value>
</data>
<data name="Title" xml:space="preserve">
<value>Benutzerkonto Erstellen</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Create Account</value>
</data>
<data name="Message" xml:space="preserve">
<value>You've successfully authenticated with {0}.
Please enter an email address for this site below.</value>
</data>
<data name="Email_Label" xml:space="preserve">
<value>Email</value>
</data>
<data name="Email_Placeholder" xml:space="preserve">
<value>name@example.com</value>
</data>
<data name="Submit" xml:space="preserve">
<value>Register</value>
</data>
</root>

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Passwort Erstellen</value>
</data>
<data name="Message" xml:space="preserve">
<value>Sie haben momentan kein lokales Benutzerkonto/Passwort für diese Seite. Fügen Sie ein lokales Konto hinzu um sich ohne externen Dienst anmelden zu können.</value>
</data>
<data name="ConfirmPassword_Label" xml:space="preserve">
<value>Neues Passwort wiederholen</value>
</data>
<data name="ConfirmPassword_Placeholder" xml:space="preserve">
<value>Bestätigen Sie ihr neues Passwort</value>
</data>
<data name="NewPassword_Label" xml:space="preserve">
<value>Neues Passwort</value>
</data>
<data name="NewPassword_Placeholder" xml:space="preserve">
<value>Geben Sie ihr neues Passwort ein</value>
</data>
<data name="ChangePassword_Submit" xml:space="preserve">
<value>Passwort Ändern</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Create Password</value>
</data>
<data name="Message" xml:space="preserve">
<value>You do not have a local username/password for this site. Add a local account, so you can log in without an external login.</value>
</data>
<data name="NewPassword_Label" xml:space="preserve">
<value>New password</value>
</data>
<data name="NewPassword_Placeholder" xml:space="preserve">
<value>Enter your new password</value>
</data>
<data name="ConfirmPassword_Label" xml:space="preserve">
<value>Confirm new password</value>
</data>
<data name="ConfirmPassword_Placeholder" xml:space="preserve">
<value>Confirm your new Password</value>
</data>
<data name="ChangePassword_Submit" xml:space="preserve">
<value>Change Password</value>
</data>
</root>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Registrierung Bestätigen</value>
</data>
<data name="Message" xml:space="preserve">
<value>Bitte öffnen Sie Ihre E-Mails und bestätigen Sie ihr Benutzerkonto. Sie können sich nicht Anmelden bevor Ihr Konto bestätigt wurde.</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Confirm Registration</value>
</data>
<data name="Message" xml:space="preserve">
<value>Please check your email to confirm your account. You cannot log in before your account has been confirmed.</value>
</data>
</root>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Anmelden über externen Dienst</value>
</data>
<data name="OpenId_Tooltip" xml:space="preserve">
<value>Melden Sie sich über ihren OpenID Connect Dienst an</value>
</data>
</root>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 1.3
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">1.3</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1">this is my long string</data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
[base64 mime encoded serialized .NET Framework object]
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
[base64 mime encoded string representing a byte array form of the .NET Framework object]
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Title" xml:space="preserve">
<value>Log In using external service</value>
</data>
<data name="OpenId_Label" xml:space="preserve">
<value>OpenID Connect</value>
</data>
<data name="OpenId_Tooltip" xml:space="preserve">
<value>Log In using OpenID Connect provider</value>
</data>
</root>

View file

@ -17,6 +17,7 @@
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="13.5.0" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="13.5.0" />
<PackageReference Include="MailKit" Version="4.3.0" /> <PackageReference Include="MailKit" Version="4.3.0" />
<PackageReference Include="Markdig" Version="0.34.0" /> <PackageReference Include="Markdig" Version="0.34.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.1" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />

File diff suppressed because one or more lines are too long