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,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);
|
||||
Reference in New Issue
Block a user