Fix admin auth gap from basePath stripping, add password reveal toggle

Middleware compared request.nextUrl.pathname (basePath already stripped
by Next.js) against paths still prefixed with /shop, so /shop/admin
pages loaded without server-side auth protection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
chris 2026-08-19 11:27:37 -04:00
parent ec2ebca836
commit 517e00aeb1
2 changed files with 19 additions and 4 deletions

View File

@ -10,6 +10,7 @@ export default function AdminLoginPage() {
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [error, setError] = useState('') const [error, setError] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault() e.preventDefault()
@ -34,16 +35,30 @@ export default function AdminLoginPage() {
<h1 className="title is-4" style={{ marginBottom: '1.5rem' }}>Admin Login</h1> <h1 className="title is-4" style={{ marginBottom: '1.5rem' }}>Admin Login</h1>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="field"> <div className="field">
<div className="control"> <div className="control has-icons-right">
<input <input
className="input" className="input"
type="password" type={showPassword ? 'text' : 'password'}
placeholder="Password" placeholder="Password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
autoFocus autoFocus
required required
/> />
<button
type="button"
className="icon is-right"
style={{ pointerEvents: 'auto', cursor: 'pointer', background: 'none', border: 'none' }}
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
tabIndex={-1}
>
{showPassword ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-8-11-8a18.5 18.5 0 0 1 5.06-5.94M9.9 4.24A10.94 10.94 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z"/><circle cx="12" cy="12" r="3"/></svg>
)}
</button>
</div> </div>
</div> </div>
{error && <p className="help is-danger">{error}</p>} {error && <p className="help is-danger">{error}</p>}

View File

@ -28,11 +28,11 @@ async function deriveSessionToken(password: string): Promise<string> {
export async function middleware(request: NextRequest) { export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl const { pathname } = request.nextUrl
if (pathname === '/shop/admin/login' || pathname === '/api/admin/login') { if (pathname === '/admin/login' || pathname === '/api/admin/login') {
return NextResponse.next() return NextResponse.next()
} }
if (pathname.startsWith('/shop/admin') || pathname.startsWith('/api/admin')) { if (pathname.startsWith('/admin') || pathname.startsWith('/api/admin')) {
const token = request.cookies.get(COOKIE)?.value const token = request.cookies.get(COOKIE)?.value
const password = process.env.ADMIN_PASSWORD const password = process.env.ADMIN_PASSWORD