Spaces:
Running
Running
| @page "/login" | |
| @layout Layout.EmptyLayout | |
| @rendermode @(new InteractiveServerRenderMode(prerender: false)) | |
| @attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous] | |
| @inject NavigationManager Navigation | |
| @inject AuthenticationStateProvider AuthStateProvider | |
| @inject MenuAuthorizationService MenuAuthorization | |
| @inject IJSRuntime JS | |
| <PageTitle>@AppLocalization.Text("common.signIn", "Sign In") - Taskify</PageTitle> | |
| <div class="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10 sm:px-6 lg:px-8"> | |
| <div class="w-full max-w-md space-y-7 rounded-xl border border-slate-200 bg-white p-8 shadow-sm"> | |
| <div> | |
| <div class="flex justify-center"> | |
| <span class="flex h-12 w-12 items-center justify-center rounded-xl bg-violet-600 text-white shadow-sm"> | |
| <i data-lucide="clipboard-check" class="h-6 w-6"></i> | |
| </span> | |
| </div> | |
| <h2 class="mt-5 text-center text-2xl font-bold text-slate-900">@AppLocalization.Text("common.signIn", "Sign In") - Taskify</h2> | |
| <p class="mt-2 text-center text-sm text-slate-500">@AppLocalization.PageDescription("dashboard", "Continue to your work dashboard.")</p> | |
| </div> | |
| <form method="post" action="/account/login" class="space-y-5"> | |
| <AntiforgeryToken /> | |
| <input type="hidden" name="ReturnUrl" value="@ReturnUrl" /> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="usernameOrEmail" class="block text-sm font-medium text-slate-700 mb-1">@AppLocalization.Text("common.user", "Username or Email")</label> | |
| <input id="usernameOrEmail" name="UsernameOrEmail" required placeholder="@AppLocalization.Text("common.searchPlaceholder", "Enter username or email")" | |
| class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" /> | |
| </div> | |
| <div class="relative"> | |
| <label for="password" class="block text-sm font-medium text-slate-700 mb-1">Password</label> | |
| <input id="password" name="Password" type="@(showPassword ? "text" : "password")" required placeholder="Password" | |
| class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all pr-10" /> | |
| <button type="button" @onclick="TogglePassword" class="absolute right-3 top-[28px] text-slate-400 hover:text-slate-600 focus:outline-none"> | |
| <i data-lucide="@(showPassword ? "eye-off" : "eye")" class="h-5 w-5"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center"> | |
| <input id="rememberMe" name="RememberMe" type="checkbox" value="true" | |
| class="h-4 w-4 rounded border-slate-350 text-slate-900 focus:ring-slate-900 focus:outline-none" /> | |
| <label for="rememberMe" class="ml-2 block text-sm text-slate-600 font-medium">@AppLocalization.Text("common.rememberMe", "Remember me")</label> | |
| </div> | |
| </div> | |
| @if (ShowUnavailableError) | |
| { | |
| <div class="p-3 rounded-lg bg-amber-50 border border-amber-100"> | |
| <p class="text-sm text-amber-800 text-center">@AppLocalization.Text("result.apiUnavailable", "Unable to reach the API server. Start TaskTrackingSystem.WebApi and try again.")</p> | |
| </div> | |
| } | |
| else if (ShowError) | |
| { | |
| <div class="p-3 rounded-lg bg-red-50 border border-red-100"> | |
| <p class="text-sm text-red-600 text-center">@ResultMessages.InvalidCredentials</p> | |
| </div> | |
| } | |
| @if (ShowLoggedOutMessage) | |
| { | |
| <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100"> | |
| <p class="text-sm text-emerald-700 text-center">@AppLocalization.Text("result.signedOut", "You have been signed out successfully.")</p> | |
| </div> | |
| } | |
| else if (ShowResetMessage) | |
| { | |
| <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100"> | |
| <p class="text-sm text-emerald-700 text-center">@AppLocalization.Text("result.passwordUpdated", "Password updated successfully. Please sign in with your new password.")</p> | |
| </div> | |
| } | |
| <p class="text-center text-sm text-slate-600"> | |
| @AppLocalization.Text("auth.needAccount", "Need an account?") | |
| <a href="/register" class="font-medium text-slate-900 hover:underline">@AppLocalization.Text("auth.register", "Register")</a> | |
| </p> | |
| <div> | |
| <button type="submit" class="group relative flex w-full justify-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-violet-700"> | |
| @AppLocalization.Text("common.signIn", "Sign In") | |
| </button> | |
| </div> | |
| <p class="text-center text-sm text-slate-600"> | |
| <a href="/reset-password" class="font-medium text-slate-900 hover:underline">@AppLocalization.Text("auth.resetPassword", "Reset password")</a> | |
| </p> | |
| </form> | |
| </div> | |
| </div> | |
| @code { | |
| [SupplyParameterFromQuery] | |
| private string? ReturnUrl { get; set; } | |
| [SupplyParameterFromQuery(Name = "error")] | |
| private string? Error { get; set; } | |
| [SupplyParameterFromQuery(Name = "loggedOut")] | |
| private string? LoggedOut { get; set; } | |
| [SupplyParameterFromQuery(Name = "reset")] | |
| private string? Reset { get; set; } | |
| private bool ShowError => string.Equals(Error, "invalid", StringComparison.OrdinalIgnoreCase); | |
| private bool ShowUnavailableError => string.Equals(Error, "unavailable", StringComparison.OrdinalIgnoreCase); | |
| private bool ShowLoggedOutMessage => string.Equals(LoggedOut, "true", StringComparison.OrdinalIgnoreCase); | |
| private bool ShowResetMessage => string.Equals(Reset, "true", StringComparison.OrdinalIgnoreCase); | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| if (firstRender) | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| if (authState.User.Identity?.IsAuthenticated == true) | |
| { | |
| Navigation.NavigateTo(await ResolveLandingPageAsync(authState.User), forceLoad: true); | |
| } | |
| } | |
| } | |
| private bool showPassword = false; | |
| private async Task TogglePassword() | |
| { | |
| showPassword = !showPassword; | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task<string> ResolveLandingPageAsync(System.Security.Claims.ClaimsPrincipal user) | |
| { | |
| try | |
| { | |
| var menus = await MenuAuthorization.GetUserMenusAsync(user); | |
| var landing = FindFirstDashboardMenu(menus); | |
| if (!string.IsNullOrWhiteSpace(landing)) | |
| { | |
| return landing; | |
| } | |
| } | |
| catch | |
| { | |
| } | |
| return "/dashboard"; | |
| } | |
| private static string? FindFirstDashboardMenu(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus) | |
| { | |
| foreach (var menu in menus) | |
| { | |
| var found = FindFirstDashboardMenu(menu); | |
| if (!string.IsNullOrWhiteSpace(found)) | |
| { | |
| return found; | |
| } | |
| } | |
| return null; | |
| } | |
| private static string? FindFirstDashboardMenu(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu) | |
| { | |
| if (!string.IsNullOrWhiteSpace(menu.MenuUrl) && | |
| menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return menu.MenuUrl; | |
| } | |
| foreach (var subMenu in menu.SubMenus) | |
| { | |
| var found = FindFirstDashboardMenu(subMenu); | |
| if (!string.IsNullOrWhiteSpace(found)) | |
| { | |
| return found; | |
| } | |
| } | |
| return null; | |
| } | |
| } | |