@@ -2,16 +2,19 @@ import {Request, Response, NextFunction, Plugin, Server} from '@blinkk/root';
22import bodyParser from 'body-parser' ;
33import micromatch from 'micromatch' ;
44import { renderToString } from 'preact-render-to-string' ;
5- import {
6- PasswordPage ,
7- PasswordPageProps ,
8- } from '../components/PasswordPage/PasswordPage.js' ;
5+ import { PasswordPage } from '../components/PasswordPage/PasswordPage.js' ;
96import { generateNonce , setSecurityHeaders } from '../core/csp.js' ;
107import { hashPassword , verifyPassword } from '../core/password.js' ;
118
129const SESSION_COOKIE_HASH = 'password_protect.hash' ;
1310const SESSION_COOKIE_SALT = 'password_protect.salt' ;
1411
12+ export enum PasswordProtectError {
13+ UNKNOWN = 'UNKNOWN' ,
14+ PASSWORD_REQUIRED = 'PASSWORD_REQUIRED' ,
15+ PASSWORD_INVALID = 'PASSWORD_INVALID' ,
16+ }
17+
1518export interface PasswordProtectedRoute {
1619 /**
1720 * URL glob pattern to match (regex not supported yet).
@@ -107,8 +110,9 @@ async function handleProtectedRoute(
107110 ! req . body . password
108111 ) {
109112 res . status ( 400 ) ;
110- renderPasswordPage ( req , res , { error : 'Bad request (no password).' } ) ;
111- return ;
113+ return renderPasswordPage ( req , res , {
114+ error : PasswordProtectError . PASSWORD_REQUIRED ,
115+ } ) ;
112116 }
113117
114118 const password = req . body . password as string ;
@@ -118,24 +122,23 @@ async function handleProtectedRoute(
118122 protectedRoute . password . salt
119123 ) ;
120124 if ( ! isValid ) {
121- renderPasswordPage ( req , res , { error : 'Incorrect password.' } ) ;
122- return ;
125+ return renderPasswordPage ( req , res , {
126+ error : PasswordProtectError . PASSWORD_INVALID ,
127+ } ) ;
123128 }
124129 // Set a session cookie value to verify subsequent requests.
125130 await setSessionCookie ( protectedRoute , res ) ;
126- next ( ) ;
127- return ;
131+ return next ( ) ;
128132 }
129133
130134 // If previously logged in, check the session cookie against the current
131135 // password hash.
132136 const isValid = await verifySessionCookie ( protectedRoute , req ) ;
133137 if ( isValid ) {
134- next ( ) ;
135- return ;
138+ return next ( ) ;
136139 }
137140
138- renderPasswordPage ( req , res ) ;
141+ return renderPasswordPage ( req , res ) ;
139142}
140143
141144/**
@@ -172,12 +175,51 @@ async function verifySessionCookie(
172175export async function renderPasswordPage (
173176 req : Request ,
174177 res : Response ,
175- props ?: Omit < PasswordPageProps , 'nonce' >
178+ options ?: { error ?: PasswordProtectError }
176179) {
177- const nonce = generateNonce ( ) ;
178- const mainHtml = renderToString ( < PasswordPage { ...props } nonce = { nonce } /> ) ;
179- const html = `<!doctype html>\n${ mainHtml } ` ;
180- res . setHeader ( 'content-type' , 'text/html' ) ;
181- setSecurityHeaders ( res , nonce ) ;
182- res . send ( html ) ;
180+ // If there's no `/401` route, fall back to the default password page.
181+ const defaultHandler = ( ) => {
182+ const nonce = generateNonce ( ) ;
183+ res . setHeader ( 'content-type' , 'text/html' ) ;
184+ res . status ( 401 ) ;
185+ setSecurityHeaders ( res , nonce ) ;
186+ const errorMsg = getErrorMessage ( options ?. error ) ;
187+ const mainHtml = renderToString (
188+ < PasswordPage error = { errorMsg } nonce = { nonce } />
189+ ) ;
190+ const html = `<!doctype html>\n${ mainHtml } ` ;
191+ res . send ( html ) ;
192+ } ;
193+
194+ // If the app defines `routes/401.tsx`, render that page.
195+ if ( req . renderer ) {
196+ const [ route , routeParams ] = req . renderer . getRoute ( '/401' ) ;
197+ if ( route ) {
198+ // Add the error type to the req object. Routes that define a `handle()`
199+ // method should be able to read from it to construct an appropriate
200+ // error message for the page.
201+ ( req as any ) . passwordProtect = { error : options ?. error } ;
202+ return req . renderer . handleRoute ( req , res , defaultHandler , route , {
203+ defaultStatusCode : 401 ,
204+ routeParams : routeParams ,
205+ } ) ;
206+ }
207+ }
208+ return defaultHandler ( ) ;
209+ }
210+
211+ /**
212+ * Converts a `PasswordProtectError` to a user-friendly error message.
213+ */
214+ function getErrorMessage ( error ?: PasswordProtectError ) {
215+ if ( ! error ) {
216+ return undefined ;
217+ }
218+ if ( error === PasswordProtectError . PASSWORD_REQUIRED ) {
219+ return 'Bad request (no password).' ;
220+ }
221+ if ( error === PasswordProtectError . PASSWORD_INVALID ) {
222+ return 'Incorrect password.' ;
223+ }
224+ return 'Unknown error.' ;
183225}
0 commit comments