fix: implement Blazor-native login form to properly update authentication state
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s
TaxBaik CI/CD / build-and-deploy (push) Successful in 2m26s
Problem: JavaScript login form saved tokens to localStorage but didn't notify CustomAuthenticationStateProvider, causing [Authorize] pages to remain in 'loading' state indefinitely. The provider only reads tokens when: 1. GetAuthenticationStateAsync() is called (page load) 2. NotifyAuthenticationStateChanged() is triggered (UI updates) But JavaScript login didn't trigger either, leaving the authentication state stale. Solution: Convert AdminLoginForm from HTML+JavaScript to pure Blazor component. Now the login flow is: 1. User enters credentials in Blazor form 2. HttpClient POST to /api/auth/login 3. Save tokens to localStorage 4. Call CustomAuthenticationStateProvider.LoginAsync() directly 5. Blazor detects auth state change and re-evaluates [Authorize] pages 6. Dashboard [Authorize] page renders successfully Result: Immediate authentication state update, no loading timeout on protected pages. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
var htmlRoot = document.getElementsByTagName('HTML')[0],
|
||||
//save states
|
||||
savePanelStateEnabled = true,
|
||||
|
||||
//mobile operator on
|
||||
mobileOperator = function () {
|
||||
// Check user agent
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const isMobileUserAgent = /iphone|ipad|ipod|android|blackberry|mini|windows\sce|palm/i.test(userAgent);
|
||||
|
||||
// Check for touch support
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// Check screen size (optional)
|
||||
const isSmallScreen = window.innerWidth <= 992; // Adjust the breakpoint as needed
|
||||
|
||||
// Return true if any of the conditions are met
|
||||
return isMobileUserAgent || isTouchDevice || isSmallScreen;
|
||||
},
|
||||
|
||||
//filter
|
||||
filterClass = function (t, e) {
|
||||
return String(t).split(/[^\w-]+/).filter(function (t) {
|
||||
return e.test(t)
|
||||
}).join(' ')
|
||||
},
|
||||
|
||||
//load
|
||||
loadSettings = function () {
|
||||
var t = sessionStorage.getItem('layoutSettings') || '',
|
||||
e = t ? JSON.parse(t) : {};
|
||||
|
||||
// Load theme setting
|
||||
var savedTheme = e.theme || 'light';
|
||||
htmlRoot.setAttribute('data-bs-theme', savedTheme);
|
||||
|
||||
// Load theme style CSS file only if one was saved
|
||||
var themeStyle = e.themeStyle || '';
|
||||
if (themeStyle) {
|
||||
loadThemeStyle(themeStyle);
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
htmlRoot: '',
|
||||
theme: savedTheme,
|
||||
themeStyle: themeStyle
|
||||
}, e)
|
||||
},
|
||||
|
||||
//save
|
||||
saveSettings = function () {
|
||||
// Save root HTML classes
|
||||
layoutSettings.htmlRoot = filterClass(htmlRoot.className, /^(set)-/i);
|
||||
|
||||
// Save theme attribute
|
||||
layoutSettings.theme = htmlRoot.getAttribute('data-bs-theme') || 'light';
|
||||
|
||||
// Save theme style CSS path
|
||||
var themeStyleElement = document.getElementById('theme-style');
|
||||
if (themeStyleElement && themeStyleElement.getAttribute('href')) {
|
||||
// Get complete href attribute
|
||||
layoutSettings.themeStyle = themeStyleElement.getAttribute('href');
|
||||
console.log('Saved theme style:', layoutSettings.themeStyle);
|
||||
} else {
|
||||
layoutSettings.themeStyle = '';
|
||||
console.log('No theme style to save');
|
||||
}
|
||||
|
||||
// Log the full settings object before saving
|
||||
console.log('Saving layout settings:', JSON.stringify(layoutSettings));
|
||||
|
||||
// Save to sessionStorage
|
||||
sessionStorage.setItem("layoutSettings", JSON.stringify(layoutSettings));
|
||||
|
||||
// Show saving indicator
|
||||
savingIndicator();
|
||||
},
|
||||
|
||||
// reset
|
||||
resetSettings = function () {
|
||||
sessionStorage.setItem("layoutSettings", "");
|
||||
// reset data-bs-theme
|
||||
htmlRoot.setAttribute('data-bs-theme', 'light');
|
||||
|
||||
// reset theme style element if it exists
|
||||
const themeStyleElement = document.getElementById('theme-style')
|
||||
if (themeStyleElement) {
|
||||
themeStyleElement.setAttribute('href', '');
|
||||
}
|
||||
|
||||
// refresh page
|
||||
window.location.reload();
|
||||
|
||||
|
||||
},
|
||||
|
||||
//load theme style
|
||||
loadThemeStyle = function (themeStyle) {
|
||||
if (!themeStyle) return;
|
||||
|
||||
// Don't do anything if the URL is empty
|
||||
if (!themeStyle.trim()) return;
|
||||
|
||||
// Get existing theme style if it exists
|
||||
var existingThemeStyle = document.getElementById('theme-style');
|
||||
|
||||
if (existingThemeStyle) {
|
||||
// Update existing theme style's href
|
||||
existingThemeStyle.href = themeStyle;
|
||||
} else {
|
||||
// Create new theme style element if none exists
|
||||
var linkElement = document.createElement('link');
|
||||
linkElement.id = 'theme-style'; // Use the standard ID
|
||||
linkElement.rel = 'stylesheet';
|
||||
linkElement.media = 'screen';
|
||||
linkElement.href = themeStyle;
|
||||
document.head.appendChild(linkElement);
|
||||
|
||||
// Flag to indicate this was loaded from sessionStorage
|
||||
linkElement.setAttribute('data-loaded-from-storage', 'true');
|
||||
}
|
||||
},
|
||||
|
||||
//get page id
|
||||
getPageIdentifier = function () {
|
||||
return window.location.pathname.split('/').pop() || 'index.html';
|
||||
},
|
||||
|
||||
//save panel state
|
||||
savePanelState = function () {
|
||||
if (!savePanelStateEnabled) return;
|
||||
|
||||
var state = [];
|
||||
var columns = document.querySelectorAll('.main-content > .row > [class^="col-"]');
|
||||
columns.forEach(function (column, columnIndex) {
|
||||
var panels = column.querySelectorAll('.panel');
|
||||
panels.forEach(function (panel, position) {
|
||||
var panelHeader = panel.querySelector('.panel-hdr');
|
||||
|
||||
// Save panel classes excluding 'panel' and 'panel-fullscreen'
|
||||
var panelClasses = panel.className.split(' ').filter(function (cls) {
|
||||
return cls !== 'panel' && cls !== 'panel-fullscreen';
|
||||
}).join(' ');
|
||||
|
||||
// Save header classes excluding 'panel-hdr'
|
||||
var headerClasses = panelHeader ? panelHeader.className.split(' ').filter(function (cls) {
|
||||
return cls !== 'panel-hdr';
|
||||
}).join(' ') : '';
|
||||
|
||||
state.push({
|
||||
id: panel.id,
|
||||
column: columnIndex,
|
||||
position: position, // Save position within column
|
||||
classes: panelClasses,
|
||||
headerClasses: headerClasses
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var pageId = getPageIdentifier();
|
||||
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
|
||||
allStates[pageId] = state;
|
||||
sessionStorage.setItem('allPanelStates', JSON.stringify(allStates));
|
||||
savingIndicator();
|
||||
},
|
||||
|
||||
loadPanelState = function () {
|
||||
var pageId = getPageIdentifier();
|
||||
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
|
||||
var savedState = allStates[pageId];
|
||||
|
||||
if (!savedState) return;
|
||||
|
||||
// Use same selector as save function
|
||||
var columns = Array.from(document.querySelectorAll('.main-content > .row > [class^="col-"]'));
|
||||
|
||||
// Store all existing panels in a map before removing them
|
||||
var panelMap = {};
|
||||
columns.forEach(function (column) {
|
||||
var existingPanels = Array.from(column.querySelectorAll('.panel'));
|
||||
existingPanels.forEach(function (panel) {
|
||||
panelMap[panel.id] = panel;
|
||||
panel.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Sort state by column and position
|
||||
savedState.sort(function (a, b) {
|
||||
if (a.column === b.column) {
|
||||
return a.position - b.position;
|
||||
}
|
||||
return a.column - b.column;
|
||||
});
|
||||
|
||||
// Reinsert panels in correct order
|
||||
savedState.forEach(function (item) {
|
||||
var panel = panelMap[item.id];
|
||||
if (panel && columns[item.column]) {
|
||||
// Update panel classes
|
||||
panel.className = 'panel ' + (item.classes || '');
|
||||
|
||||
// Update header classes
|
||||
var panelHeader = panel.querySelector('.panel-hdr');
|
||||
if (panelHeader && item.headerClasses) {
|
||||
panelHeader.className = 'panel-hdr ' + item.headerClasses;
|
||||
}
|
||||
|
||||
// Append to correct column
|
||||
columns[item.column].appendChild(panel);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Reset panel state
|
||||
resetPanelState = function () {
|
||||
var pageId = getPageIdentifier();
|
||||
var allStates = JSON.parse(sessionStorage.getItem('allPanelStates') || '{}');
|
||||
delete allStates[pageId];
|
||||
sessionStorage.setItem('allPanelStates', JSON.stringify(allStates));
|
||||
//refresh page
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
savingIndicator = function () {
|
||||
// Create or get the indicator element
|
||||
let indicator = document.getElementById('saving-indicator');
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('div');
|
||||
indicator.id = 'saving-indicator';
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
// Show saving animation
|
||||
//indicator.textContent = '';
|
||||
indicator.className = 'saving-indicator spinner-border show';
|
||||
|
||||
// After a brief delay, show success and hide
|
||||
setTimeout(() => {
|
||||
//indicator.textContent = '';
|
||||
indicator.className = 'saving-indicator spinner-border show success';
|
||||
setTimeout(() => {
|
||||
indicator.className = 'saving-indicator spinner-border success';
|
||||
}, 500);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
//load page layout settings
|
||||
layoutSettings = loadSettings();
|
||||
layoutSettings.htmlRoot && (htmlRoot.className = layoutSettings.htmlRoot);
|
||||
|
||||
// Load panel settings is triggered just before <script> tag
|
||||
|
||||
|
||||
loadPanelState();
|
||||
Reference in New Issue
Block a user