admin/custom: customizable "not found" and "forbidden" error pages
Massimo Melina committed
Oct 28, 2023 at 11:22 UTC
f5932c8a0074ce6944ed9f6a06f862e057deba1c
8 files changed
+51
-37
admin/src/CustomHtmlPage.ts
+7
-4
@@ -4,7 +4,7 @@ import { createElement as h, Fragment, useEffect, useMemo, useState } from 'reac
4
import { Field, SelectField } from '@hfs/mui-grid-form'
5
import { apiCall, useApiEx } from './api'
6
import { Alert, Box } from '@mui/material'
7
-import { Dict, IconBtn, isCtrlKey, modifiedSx, reloadBtn, wikiLink } from './misc';
7
+import { Dict, HTTP_MESSAGES, IconBtn, isCtrlKey, modifiedSx, prefix, reloadBtn, wikiLink } from './misc';
8
import { Save } from '@mui/icons-material'
9
import _ from 'lodash'
10
import { useDebounce } from 'usehooks-ts'
@@ -19,10 +19,13 @@ export default function CustomHtmlPage() {
19
useEffect(() => data && setSaved(data?.sections), [data])
20
useEffect(() => setAll(saved), [saved])
21
const options = useMemo(() => {
22
- const keys = Object.keys(all)
22
+ const keys = _.sortBy(Object.keys(all), x => !isNaN(+x)) // http codes at the bottom
23
if (!keys.includes(section))
24
- setSection(keys?.[0] || '')
25
- return keys.map(x => ({ value: x, label: _.startCase(x) + (all[x]?.trim() ? ' *' : '') }))
24
+ setSection(_.findKey(all, Boolean) || keys?.[0] || '') // prefer any key with content
25
+ return keys.map(x => ({
26
+ value: x,
27
+ label: (prefix('HTTP ', HTTP_MESSAGES[x as any]) || _.startCase(x)) + (all[x]?.trim() ? ' *' : '')
28
+ }))
29
}, [useDebounce(all, 500)])
30
const anyChange = useMemo(() => !_.isEqualWith(saved, all, (a,b) => !a && !b || undefined),
31
[saved, all])
frontend/src/misc.ts
+4
-11
@@ -4,12 +4,12 @@ import React, { createElement as h } from 'react'
4
import { Spinner } from './components'
5
import { newDialog } from './dialog'
6
import { Icon } from './icons'
7
-import { Dict, getHFS, useBatch } from '@hfs/shared'
7
+import { Dict, getHFS, HTTP_MESSAGES, useBatch } from '@hfs/shared'
8
+import { apiCall, useApi } from '@hfs/shared/api'
9
import { state } from './state'
10
import { t } from './i18n'
11
import * as dialogLib from './dialog'
12
import _ from 'lodash'
12
-import { apiCall, setDefaultApiCallOptions, useApi } from '@hfs/shared/api'
13
import { reloadList } from './useFetchList'
14
import { logout } from './login'
15
import { subscribeKey } from 'valtio/utils'
@@ -17,16 +17,9 @@ import { uploadState } from './upload'
17
import { fileShow } from './show'
18
export * from '@hfs/shared'
19
20
-export const ERRORS: Record<number, string> = {
21
- 401: "Unauthorized",
22
- 403: "Forbidden",
23
- 404: "Not found",
24
- 500: "Server error",
25
-}
26
-
20
export function err2msg(err: number | Error) {
28
- return typeof err === 'number' ? ERRORS[err]
29
- : (ERRORS[(err as any).code] || err.message || String(err))
21
+ return typeof err === 'number' ? HTTP_MESSAGES[err]
22
+ : (HTTP_MESSAGES[(err as any).code] || err.message || String(err))
23
}
24
25
export function hIcon(name: string, props?:any) {
frontend/src/useFetchList.ts
+2
-2
@@ -7,7 +7,7 @@ import _ from 'lodash'
7
import { subscribeKey } from 'valtio/utils'
8
import { useIsMounted } from 'usehooks-ts'
9
import { alertDialog } from './dialog'
10
-import { ERRORS } from './misc'
10
+import { HTTP_MESSAGES, xlate } from './misc'
11
import { t } from './i18n'
12
import { useLocation, useNavigate } from 'react-router-dom'
13
@@ -84,7 +84,7 @@ export default function useFetchList() {
84
}
85
if (error) {
86
state.stopSearch?.()
87
- state.error = (ERRORS as any)[error] || String(error)
87
+ state.error = xlate(error, HTTP_MESSAGES)
88
if (error === 401 && snap.username)
89
alertDialog(t('wrong_account', { u: snap.username }, "Account {u} has no access, try another"), 'warning').then()
90
state.loginRequired = error === 401
src/adminApis.ts
+2
-1
@@ -35,6 +35,7 @@ import _ from 'lodash'
35
import { getUpdates, localUpdateAvailable, update, updateSupported } from './update'
36
import { consoleLog } from './consoleLog'
37
import { resolve } from 'path'
38
+import { getErrorSections } from './errorPages'
39
40
export const adminApis: ApiHandlers = {
41
@@ -78,7 +79,7 @@ export const adminApis: ApiHandlers = {
79
get_custom_html() {
80
return {
81
sections: Object.fromEntries([
81
- ...customHtmlSections.map(k => [k,'']),
82
+ ...customHtmlSections.concat(getErrorSections()).map(k => [k,'']),
83
...customHtmlState.sections
84
])
85
}
src/cross-const.ts
+7
@@ -31,3 +31,10 @@ export const HTTP_FAILED_DEPENDENCY = 424
31
export const HTTP_SERVER_ERROR = 500
32
export const HTTP_SERVICE_UNAVAILABLE = 503
33
34
+export const HTTP_MESSAGES: Record<number, string> = {
35
+ [HTTP_UNAUTHORIZED]: "Unauthorized",
36
+ [HTTP_FORBIDDEN]: "Forbidden",
37
+ [HTTP_NOT_FOUND]: "Not found",
38
+ [HTTP_SERVER_ERROR]: "Server error",
39
+}
40
+
src/customHtml.ts
+1
-1
@@ -17,7 +17,7 @@ export const customHtmlState = proxy({
17
export function watchLoadCustomHtml(folder='') {
18
const state = new Map<string, string>()
19
const res = watchLoad(prefix('', folder, '/') + FILE, data => {
20
- const re = /^\[(\w+)] *$/gm
20
+ const re = /^\[([^\]]+)] *$/gm
21
state.clear()
22
if (!data) return
23
let name: string | undefined = 'top'
src/errorPages.ts
new
+25
@@ -0,0 +1,25 @@
1
+import Koa from 'koa'
2
+import { getLangData } from './lang'
3
+import { getSection } from './customHtml'
4
+import { HTTP_FORBIDDEN, HTTP_MESSAGES, HTTP_NOT_FOUND } from './cross'
5
+
6
+const declaredErrorPages = [HTTP_NOT_FOUND, HTTP_FORBIDDEN].map(String)
7
+
8
+export function getErrorSections() {
9
+ return declaredErrorPages
10
+}
11
+
12
+export async function sendErrorPage(ctx: Koa.Context, code: number) {
13
+ ctx.status = code
14
+ const msg = HTTP_MESSAGES[ctx.status]
15
+ if (!msg) return
16
+ const lang = await getLangData(ctx)
17
+ if (!lang) return
18
+ const trans = (Object.values(lang)[0] as any)?.translate
19
+ ctx.body = trans?.[msg] ?? msg
20
+ const errorPage = getSection(String(ctx.status))
21
+ if (!errorPage) return
22
+ if (errorPage.includes('<'))
23
+ ctx.type = 'html'
24
+ ctx.body = errorPage.replace('$MESSAGE', String(ctx.body))
25
+}
src/middlewares.ts
+3
-18
@@ -15,7 +15,7 @@ import {
15
filterMapGenerator,
16
isLocalHost,
17
stream2string,
18
- tryJson
18
+ tryJson, Dict
19
} from './misc'
20
import { zipStreamFromFolder } from './zip'
21
import { serveFile, serveFileNode } from './serveFile'
@@ -37,6 +37,8 @@ import { constants } from 'zlib'
37
import { baseUrl, getHttpsWorkingPort } from './listen'
38
import { defineConfig } from './config'
39
import { getLangData } from './lang'
40
+import { getSection } from './customHtml'
41
+import { sendErrorPage } from './errorPages'
42
43
const forceHttps = defineConfig('force_https', true)
44
const ignoreProxies = defineConfig('ignore_proxies', false)
@@ -157,23 +159,6 @@ export const serveGuiAndSharedFiles: Koa.Middleware = async (ctx, next) => {
159
}
160
161
// to be used with errors whose recipient is possibly human
160
-export async function sendErrorPage(ctx: Koa.Context, code: number) {
161
- ctx.status = code
162
- const msg = (errorMessages as any)[ctx.status]
163
- if (!msg) return
164
- const lang = await getLangData(ctx)
165
- if (!lang) return
166
- const trans = (Object.values(lang)[0] as any)?.translate
167
- if (!trans) return
168
- ctx.body = trans[msg]
169
-}
170
-
171
-const errorMessages = {
172
- [HTTP_NOT_FOUND]: "Not found",
173
- [HTTP_UNAUTHORIZED]: "Unauthorized",
174
- [HTTP_FORBIDDEN]: "Forbidden",
175
-}
176
-
162
async function sendFolderList(node: VfsNode, ctx: Koa.Context) {
163
let { depth=0, folders, prepend } = ctx.query
164
ctx.type = 'text'