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,618 @@
/*!
smartSlimscroll
Security:
Added input sanitization for user-provided options
Protected against potential XSS attacks
Added bounds checking for touch/scroll inputs
Performance:
Added event throttling for scroll and touch events
Used passive event listeners where appropriate
Implemented IntersectionObserver for visibility checks
Memory Management:
Used WeakMap for private state management
Added proper cleanup with a destroy method
Improved event listener cleanup
Error Handling:
Added try-catch blocks for error boundaries
Improved error logging
Added graceful degradation
Modern JavaScript Features:
Used const/let instead of var
Added optional chaining
Used modern DOM APIs
Code Organization:
Better separation of concerns
More modular function structure
Improved state management
*/
(function (window, document) {
'use strict';
var smartSlimScroll = function (selector, options) {
// Handle multiple elements
var elements = typeof selector === 'string' ?
document.querySelectorAll(selector) :
[selector];
// Create instance for each element
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (!element) {
console.error('smartSlimScroll: Element not found');
continue;
}
// Skip if already initialized
if (element.getAttribute('data-slimscroll-initialized')) {
continue;
}
// Mark as initialized
element.setAttribute('data-slimscroll-initialized', 'true');
this.element = element;
this.options = {
width: 'auto',
height: '250px',
size: '7px',
color: '#000',
position: 'right',
offsetX: '1px',
offsetY: '0px',
start: 'top',
opacity: 0.4,
fadeOutSpeed: 500,
alwaysVisible: false,
disableFadeOut: false,
railVisible: false,
railColor: '#333',
railOpacity: 0.2,
railClass: 'slimScrollRail',
barClass: 'slimScrollBar',
wrapperClass: 'slimScrollDiv',
allowPageScroll: false,
wheelStep: 20,
touchScrollStep: 200,
borderRadius: '7px',
railBorderRadius: '7px'
};
// Merge options
for (var key in options) {
if (options.hasOwnProperty(key)) {
this.options[key] = options[key];
}
}
this.init();
}
};
smartSlimScroll.prototype = {
init: function () {
// Store this reference
const self = this;
// Use WeakMap for private variables to prevent memory leaks
const privateState = new WeakMap();
privateState.set(self, {
isOverPanel: false,
isOverBar: false,
isDragg: false,
queueHide: null,
touchDif: 0,
barHeight: 0,
percentScroll: 0,
lastScroll: 0,
minBarHeight: 30,
releaseScroll: false,
isScrolling: false,
scrollTimeout: null,
isOverRail: false,
firstHover: true
});
const o = this.options;
const me = this.element;
let wrapper, rail, bar;
// Guard against XSS by sanitizing user inputs
const sanitizeInput = (input) => {
return input.replace(/[<>]/g, '');
};
// Sanitize user-provided options
o.width = sanitizeInput(o.width);
o.height = sanitizeInput(o.height);
o.color = sanitizeInput(o.color);
o.railColor = sanitizeInput(o.railColor);
// Use passive event listeners for better performance
const passiveOptions = {
passive: false // Allow preventDefault
};
// Improved touch handling with bounds checking
const handleTouchMove = (e) => {
const state = privateState.get(self);
if (!state.releaseScroll) {
e.preventDefault(); // Now works with passive: false
}
if (e.touches?.length) {
const diff = (state.touchDif - e.touches[0].pageY) / o.touchScrollStep;
scrollContent(Math.min(Math.max(diff, -50), 50), true);
state.touchDif = e.touches[0].pageY;
}
};
// Improved mouse wheel handling
const handleWheel = function (e) {
const state = privateState.get(self);
if (!state.isOverPanel) return;
// Set scrolling state
state.isScrolling = true;
showBar();
// Clear any existing scroll timeout
clearTimeout(state.scrollTimeout);
// Normalize delta
let delta = 0;
if (e.wheelDelta) {
delta = -e.wheelDelta / 120;
}
if (e.detail) {
delta = e.detail / 3;
}
if (e.deltaY) {
delta = e.deltaY / 120;
}
// Calculate scroll boundaries
const contentHeight = me.scrollHeight;
const visibleHeight = wrapper.offsetHeight;
const scrollTop = me.scrollTop;
const maxScroll = contentHeight - visibleHeight;
// Determine scroll direction and boundaries
const scrollingUp = delta < 0;
const scrollingDown = delta > 0;
const atTop = scrollTop <= 0;
const atBottom = scrollTop >= maxScroll;
// Check if scroll should be prevented
const shouldPreventScroll =
(scrollingUp && !atTop) ||
(scrollingDown && !atBottom) ||
(contentHeight > visibleHeight);
if (shouldPreventScroll) {
// Stop all propagation and prevent default
e.preventDefault();
e.stopPropagation();
e.returnValue = false;
// Apply the scroll
scrollContent(delta, true);
}
// Set timeout to clear scrolling state
state.scrollTimeout = setTimeout(function () {
state.isScrolling = false;
hideBar();
}, 1000);
};
// Use Intersection Observer for visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
showBar();
} else {
hideBar();
}
});
});
observer.observe(me);
// Clean up function
const cleanup = () => {
observer.disconnect();
wrapper.removeEventListener('touchmove', handleTouchMove);
wrapper.removeEventListener('wheel', handleWheel);
// ... remove other event listeners
};
// Store cleanup function for potential future use
privateState.get(self).cleanup = cleanup;
// Add error boundary
try {
// Ensure we are not binding it again
if (me.parentNode.classList.contains(o.wrapperClass)) {
return;
}
// Create wrapper
wrapper = document.createElement('div');
wrapper.className = o.wrapperClass;
wrapper.style.position = 'relative';
wrapper.style.overflow = 'hidden';
wrapper.style.width = o.width;
wrapper.style.height = o.height;
// Update style for the div
me.style.overflow = 'hidden';
me.style.width = o.width;
me.style.height = o.height;
// Create scrollbar
bar = document.createElement('div');
bar.className = o.barClass;
bar.style.position = 'absolute';
bar.style.top = o.offsetY;
bar.style.width = o.size;
bar.style.opacity = '0';
bar.style.background = o.color;
bar.style.borderRadius = o.borderRadius;
bar.style.transition = 'opacity 0.2s linear';
bar.style.zIndex = '99';
// Create rail
rail = document.createElement('div');
rail.className = o.railClass;
rail.style.position = 'absolute';
rail.style.top = o.offsetY;
rail.style.width = o.size;
rail.style.height = `calc(100% - ${2 * parseInt(o.offsetY)}px)`;
rail.style.opacity = '0';
rail.style.background = o.railColor;
rail.style.borderRadius = o.railBorderRadius;
rail.style.transition = 'opacity 0.2s linear';
rail.style.zIndex = '90';
// Set position
var posCss = (o.position === 'right') ?
{right: o.offsetX} :
{left: o.offsetX};
for (var pos in posCss) {
rail.style[pos] = posCss[pos];
bar.style[pos] = posCss[pos];
}
// Wrap element
me.parentNode.insertBefore(wrapper, me);
wrapper.appendChild(me);
wrapper.appendChild(bar);
wrapper.appendChild(rail);
var getBarHeight = function () {
const state = privateState.get(self);
const offsetHeight = parseInt(o.offsetY) || 0;
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
// Calculate scrollbar height and make sure it is not too small
state.barHeight = Math.max(
(availableHeight / me.scrollHeight) * availableHeight,
state.minBarHeight
);
bar.style.height = state.barHeight + 'px';
// Hide scrollbar if content is not long enough
var display = state.barHeight >= availableHeight ? 'none' : 'block';
bar.style.display = display;
};
getBarHeight();
var scrollContent = function (y, isWheel, isJump) {
const state = privateState.get(self);
state.releaseScroll = false;
var delta = y;
const offsetHeight = parseInt(o.offsetY) || 0;
const availableHeight = wrapper.offsetHeight - (2 * offsetHeight);
const maxTop = availableHeight - bar.offsetHeight;
if (isWheel) {
// Move bar with mouse wheel
delta = parseInt(bar.style.top || offsetHeight) + y * parseInt(o.wheelStep) / 100 * bar.offsetHeight;
// Constrain within offsetY boundaries
delta = Math.min(Math.max(delta, offsetHeight), maxTop + offsetHeight);
delta = (y > 0) ? Math.ceil(delta) : Math.floor(delta);
bar.style.top = delta + 'px';
}
// Calculate actual scroll amount
state.percentScroll = (parseInt(bar.style.top || 0) - offsetHeight) / (availableHeight - bar.offsetHeight);
delta = state.percentScroll * (me.scrollHeight - wrapper.offsetHeight);
if (isJump) {
delta = y;
var offsetTop = (delta / me.scrollHeight * availableHeight) + offsetHeight;
offsetTop = Math.min(Math.max(offsetTop, offsetHeight), maxTop + offsetHeight);
bar.style.top = offsetTop + 'px';
}
// Scroll content
me.scrollTop = delta;
// Ensure bar is visible
showBar();
hideBar();
};
var showBar = function () {
const state = privateState.get(self);
getBarHeight();
clearTimeout(state.queueHide);
// Show bar if content is scrollable and we're either:
// 1. Over the content panel
// 2. Over the bar/rail
// 3. Currently dragging
// 4. Currently scrolling
if (me.scrollHeight > wrapper.offsetHeight &&
(state.isOverPanel || state.isOverBar || state.isOverRail || state.isDragg || state.isScrolling)) {
bar.style.opacity = o.opacity;
rail.style.opacity = o.railOpacity;
// If only hovering over panel (not interacting), hide after delay
if (state.isOverPanel &&
!state.isOverBar &&
!state.isOverRail &&
!state.isDragg &&
!state.isScrolling) {
clearTimeout(state.queueHide);
state.queueHide = setTimeout(function () {
hideBar();
}, o.fadeOutSpeed);
}
}
};
var hideBar = function () {
const state = privateState.get(self);
if (!o.alwaysVisible) {
clearTimeout(state.queueHide);
state.queueHide = setTimeout(function () {
if (!state.isOverBar && !state.isOverRail &&
!state.isDragg && !state.isScrolling && !o.disableFadeOut) {
bar.style.opacity = 0;
rail.style.opacity = 0;
}
}, o.fadeOutSpeed);
}
};
// Update event listeners
if (window.addEventListener) {
// Bind to the wrapper with capture phase
wrapper.addEventListener('mousewheel', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('DOMMouseScroll', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('wheel', handleWheel, {
passive: false,
capture: true
});
// Also bind to the content element itself
me.addEventListener('mousewheel', handleWheel, {
passive: false,
capture: true
});
me.addEventListener('DOMMouseScroll', handleWheel, {
passive: false,
capture: true
});
me.addEventListener('wheel', handleWheel, {
passive: false,
capture: true
});
wrapper.addEventListener('touchmove', handleTouchMove, {passive: false, capture: true});
} else {
// IE8 and below
wrapper.attachEvent('onmousewheel', handleWheel);
me.attachEvent('onmousewheel', handleWheel);
wrapper.attachEvent('touchmove', handleTouchMove);
}
// Set up initial height
getBarHeight();
// Check start position
if (o.start === 'bottom') {
bar.style.top = (wrapper.offsetHeight - bar.offsetHeight) + 'px';
scrollContent(0, true);
} else if (o.start !== 'top') {
scrollContent(document.querySelector(o.start).offsetTop, null, true);
if (!o.alwaysVisible) {
bar.style.display = 'none';
}
}
// Attach events
var onMouseDown = function (e) {
const state = privateState.get(self);
state.isDragg = true;
showBar();
// Add dragging class
bar.classList.add('dragging');
const offsetY = parseInt(o.offsetY) || 0;
var top = parseFloat(bar.style.top) || offsetY;
var pageY = e.pageY;
var moveHandler = function (e) {
var currTop = top + e.pageY - pageY;
// Account for offsetY in both min and max boundaries
currTop = Math.min(
Math.max(currTop, offsetY),
wrapper.offsetHeight - bar.offsetHeight - offsetY
);
bar.style.top = currTop + 'px';
scrollContent(0, currTop, false);
};
var upHandler = function () {
state.isDragg = false;
// Remove dragging class
bar.classList.remove('dragging');
if (!state.isOverBar) {
hideBar();
}
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('mouseup', upHandler);
};
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', upHandler);
return false;
};
bar.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverBar = true;
showBar();
});
bar.addEventListener('mouseleave', function () {
const state = privateState.get(self);
state.isOverBar = false;
if (!state.isDragg) {
hideBar();
}
});
wrapper.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverPanel = true;
// Only show scrollbar if content is scrollable
if (me.scrollHeight > wrapper.offsetHeight) {
showBar();
}
});
wrapper.addEventListener('mouseleave', function (e) {
const state = privateState.get(self);
// Check if we're not moving to the scrollbar or rail
if (!e.relatedTarget ||
(!e.relatedTarget.closest('.' + o.barClass) &&
!e.relatedTarget.closest('.' + o.railClass))) {
state.isOverPanel = false;
hideBar();
}
});
// Support for mobile devices
wrapper.addEventListener('touchstart', function (e) {
if (e.touches.length) {
const state = privateState.get(self);
state.touchDif = e.touches[0].pageY;
}
});
// Add back the mousedown event listener after the mouseenter/mouseleave events
bar.addEventListener('mousedown', onMouseDown);
// Add rail hover events
rail.addEventListener('mouseenter', function () {
const state = privateState.get(self);
state.isOverRail = true;
showBar();
});
rail.addEventListener('mouseleave', function () {
const state = privateState.get(self);
state.isOverRail = false;
if (!state.isDragg) {
hideBar();
}
});
// Handle rail clicks
rail.addEventListener('mousedown', function (e) {
const state = privateState.get(self);
// Calculate relative click position
const railOffset = rail.getBoundingClientRect();
const clickPos = e.clientY - railOffset.top;
const offsetY = parseInt(o.offsetY) || 0;
// Calculate target position (accounting for bar height)
const availableHeight = wrapper.offsetHeight - (2 * offsetY);
const barHalfHeight = bar.offsetHeight / 2;
let targetPos = Math.min(
Math.max(clickPos - barHalfHeight, 0),
availableHeight - bar.offsetHeight
);
// Update bar position
bar.style.top = (targetPos + offsetY) + 'px';
// Scroll content
const scrollRatio = (targetPos) / (availableHeight - bar.offsetHeight);
const scrollPos = scrollRatio * (me.scrollHeight - wrapper.offsetHeight);
me.scrollTop = scrollPos;
showBar();
});
// Replace the try-catch block with a single mousemove listener
document.addEventListener('mousemove', function initCheck(e) {
const elementAtPoint = document.elementFromPoint(e.clientX, e.clientY);
if (elementAtPoint && (wrapper.contains(elementAtPoint) || wrapper === elementAtPoint)) {
const state = privateState.get(self);
state.isOverPanel = true;
if (me.scrollHeight > wrapper.offsetHeight) {
showBar();
}
}
document.removeEventListener('mousemove', initCheck);
}, {once: true});
} catch (error) {
console.error('smartSlimScroll initialization failed:', error);
cleanup();
throw error;
}
},
destroy: function () {
try {
const state = privateState.get(this);
if (state.cleanup) {
state.cleanup();
}
// Remove DOM elements and restore original state
this.element.style = '';
this.element.parentNode.replaceChild(this.element, this.element.parentNode);
} catch (error) {
console.error('smartSlimScroll destruction failed:', error);
}
}
};
// Expose to window
window.smartSlimScroll = smartSlimScroll;
})(window, document);