fix: implement Blazor-native login form to properly update authentication state
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:
2026-07-03 13:03:53 +09:00
parent 041d3cae96
commit 40cffb3beb
326 changed files with 327714 additions and 47 deletions
@@ -0,0 +1,131 @@
document.addEventListener('DOMContentLoaded', function() {
// Use direct filtering implementation instead of SmartFilter
implementDirectFiltering();
// Function to remove classes with a specific prefix
function removeClassPrefix(elements, prefix) {
elements.forEach(element => {
const classes = Array.from(element.classList);
classes.forEach(cls => {
if (cls.startsWith(prefix)) {
element.classList.remove(cls);
}
});
});
}
// Select all radio inputs with name="contactview"
const radioButtons = document.querySelectorAll('input[type="radio"][name="contactview"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', function() {
const jsContacts = document.querySelector('#js-contacts');
const cards = jsContacts.querySelectorAll('.card');
const colXlElements = jsContacts.querySelectorAll('[class*="col-xl-"]');
const expandButtons = jsContacts.querySelectorAll('.js-expand-btn');
const doubleCardBodies = jsContacts.querySelectorAll('.card-body + .card-body');
if (this.value === 'grid') {
// Handle cards
removeClassPrefix(cards, 'mb-');
cards.forEach(card => card.classList.add('mb-g'));
// Handle col-xl classes
removeClassPrefix(colXlElements, 'col-xl-');
colXlElements.forEach(el => el.classList.add('col-xl-4'));
// Handle expand buttons
expandButtons.forEach(btn => btn.classList.add('d-none'));
// Handle double card bodies
doubleCardBodies.forEach(body => body.classList.add('show'));
}
else if (this.value === 'table') {
// Handle cards
removeClassPrefix(cards, 'mb-');
cards.forEach(card => card.classList.add('mb-1'));
// Handle col-xl classes
removeClassPrefix(colXlElements, 'col-xl-');
colXlElements.forEach(el => el.classList.add('col-xl-12'));
// Handle expand buttons
expandButtons.forEach(btn => btn.classList.remove('d-none'));
// Handle double card bodies
doubleCardBodies.forEach(body => body.classList.remove('show'));
}
});
});
// Direct filtering implementation that doesn't rely on SmartFilter
function implementDirectFiltering() {
const filterInput = document.getElementById('js-filter-contacts');
const clearBtn = document.getElementById('js-clear-filter');
const counterEl = document.getElementById('filter-result-counter');
// Input filtering
filterInput.addEventListener('input', function() {
const filterValue = this.value.toLowerCase();
const cards = document.querySelectorAll('#js-contacts .card');
const columns = document.querySelectorAll('#js-contacts [class*="col-xl-"]');
let visibleCount = 0;
// First, hide all columns
columns.forEach(col => {
col.style.display = 'none';
});
// Filter cards based on their data-filter-tags attribute
cards.forEach(card => {
const filterTags = card.getAttribute('data-filter-tags') || '';
const parentColumn = card.closest('[class*="col-xl-"]');
if (filterValue === '' || filterTags.toLowerCase().includes(filterValue)) {
if (parentColumn) {
parentColumn.style.display = ''; // Show the column
}
visibleCount++;
}
});
// Update UI
if (filterValue) {
counterEl.textContent = `Showing ${visibleCount} of ${cards.length} contacts`;
filterInput.classList.add('border-primary');
clearBtn.classList.remove('d-none');
} else {
counterEl.textContent = '';
filterInput.classList.remove('border-primary');
clearBtn.classList.add('d-none');
}
});
// Clear button functionality
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
filterInput.value = '';
// Show all columns
const columns = document.querySelectorAll('#js-contacts [class*="col-xl-"]');
columns.forEach(col => {
col.style.display = '';
});
// Reset UI
counterEl.textContent = '';
filterInput.classList.remove('border-primary');
clearBtn.classList.add('d-none');
});
// Set up keyboard events for convenience
filterInput.addEventListener('keydown', function(e) {
// Clear on Escape key
if (e.key === 'Escape') {
e.preventDefault();
this.value = '';
clearBtn.click();
}
});
}
});