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,599 @@
import ApexCharts from '../thirdparty/apexchartsWrapper.js';
document.addEventListener('DOMContentLoaded', function() {
'use strict';
// Basic Range Area Chart
if (document.getElementById('basic-range-area-chart')) {
const basicRangeAreaOptions = {
series: [
{
name: 'Temperature Range',
data: [
{ x: 'Jan', y: [5, 15] },
{ x: 'Feb', y: [7, 18] },
{ x: 'Mar', y: [10, 22] },
{ x: 'Apr', y: [15, 25] },
{ x: 'May', y: [18, 30] },
{ x: 'Jun', y: [22, 35] },
{ x: 'Jul', y: [25, 38] },
{ x: 'Aug', y: [23, 36] },
{ x: 'Sep', y: [20, 32] },
{ x: 'Oct', y: [15, 26] },
{ x: 'Nov', y: [10, 20] },
{ x: 'Dec', y: [6, 16] }
]
}
],
chart: {
type: 'rangeArea',
height: 350,
zoom: {
enabled: false
}
},
colors: ['#3F80EA'],
stroke: {
curve: 'straight',
color: 'var(--primary-500)',
width: 1
},
markers: {
hover: {
sizeOffset: 5
}
},
dataLabels: {
enabled: false
},
yaxis: {
labels: {
formatter: function(val) {
return val + '°C';
}
}
},
tooltip: {
shared: false,
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const data = w.globals.initialSeries[seriesIndex].data[dataPointIndex];
return '<div class="p-2">' +
'<span>Month: ' + data.x + '</span><br>' +
'<span>Min: ' + data.y[0] + '°C</span><br>' +
'<span>Max: ' + data.y[1] + '°C</span>' +
'</div>';
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const basicRangeAreaChart = new ApexCharts(
document.getElementById('basic-range-area-chart'),
basicRangeAreaOptions
);
basicRangeAreaChart.render();
}
// Range Area with Datetime X-axis
if (document.getElementById('datetime-range-area-chart')) {
const datetimeRangeAreaOptions = {
series: [
{
name: 'Price Range',
data: [
{
x: new Date('2019-01-01').getTime(),
y: [30, 45]
},
{
x: new Date('2019-02-01').getTime(),
y: [35, 50]
},
{
x: new Date('2019-03-01').getTime(),
y: [40, 55]
},
{
x: new Date('2019-04-01').getTime(),
y: [45, 62]
},
{
x: new Date('2019-05-01').getTime(),
y: [50, 70]
},
{
x: new Date('2019-06-01').getTime(),
y: [55, 72]
},
{
x: new Date('2019-07-01').getTime(),
y: [58, 75]
},
{
x: new Date('2019-08-01').getTime(),
y: [60, 80]
},
{
x: new Date('2019-09-01').getTime(),
y: [55, 75]
},
{
x: new Date('2019-10-01').getTime(),
y: [50, 65]
},
{
x: new Date('2019-11-01').getTime(),
y: [45, 60]
},
{
x: new Date('2019-12-01').getTime(),
y: [40, 55]
},
{
x: new Date('2020-01-01').getTime(),
y: [45, 60]
},
{
x: new Date('2020-02-01').getTime(),
y: [50, 65]
},
{
x: new Date('2020-03-01').getTime(),
y: [55, 70]
},
{
x: new Date('2020-04-01').getTime(),
y: [60, 75]
},
{
x: new Date('2020-05-01').getTime(),
y: [65, 80]
},
{
x: new Date('2020-06-01').getTime(),
y: [70, 85]
},
{
x: new Date('2020-07-01').getTime(),
y: [75, 90]
},
{
x: new Date('2020-08-01').getTime(),
y: [80, 95]
},
{
x: new Date('2020-09-01').getTime(),
y: [75, 90]
},
{
x: new Date('2020-10-01').getTime(),
y: [70, 85]
},
{
x: new Date('2020-11-01').getTime(),
y: [65, 80]
},
{
x: new Date('2020-12-01').getTime(),
y: [60, 75]
}
]
}
],
chart: {
height: 350,
type: 'rangeArea',
toolbar: {
show: true
}
},
stroke: {
curve: 'smooth',
width: 2
},
dataLabels: {
enabled: false
},
markers: {
size: 0,
hover: {
size: 6
}
},
colors: [window.colorMap.info[500].hex],
tooltip: {
y: {
formatter: function(val) {
return "$" + val;
}
}
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.6,
opacityTo: 0.2,
stops: [0, 100]
}
},
xaxis: {
type: 'datetime'
},
yaxis: {
title: {
text: 'Price ($)'
}
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
};
const datetimeRangeAreaChart = new ApexCharts(
document.getElementById('datetime-range-area-chart'),
datetimeRangeAreaOptions
);
datetimeRangeAreaChart.render();
}
// Multiple Range Area Charts
if (document.getElementById('multiple-range-area-chart')) {
const multipleRangeAreaOptions = {
series: [
{
name: 'Temperature New York',
data: [
{
x: 'Jan',
y: [-5, 5]
},
{
x: 'Feb',
y: [-3, 7]
},
{
x: 'Mar',
y: [2, 12]
},
{
x: 'Apr',
y: [8, 18]
},
{
x: 'May',
y: [15, 25]
},
{
x: 'Jun',
y: [20, 30]
},
{
x: 'Jul',
y: [22, 32]
},
{
x: 'Aug',
y: [21, 31]
},
{
x: 'Sep',
y: [16, 26]
},
{
x: 'Oct',
y: [10, 20]
},
{
x: 'Nov',
y: [5, 15]
},
{
x: 'Dec',
y: [-2, 8]
}
]
},
{
name: 'Temperature London',
data: [
{
x: 'Jan',
y: [2, 8]
},
{
x: 'Feb',
y: [3, 9]
},
{
x: 'Mar',
y: [5, 12]
},
{
x: 'Apr',
y: [7, 15]
},
{
x: 'May',
y: [10, 18]
},
{
x: 'Jun',
y: [14, 22]
},
{
x: 'Jul',
y: [16, 24]
},
{
x: 'Aug',
y: [16, 24]
},
{
x: 'Sep',
y: [14, 22]
},
{
x: 'Oct',
y: [10, 18]
},
{
x: 'Nov',
y: [6, 14]
},
{
x: 'Dec',
y: [4, 10]
}
]
}
],
chart: {
height: 350,
type: 'rangeArea',
toolbar: {
show: true
}
},
stroke: {
curve: 'straight',
width: [2, 2]
},
dataLabels: {
enabled: false
},
markers: {
hover: {
sizeOffset: 5
}
},
colors: [window.colorMap.success[500].hex, window.colorMap.warning[500].hex],
fill: {
opacity: [0.5, 0.5],
type: 'solid'
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'category'
},
yaxis: {
title: {
text: 'Temperature (°C)'
},
tooltip: {
enabled: true
}
}
};
const multipleRangeAreaChart = new ApexCharts(
document.getElementById('multiple-range-area-chart'),
multipleRangeAreaOptions
);
multipleRangeAreaChart.render();
}
// Combo Range Area Chart
if (document.getElementById('combo-range-area-chart')) {
const comboRangeAreaOptions = {
series: [
{
name: 'Temperature Range',
type: 'rangeArea',
data: [
{ x: 'Jan', y: [5, 15] },
{ x: 'Feb', y: [7, 18] },
{ x: 'Mar', y: [10, 22] },
{ x: 'Apr', y: [15, 25] },
{ x: 'May', y: [18, 30] },
{ x: 'Jun', y: [22, 35] },
{ x: 'Jul', y: [25, 38] },
{ x: 'Aug', y: [23, 36] },
{ x: 'Sep', y: [20, 32] },
{ x: 'Oct', y: [15, 26] },
{ x: 'Nov', y: [10, 20] },
{ x: 'Dec', y: [6, 16] }
]
},
{
name: 'Average Temperature',
type: 'line',
data: [
{ x: 'Jan', y: 10 },
{ x: 'Feb', y: 12.5 },
{ x: 'Mar', y: 16 },
{ x: 'Apr', y: 20 },
{ x: 'May', y: 24 },
{ x: 'Jun', y: 28.5 },
{ x: 'Jul', y: 31.5 },
{ x: 'Aug', y: 29.5 },
{ x: 'Sep', y: 26 },
{ x: 'Oct', y: 20.5 },
{ x: 'Nov', y: 15 },
{ x: 'Dec', y: 11 }
]
},
{
name: 'Precipitation',
type: 'column',
data: [
{ x: 'Jan', y: 76 },
{ x: 'Feb', y: 85 },
{ x: 'Mar', y: 101 },
{ x: 'Apr', y: 98 },
{ x: 'May', y: 87 },
{ x: 'Jun', y: 105 },
{ x: 'Jul', y: 91 },
{ x: 'Aug', y: 114 },
{ x: 'Sep', y: 94 },
{ x: 'Oct', y: 86 },
{ x: 'Nov', y: 115 },
{ x: 'Dec', y: 91 }
]
}
],
chart: {
height: 350,
type: 'rangeArea',
stacked: false
},
colors: [window.colorMap.primary[500].hex, window.colorMap.danger[500].hex, window.colorMap.primary[500].hex],
stroke: {
curve: 'smooth',
width: [0, 3, 0]
},
fill: {
opacity: [0.2, 1, 1]
},
markers: {
size: [0, 4, 0],
strokeWidth: 2,
hover: {
size: 6
}
},
dataLabels: {
enabled: false
},
plotOptions: {
bar: {
columnWidth: '40%',
borderRadius: 2
}
},
tooltip: {
shared: true,
custom: function({ series, seriesIndex, dataPointIndex, w }) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[dataPointIndex];
const rangeData = w.globals.initialSeries[0].data[dataPointIndex];
const avgTemp = w.globals.initialSeries[1].data[dataPointIndex].y;
const precip = w.globals.initialSeries[2].data[dataPointIndex].y;
return '<div class="p-2">' +
'<span>Month: ' + month + '</span><br>' +
'<span><span style="color:#3F80EA">■</span> Min-Max Temp: ' + rangeData.y[0] + '-' + rangeData.y[1] + '°C</span><br>' +
'<span><span style="color:#E63946">■</span> Avg Temp: ' + avgTemp + '°C</span><br>' +
'<span><span style="color:#6D9EEB">■</span> Precipitation: ' + precip + ' mm</span>' +
'</div>';
}
},
legend: {
position: 'bottom',
horizontalAlign: 'center',
},
grid: {
show: true,
borderColor: window.colorMap.bootstrapVars.bodyColor.rgba(0.08),
strokeDashArray: 5,
position: 'back',
xaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'category',
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
},
yaxis: [
{
seriesName: 'Temperature Range',
title: {
text: 'Temperature (°C)',
style: {
color: window.colorMap.danger[500].hex,
}
},
labels: {
formatter: function(val) {
return val + '°C';
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
},
{
seriesName: 'Average Temperature',
show: false
},
{
opposite: true,
seriesName: 'Precipitation',
title: {
text: 'Precipitation (mm)',
style: {
color: window.colorMap.primary[500].hex,
}
},
labels: {
formatter: function(val) {
return val + ' mm';
},
style: {
colors: window.colorMap.bootstrapVars.bodyColor.hex
}
}
}
]
};
const comboRangeAreaChart = new ApexCharts(
document.getElementById('combo-range-area-chart'),
comboRangeAreaOptions
);
comboRangeAreaChart.render();
}
});