mirror of
https://github.com/Molunerfinn/PicGo.git
synced 2026-10-19 16:14:41 +00:00
✨ Feature(gui): configure server upload secret in settings (#1451)
* ✨ Feature(gui): configure server upload secret in settings PicGo Core already supports authenticating `POST /upload` with a shared secret (`Authorization: Bearer <secret>`, `X-PicGo-Secret` or `?secret=`), resolved from `PICGO_SERVER_SECRET` or `settings.server.secret`, but the GUI offered no way to set it. Add a masked secret field with a show/hide toggle to the PicGo Server dialog, next to host and port. The dialog saves the trimmed value as `settings.server.secret`; an empty value disables authentication. Saving already restarts the server, and Core reads the secret from config on `listen()`, so the main process needs no change and `PICGO_SERVER_SECRET` still takes precedence. The secret value is never added to the settings search index. * 🐛 Fix(gui): keep server secret when normalizing settings normalizeSettingsConfig only copied host, port and enable from `settings.server`, so after a reload the renderer state had no secret: the server dialog reopened with an empty secret field, and saving it again (e.g. after changing the port) would have silently cleared the secret and disabled authentication. Keep `secret` (falling back to an empty string) and cover it with a test. * 📝 Docs: document renderer config normalizer whitelist in AGENTS.md The server secret was dropped from renderer state because normalizeSettingsConfig only copies listed fields, and the server dialog re-saves the whole object. Record the rule for future changes: when adding a persisted config field, update the shared types, renderer state type, default value and normalizer, and add a round-trip normalizer test. Also give `settings.server.secret` an explicit empty default to follow that rule. * 📝 Docs: require English PRs and squash merges in AGENTS.md
This commit is contained in:
@@ -49,6 +49,7 @@ Static assets are served from `public/`. In the main process use `getStaticPath`
|
||||
- Renderer-side date/time formatting should use `dayjs` and shared format constants from `src/renderer/utils/consts.ts` (for example `DEFAULT_DATE_TIME_FORMAT`) instead of `Intl.DateTimeFormat` or ad-hoc inline format strings.
|
||||
- Enum-like object constants declared with `as const` (for example status maps, option maps, and value registries) must use PascalCase names, not camelCase. Prefer names like `PicGoCloudRequestStatusValues`, `SettingsAppearanceValues`, or `AppPlatformValues`.
|
||||
- If a renderer → main request mutates persisted config/state without using `saveConfig`, call `notifyAppConfigUpdated()` in main to inform renderers.
|
||||
- Renderer config normalization is a whitelist: `normalizeAppConfig` / `normalizeSettingsConfig` in `src/renderer/store/utils.ts` copy only the fields they list (for example `settings.server` → `host`, `port`, `enable`, `secret`), and anything not listed is silently dropped from renderer state after every reload. When adding a new persisted config field, update all of: the shared types (`src/universal/types/types.d.ts` / `view.d.ts`), the renderer state type (e.g. `SettingsConfigState` in `src/renderer/components/main/settings/utils.ts`), its default value, and the normalizer. Settings dialogs re-save the whole object (e.g. `settings.server`), so a field missing from the normalizer is not just invisible in the UI but gets overwritten with an empty value on the next save. Add a round-trip test (raw config → normalizer → field still present) next to `src/__tests__/renderer/store/settings-normalize.spec.ts`; a component test that feeds drafts directly will not catch this.
|
||||
- Prefer enums over union types for discrete value sets (e.g., encryption methods). Avoid introducing new string literal union types.
|
||||
- Renderer page/component styles should prefer Tailwind utility classes; avoid adding new Vue `<style>` blocks unless there's no reasonable Tailwind equivalent.
|
||||
- New renderer ↔ main request/response APIs should be implemented via RPC routes (see `src/main/events/rpc/routes/system.ts`) with `RPCRouter` + `IRPCActionType` rather than adding ad-hoc IPC modules (e.g. `picgoCloudIPC`).
|
||||
@@ -61,6 +62,8 @@ Place renderer unit specs in `test/unit/specs` with the `.spec.js` suffix; Karma
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
Commits follow the PicGo conventional preset enforced by Husky (`pnpm lint:dpdm` + Commitlint). Stage your changes and run `pnpm cz` to craft messages that pass CI. Pull requests should explain the change, link related issues, and attach UI screenshots or recordings. Note how you validated the work (dev server, build, Karma, Spectron) and call out migration or configuration steps reviewers must perform.
|
||||
- Write pull request titles, descriptions, and review comments in English, even when the conversation with the maintainer is in another language.
|
||||
- Merge pull requests with squash merge (`gh pr merge <number> --squash`), using the PR title as the squash commit subject. Do not use merge commits or rebase merges, and only merge after all required CI checks have passed.
|
||||
|
||||
## Internationalization Tips
|
||||
Add locales by creating `public/i18n/<locale>.yml`, exposing its `LANG_DISPLAY_LABEL`, and registering it in `src/universal/i18n/index.ts`. Typed i18n declarations are generated automatically from `public/i18n/en.yml` into `src/universal/types/i18n.d.ts` and `src/renderer/i18n/i18next.d.ts`.
|
||||
|
||||
@@ -233,6 +233,11 @@ SETTINGS_SET_SERVER_HOST: Set Server Host
|
||||
SETTINGS_SET_SERVER_PORT: Set Server Port
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: Default:127.0.0.1
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: Default:36677
|
||||
SETTINGS_SET_SERVER_SECRET: Set Server Secret
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: Leave empty to disable authentication
|
||||
SETTINGS_TIPS_SERVER_SECRET: "When set, POST /upload requests must send the header Authorization: Bearer <secret> (or X-PicGo-Secret: <secret>). Leave empty to allow uploads without authentication."
|
||||
SETTINGS_SHOW_SECRET: Show secret
|
||||
SETTINGS_HIDE_SECRET: Hide secret
|
||||
SETTINGS_LOG_LEVEL_ALL: All
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: Success
|
||||
SETTINGS_LOG_LEVEL_ERROR: Error
|
||||
|
||||
@@ -233,6 +233,11 @@ SETTINGS_SET_SERVER_HOST: サーバーホストを設定
|
||||
SETTINGS_SET_SERVER_PORT: サーバーポートを設定
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: デフォルト:127.0.0.1
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: デフォルト:36677
|
||||
SETTINGS_SET_SERVER_SECRET: サーバーシークレットを設定
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: 空欄の場合は認証を無効にします
|
||||
SETTINGS_TIPS_SERVER_SECRET: "設定すると、POST /upload リクエストには Authorization: Bearer <secret>(または X-PicGo-Secret: <secret>)ヘッダーが必要になります。空欄の場合は認証なしでアップロードできます。"
|
||||
SETTINGS_SHOW_SECRET: シークレットを表示
|
||||
SETTINGS_HIDE_SECRET: シークレットを隠す
|
||||
SETTINGS_LOG_LEVEL_ALL: すべて
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: 成功
|
||||
SETTINGS_LOG_LEVEL_ERROR: エラー
|
||||
|
||||
@@ -233,6 +233,11 @@ SETTINGS_SET_SERVER_HOST: 서버 호스트 설정
|
||||
SETTINGS_SET_SERVER_PORT: 서버 포트 설정
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: 기본값:127.0.0.1
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: 기본값:36677
|
||||
SETTINGS_SET_SERVER_SECRET: 서버 시크릿 설정
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: 비워 두면 인증을 사용하지 않습니다
|
||||
SETTINGS_TIPS_SERVER_SECRET: "설정하면 POST /upload 요청에 Authorization: Bearer <secret> (또는 X-PicGo-Secret: <secret>) 헤더가 필요합니다. 비워 두면 인증 없이 업로드할 수 있습니다."
|
||||
SETTINGS_SHOW_SECRET: 시크릿 표시
|
||||
SETTINGS_HIDE_SECRET: 시크릿 숨기기
|
||||
SETTINGS_LOG_LEVEL_ALL: 전체
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: 성공
|
||||
SETTINGS_LOG_LEVEL_ERROR: 오류
|
||||
|
||||
@@ -233,6 +233,11 @@ SETTINGS_SET_SERVER_HOST: 设置监听地址
|
||||
SETTINGS_SET_SERVER_PORT: 设置监听端口
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: 推荐默认地址:127.0.0.1
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: 推荐默认端口:36677
|
||||
SETTINGS_SET_SERVER_SECRET: 设置上传鉴权密钥
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: 留空则不开启鉴权
|
||||
SETTINGS_TIPS_SERVER_SECRET: "设置后,调用 POST /upload 需要带上请求头 Authorization: Bearer <密钥>(或 X-PicGo-Secret: <密钥>)。留空则无需鉴权即可上传。"
|
||||
SETTINGS_SHOW_SECRET: 显示密钥
|
||||
SETTINGS_HIDE_SECRET: 隐藏密钥
|
||||
SETTINGS_LOG_LEVEL_ALL: 全部-All
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: 成功-Success
|
||||
SETTINGS_LOG_LEVEL_ERROR: 错误-Error
|
||||
|
||||
@@ -233,6 +233,11 @@ SETTINGS_SET_SERVER_HOST: 設定監聽地址
|
||||
SETTINGS_SET_SERVER_PORT: 設定監聽端口
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: 推薦預設地址:127.0.0.1
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: 推薦預設端口:36677
|
||||
SETTINGS_SET_SERVER_SECRET: 設定上傳驗證金鑰
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: 留空則不啟用驗證
|
||||
SETTINGS_TIPS_SERVER_SECRET: "設定後,呼叫 POST /upload 需要帶上請求標頭 Authorization: Bearer <金鑰>(或 X-PicGo-Secret: <金鑰>)。留空則無需驗證即可上傳。"
|
||||
SETTINGS_SHOW_SECRET: 顯示金鑰
|
||||
SETTINGS_HIDE_SECRET: 隱藏金鑰
|
||||
SETTINGS_LOG_LEVEL_ALL: 全部-All
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: 成功-Success
|
||||
SETTINGS_LOG_LEVEL_ERROR: 錯誤-Error
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { useState } from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
const saveSettingsConfig = vi.fn(async (_path: string, _value?: unknown) => true)
|
||||
|
||||
vi.mock('@/components/main/settings/use-settings-save', () => ({
|
||||
useSettingsSave: () => saveSettingsConfig
|
||||
}))
|
||||
|
||||
import { SettingsServerDialog } from '@/components/main/settings/settings-dialog-server'
|
||||
|
||||
function ServerDialogHarness ({ initialSecret = '' }: { initialSecret?: string }) {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [host, setHost] = useState('127.0.0.1')
|
||||
const [port, setPort] = useState('36677')
|
||||
const [enable, setEnable] = useState(true)
|
||||
const [secret, setSecret] = useState(initialSecret)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setOpen(true)}>reopen</button>
|
||||
<SettingsServerDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
serverHostDraft={host}
|
||||
serverPortDraft={port}
|
||||
serverEnableDraft={enable}
|
||||
serverSecretDraft={secret}
|
||||
onServerHostDraftChange={setHost}
|
||||
onServerPortDraftChange={setPort}
|
||||
onServerEnableDraftChange={setEnable}
|
||||
onServerSecretDraftChange={setSecret}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function getSecretInput () {
|
||||
return screen.getByLabelText('SETTINGS_SET_SERVER_SECRET') as HTMLInputElement
|
||||
}
|
||||
|
||||
describe('SettingsServerDialog — server secret', () => {
|
||||
beforeEach(() => {
|
||||
saveSettingsConfig.mockClear()
|
||||
})
|
||||
|
||||
it('saves the trimmed secret together with host, port and enable', async () => {
|
||||
render(<ServerDialogHarness />)
|
||||
|
||||
fireEvent.change(getSecretInput(), { target: { value: ' my-secret ' } })
|
||||
fireEvent.click(screen.getByText('CONFIRM'))
|
||||
|
||||
await waitFor(() => expect(saveSettingsConfig).toHaveBeenCalledTimes(1))
|
||||
expect(saveSettingsConfig).toHaveBeenCalledWith('settings.server', {
|
||||
host: '127.0.0.1',
|
||||
port: 36677,
|
||||
enable: true,
|
||||
secret: 'my-secret'
|
||||
})
|
||||
})
|
||||
|
||||
it('saves an empty secret when the field is cleared, which disables authentication', async () => {
|
||||
render(<ServerDialogHarness initialSecret='old-secret' />)
|
||||
|
||||
fireEvent.change(getSecretInput(), { target: { value: ' ' } })
|
||||
fireEvent.click(screen.getByText('CONFIRM'))
|
||||
|
||||
await waitFor(() => expect(saveSettingsConfig).toHaveBeenCalledTimes(1))
|
||||
expect(saveSettingsConfig).toHaveBeenCalledWith('settings.server', expect.objectContaining({ secret: '' }))
|
||||
})
|
||||
|
||||
it('masks the secret by default, can reveal it, and masks it again after reopening', async () => {
|
||||
render(<ServerDialogHarness initialSecret='my-secret' />)
|
||||
|
||||
expect(getSecretInput().type).toBe('password')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'SETTINGS_SHOW_SECRET' }))
|
||||
expect(getSecretInput().type).toBe('text')
|
||||
|
||||
fireEvent.click(screen.getByText('CANCEL'))
|
||||
await waitFor(() => expect(screen.queryByLabelText('SETTINGS_SET_SERVER_SECRET')).toBeNull())
|
||||
|
||||
fireEvent.click(screen.getByText('reopen'))
|
||||
await waitFor(() => expect(getSecretInput().type).toBe('password'))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { IConfig } from 'picgo'
|
||||
import { normalizeSettingsConfig } from '@/store/utils'
|
||||
|
||||
function configWithServer (server: Record<string, unknown>): IConfig {
|
||||
return { settings: { server } } as unknown as IConfig
|
||||
}
|
||||
|
||||
describe('normalizeSettingsConfig — server', () => {
|
||||
it('keeps the saved server secret so the settings dialog can show it again', () => {
|
||||
const settings = normalizeSettingsConfig(configWithServer({
|
||||
host: '127.0.0.1',
|
||||
port: 36677,
|
||||
enable: true,
|
||||
secret: 'my-secret'
|
||||
}))
|
||||
|
||||
expect(settings.server).toEqual({
|
||||
host: '127.0.0.1',
|
||||
port: 36677,
|
||||
enable: true,
|
||||
secret: 'my-secret'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to an empty secret when none is saved or the value is not a string', () => {
|
||||
expect(normalizeSettingsConfig(configWithServer({ host: '127.0.0.1', port: 36677, enable: true })).server.secret).toBe('')
|
||||
expect(normalizeSettingsConfig(configWithServer({ host: '127.0.0.1', port: 36677, enable: true, secret: 123 })).server.secret).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -21,9 +23,11 @@ interface SettingsServerDialogProps {
|
||||
serverHostDraft: string
|
||||
serverPortDraft: string
|
||||
serverEnableDraft: boolean
|
||||
serverSecretDraft: string
|
||||
onServerHostDraftChange: (value: string) => void
|
||||
onServerPortDraftChange: (value: string) => void
|
||||
onServerEnableDraftChange: (value: boolean) => void
|
||||
onServerSecretDraftChange: (value: string) => void
|
||||
}
|
||||
|
||||
export function SettingsServerDialog({
|
||||
@@ -32,12 +36,21 @@ export function SettingsServerDialog({
|
||||
serverHostDraft,
|
||||
serverPortDraft,
|
||||
serverEnableDraft,
|
||||
serverSecretDraft,
|
||||
onServerHostDraftChange,
|
||||
onServerPortDraftChange,
|
||||
onServerEnableDraftChange,
|
||||
onServerSecretDraftChange,
|
||||
}: SettingsServerDialogProps) {
|
||||
const { t } = useTranslation()
|
||||
const saveSettingsConfig = useSettingsSave()
|
||||
const [isSecretVisible, setIsSecretVisible] = useState(false)
|
||||
|
||||
// Mask the secret again whenever the dialog closes, so it always reopens hidden.
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) setIsSecretVisible(false)
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const port = Number(serverPortDraft)
|
||||
@@ -45,16 +58,18 @@ export function SettingsServerDialog({
|
||||
host: serverHostDraft,
|
||||
port: Number.isFinite(port) ? port : 36677,
|
||||
enable: serverEnableDraft,
|
||||
// Core trims the secret and treats an empty value as "authentication disabled".
|
||||
secret: serverSecretDraft.trim(),
|
||||
}
|
||||
|
||||
const isSaved = await saveSettingsConfig("settings.server", nextServer)
|
||||
if (isSaved) {
|
||||
onOpenChange(false)
|
||||
handleOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("SETTINGS_SET_PICGO_SERVER")}</DialogTitle>
|
||||
@@ -90,11 +105,40 @@ export function SettingsServerDialog({
|
||||
placeholder={t("SETTINGS_TIP_PLACEHOLDER_PORT")}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="settings-server-secret">{t("SETTINGS_SET_SERVER_SECRET")}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="settings-server-secret"
|
||||
type={isSecretVisible ? "text" : "password"}
|
||||
autoComplete="off"
|
||||
className="pr-10"
|
||||
value={serverSecretDraft}
|
||||
onChange={(event) => onServerSecretDraftChange(event.target.value)}
|
||||
placeholder={t("SETTINGS_TIP_PLACEHOLDER_SECRET")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute top-1/2 right-1 -translate-y-1/2"
|
||||
aria-label={t(isSecretVisible ? "SETTINGS_HIDE_SECRET" : "SETTINGS_SHOW_SECRET")}
|
||||
onClick={() => setIsSecretVisible((prev) => !prev)}
|
||||
>
|
||||
{isSecretVisible ? (
|
||||
<EyeOffIcon className="size-4" />
|
||||
) : (
|
||||
<EyeIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{t("SETTINGS_TIPS_SERVER_SECRET")}</p>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||
{t("CANCEL")}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm}>{t("CONFIRM")}</Button>
|
||||
|
||||
@@ -29,6 +29,7 @@ export function SettingsSectionNetwork({
|
||||
const [serverHostDraft, setServerHostDraft] = useState(settingsConfig.server.host)
|
||||
const [serverPortDraft, setServerPortDraft] = useState(String(settingsConfig.server.port))
|
||||
const [serverEnableDraft, setServerEnableDraft] = useState(settingsConfig.server.enable)
|
||||
const [serverSecretDraft, setServerSecretDraft] = useState(settingsConfig.server.secret ?? "")
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,6 +66,7 @@ export function SettingsSectionNetwork({
|
||||
setServerHostDraft(settingsConfig.server.host)
|
||||
setServerPortDraft(String(settingsConfig.server.port))
|
||||
setServerEnableDraft(settingsConfig.server.enable)
|
||||
setServerSecretDraft(settingsConfig.server.secret ?? "")
|
||||
setServerDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
@@ -91,9 +93,11 @@ export function SettingsSectionNetwork({
|
||||
serverHostDraft={serverHostDraft}
|
||||
serverPortDraft={serverPortDraft}
|
||||
serverEnableDraft={serverEnableDraft}
|
||||
serverSecretDraft={serverSecretDraft}
|
||||
onServerHostDraftChange={setServerHostDraft}
|
||||
onServerPortDraftChange={setServerPortDraft}
|
||||
onServerEnableDraftChange={setServerEnableDraft}
|
||||
onServerSecretDraftChange={setServerSecretDraft}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface SettingsServerConfig {
|
||||
port: number
|
||||
host: string
|
||||
enable: boolean
|
||||
// Shared secret for `POST /upload`; empty or missing disables server authentication.
|
||||
secret?: string
|
||||
}
|
||||
|
||||
export interface SettingsShortcutItem {
|
||||
@@ -338,6 +340,7 @@ export const defaultSettingsConfig: SettingsConfigState = {
|
||||
port: 36677,
|
||||
host: "127.0.0.1",
|
||||
enable: true,
|
||||
secret: "",
|
||||
},
|
||||
startupMode: IStartupMode.HIDE,
|
||||
shortKey: defaultSettingsShortKeyMap,
|
||||
@@ -417,7 +420,7 @@ const settingsSearchKeywordsByItemId: Record<string, string[]> = {
|
||||
"upload-proxy": ["代理", "镜像", "鏡像", "上传代理", "上傳代理", "代理和镜像", "代理與鏡像"],
|
||||
"plugin-proxy": ["插件代理", "外掛代理", "插件安装代理", "外掛安裝代理"],
|
||||
"plugin-mirror": ["插件镜像", "外掛鏡像", "插件安装镜像", "外掛安裝鏡像", "镜像源", "鏡像源"],
|
||||
server: ["服务", "服務", "服务端", "服務端", "端口", "埠", "主机", "主機"],
|
||||
server: ["服务", "服務", "服务端", "服務端", "端口", "埠", "主机", "主機", "密钥", "金鑰", "鉴权", "驗證", "secret", "auth"],
|
||||
"log-level": ["日志级别", "日誌級別", "日志等级", "日誌等級"],
|
||||
"log-size": ["日志大小", "日誌大小", "日志文件大小", "日誌檔案大小"],
|
||||
"open-config-file": ["配置文件", "設定檔", "配置檔", "打开配置", "開啟設定檔"],
|
||||
|
||||
@@ -285,7 +285,9 @@ export function normalizeSettingsConfig (
|
||||
server: {
|
||||
host: normalizeString(rawServer.host, defaultSettingsConfig.server.host),
|
||||
port: normalizeNumber(rawServer.port, defaultSettingsConfig.server.port),
|
||||
enable: normalizeBoolean(rawServer.enable, defaultSettingsConfig.server.enable)
|
||||
enable: normalizeBoolean(rawServer.enable, defaultSettingsConfig.server.enable),
|
||||
// Keep the saved secret: the server dialog re-saves the whole `settings.server` object, so dropping it here would silently clear it (and disable authentication) on the next save.
|
||||
secret: normalizeString(rawServer.secret, '')
|
||||
},
|
||||
startupMode: isSettingsStartupMode(rawSettings.startupMode)
|
||||
? rawSettings.startupMode
|
||||
|
||||
Vendored
+5
@@ -229,6 +229,11 @@ interface ILocales {
|
||||
SETTINGS_SET_SERVER_PORT: string
|
||||
SETTINGS_TIP_PLACEHOLDER_HOST: string
|
||||
SETTINGS_TIP_PLACEHOLDER_PORT: string
|
||||
SETTINGS_SET_SERVER_SECRET: string
|
||||
SETTINGS_TIP_PLACEHOLDER_SECRET: string
|
||||
SETTINGS_TIPS_SERVER_SECRET: string
|
||||
SETTINGS_SHOW_SECRET: string
|
||||
SETTINGS_HIDE_SECRET: string
|
||||
SETTINGS_LOG_LEVEL_ALL: string
|
||||
SETTINGS_LOG_LEVEL_SUCCESS: string
|
||||
SETTINGS_LOG_LEVEL_ERROR: string
|
||||
|
||||
Vendored
+1
@@ -42,6 +42,7 @@ interface IServerConfig {
|
||||
port: number | string
|
||||
host: string
|
||||
enable: boolean
|
||||
secret?: string
|
||||
}
|
||||
|
||||
// Image && PicBed
|
||||
|
||||
Vendored
+1
@@ -23,6 +23,7 @@ interface ISettingForm {
|
||||
port: number
|
||||
host: string
|
||||
enable: boolean
|
||||
secret?: string
|
||||
}
|
||||
startupMode: import('#/types/enum').IStartupMode
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user