Skip to content

Commit 6c35166

Browse files
committed
feat(core-editor): enhance shadowsocks password generation validation
- Upgrade @pasarguard/core-kit to 0.2.2 and @pasarguard/xray-config-kit to 0.3.2 - Add canGenerateShadowsocksPassword validation check before password generation - Prevent password field from being populated for unsupported shadowsocks methods - Add canSave and saveLabel props to CoreEditorLayout and StickySaveBar for flexible save state control - Update password merge logic to validate method compatibility before preserving password - Clean up undefined password field in inbound patch operations - Improve form state management for shadowsocks inbound configuration
1 parent 0f4f021 commit 6c35166

9 files changed

Lines changed: 162 additions & 79 deletions

File tree

dashboard/bun.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@hookform/resolvers": "^5.2.2",
1818
"@monaco-editor/react": "^4.7.0",
1919
"@noble/post-quantum": "^0.5.4",
20-
"@pasarguard/core-kit": "^0.2.0",
20+
"@pasarguard/core-kit": "^0.2.2",
2121
"@radix-ui/react-accordion": "^1.2.12",
2222
"@radix-ui/react-alert-dialog": "^1.1.15",
2323
"@radix-ui/react-avatar": "^1.1.11",

dashboard/src/features/core-editor/components/shell/core-editor-layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface CoreEditorLayoutProps {
99
sectionHeader?: ReactNode
1010
main: ReactNode
1111
dirty: boolean
12+
canSave?: boolean
13+
saveLabel?: string
1214
onSave: () => void
1315
onDiscard: () => void
1416
saving: boolean
@@ -23,6 +25,8 @@ export function CoreEditorLayout({
2325
sectionHeader,
2426
main,
2527
dirty,
28+
canSave,
29+
saveLabel,
2630
onSave,
2731
onDiscard,
2832
saving,
@@ -42,6 +46,8 @@ export function CoreEditorLayout({
4246
</div>
4347
<StickySaveBar
4448
dirty={dirty}
49+
canSave={canSave}
50+
saveLabel={saveLabel}
4551
onSave={onSave}
4652
onDiscard={onDiscard}
4753
saving={saving}

dashboard/src/features/core-editor/components/shell/sticky-save-bar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next'
88

99
interface StickySaveBarProps {
1010
dirty: boolean
11+
canSave?: boolean
12+
saveLabel?: string
1113
onSave: () => void
1214
onDiscard: () => void
1315
saving: boolean
@@ -19,6 +21,8 @@ interface StickySaveBarProps {
1921

2022
export function StickySaveBar({
2123
dirty,
24+
canSave = dirty,
25+
saveLabel,
2226
onSave,
2327
onDiscard,
2428
saving,
@@ -73,8 +77,8 @@ export function StickySaveBar({
7377
{statusLabel}
7478
</TooltipContent>
7579
</Tooltip>
76-
<LoaderButton type="button" size="sm" disabled={!dirty || saving} isLoading={saving} onClick={onSave}>
77-
{t('save', { defaultValue: 'Save' })}
80+
<LoaderButton type="button" size="sm" disabled={!canSave || saving} isLoading={saving} onClick={onSave}>
81+
{saveLabel ?? t('save', { defaultValue: 'Save' })}
7882
</LoaderButton>
7983
</div>
8084
</TooltipProvider>

dashboard/src/features/core-editor/components/xray/xray-inbounds-section.tsx

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { useCoreEditorStore } from '@/features/core-editor/state/core-editor-sto
3131
import { generateWireGuardKeyPair, getWireGuardPublicKey } from '@/utils/wireguard'
3232
import {
3333
buildVlessGenerationOptionsFromInboundForm,
34+
canGenerateShadowsocksPassword,
3435
generateShadowsocksPassword,
3536
generateRealityKeyPair,
3637
generateRealityShortId,
@@ -740,6 +741,7 @@ function shadowsocksMethodFormValue(row: Inbound): string {
740741

741742
function shadowsocksPasswordFormValue(row: Inbound): string {
742743
if (row.protocol !== 'shadowsocks') return ''
744+
if (!canGenerateShadowsocksPassword(shadowsocksMethodFormValue(row))) return ''
743745
const p = 'password' in row ? row.password : undefined
744746
return p === undefined || p === null ? '' : String(p)
745747
}
@@ -764,7 +766,7 @@ function mergeShadowsocksInboundStreamFields(prev: Inbound, next: Inbound): Inbo
764766
const p = prev as { method?: string; password?: string; network?: string | string[] }
765767
const merged = { ...next } as { method?: string; password?: string; network?: string | string[] }
766768
if (p.method !== undefined) merged.method = p.method
767-
if (p.password !== undefined) merged.password = p.password
769+
if (p.password !== undefined && canGenerateShadowsocksPassword(merged.method ?? '')) merged.password = p.password
768770
if (p.network !== undefined) merged.network = p.network
769771
return merged as Inbound
770772
}
@@ -1714,6 +1716,7 @@ export function XrayInboundsSection({ headerAddPulse, headerAddEpoch }: XrayInbo
17141716
if ('network' in patch && patch.network === undefined) delete base.network
17151717
if ('raw' in patch && patch.raw === undefined) delete base.raw
17161718
if ('portMap' in patch && patch.portMap === undefined) delete base.portMap
1719+
if ('password' in patch && patch.password === undefined) delete base.password
17171720
if ('fallbacks' in patch) {
17181721
const fb = patch.fallbacks as Fallback[] | undefined
17191722
if (fb === undefined || (Array.isArray(fb) && fb.length === 0)) delete base.fallbacks
@@ -2024,6 +2027,10 @@ export function XrayInboundsSection({ headerAddPulse, headerAddEpoch }: XrayInbo
20242027

20252028
const generateInboundShadowsocksPassword = () => {
20262029
const methodValue = form.getValues('shadowsocksMethod')
2030+
if (!canGenerateShadowsocksPassword(methodValue)) {
2031+
toast.error(t('coreConfigModal.shadowsocksPasswordGenerationFailed'))
2032+
return
2033+
}
20272034
setIsGeneratingShadowsocksPassword(true)
20282035
try {
20292036
const result = generateShadowsocksPassword(methodValue)
@@ -2586,7 +2593,12 @@ export function XrayInboundsSection({ headerAddPulse, headerAddEpoch }: XrayInbo
25862593
onValueChange={v => {
25872594
field.onChange(v)
25882595
setShadowsocksPasswordJustGenerated(false)
2589-
patchInbound({ method: v as ShadowsocksMethod } as Partial<Inbound>)
2596+
if (canGenerateShadowsocksPassword(v)) {
2597+
patchInbound({ method: v as ShadowsocksMethod } as Partial<Inbound>)
2598+
} else {
2599+
form.setValue('shadowsocksPassword', '')
2600+
patchInbound({ method: v as ShadowsocksMethod, password: undefined } as Partial<Inbound>)
2601+
}
25902602
}}
25912603
>
25922604
<FormControl>
@@ -2639,45 +2651,48 @@ export function XrayInboundsSection({ headerAddPulse, headerAddEpoch }: XrayInbo
26392651
)}
26402652
/>
26412653

2642-
<FormField
2643-
control={form.control}
2644-
name="shadowsocksPassword"
2645-
render={({ field }) => (
2646-
<FormItem className="w-full min-w-0 sm:col-span-2">
2647-
<FormLabel className="text-muted-foreground text-xs font-semibold tracking-wide">{t('coreConfigModal.shadowsocksPassword')}</FormLabel>
2648-
<FormControl>
2649-
<PasswordInput
2650-
dir="ltr"
2651-
autoComplete="new-password"
2652-
className="h-10 w-full"
2653-
value={field.value}
2654-
onChange={e => {
2655-
const v = e.target.value
2656-
field.onChange(v)
2657-
setShadowsocksPasswordJustGenerated(false)
2658-
patchInbound({ password: v } as Partial<Inbound>)
2659-
}}
2660-
/>
2661-
</FormControl>
2662-
<FormMessage />
2663-
</FormItem>
2664-
)}
2665-
/>
2666-
2667-
<div className="sm:col-span-2">
2668-
<LoaderButton
2669-
type="button"
2670-
onClick={generateInboundShadowsocksPassword}
2671-
className="h-10 w-full text-sm font-medium transition-all hover:shadow-md sm:h-11"
2672-
isLoading={isGeneratingShadowsocksPassword}
2673-
loadingText={t('coreConfigModal.generatingShadowsocksPassword')}
2674-
>
2675-
<span className="flex items-center gap-2 truncate">
2676-
{shadowsocksPasswordJustGenerated && <span className="h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 ring-2 ring-green-500/20" />}
2677-
{t('coreConfigModal.generateShadowsocksPassword')}
2678-
</span>
2679-
</LoaderButton>
2680-
</div>
2654+
{canGenerateShadowsocksPassword(form.watch('shadowsocksMethod')) && (
2655+
<>
2656+
<FormField
2657+
control={form.control}
2658+
name="shadowsocksPassword"
2659+
render={({ field }) => (
2660+
<FormItem className="w-full min-w-0 sm:col-span-2">
2661+
<FormLabel className="text-muted-foreground text-xs font-semibold tracking-wide">{t('coreConfigModal.shadowsocksPassword')}</FormLabel>
2662+
<FormControl>
2663+
<PasswordInput
2664+
dir="ltr"
2665+
autoComplete="new-password"
2666+
className="h-10 w-full"
2667+
value={field.value}
2668+
onChange={e => {
2669+
const v = e.target.value
2670+
field.onChange(v)
2671+
setShadowsocksPasswordJustGenerated(false)
2672+
patchInbound({ password: v } as Partial<Inbound>)
2673+
}}
2674+
/>
2675+
</FormControl>
2676+
<FormMessage />
2677+
</FormItem>
2678+
)}
2679+
/>
2680+
<div className="sm:col-span-2">
2681+
<LoaderButton
2682+
type="button"
2683+
onClick={generateInboundShadowsocksPassword}
2684+
className="h-10 w-full text-sm font-medium transition-all hover:shadow-md sm:h-11"
2685+
isLoading={isGeneratingShadowsocksPassword}
2686+
loadingText={t('coreConfigModal.generatingShadowsocksPassword')}
2687+
>
2688+
<span className="flex items-center gap-2 truncate">
2689+
{shadowsocksPasswordJustGenerated && <span className="h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 ring-2 ring-green-500/20" />}
2690+
{t('coreConfigModal.generateShadowsocksPassword')}
2691+
</span>
2692+
</LoaderButton>
2693+
</div>
2694+
</>
2695+
)}
26812696
</>
26822697
)}
26832698

dashboard/src/features/core-editor/routes/core-editor-page.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export default function CoreEditorPage() {
200200

201201
const [discardOpen, setDiscardOpen] = useState(false)
202202
const [saving, setSaving] = useState(false)
203+
const [nameSubmitAttempted, setNameSubmitAttempted] = useState(false)
203204
const [headerAddPulse, setHeaderAddPulse] = useState<SectionHeaderAddPulse>({ target: '', n: 0 })
204205
const [headerAddEpoch, setHeaderAddEpoch] = useState(0)
205206

@@ -281,9 +282,11 @@ export default function CoreEditorPage() {
281282
const handleSave = useCallback(async () => {
282283
const name = coreName.trim()
283284
if (!name) {
284-
toast.error(t('coreEditor.nameRequired', { defaultValue: 'Name is required' }))
285+
setNameSubmitAttempted(true)
286+
toast.error(t('coreConfigModal.nameRequired', { defaultValue: 'Core name is required' }))
285287
return
286288
}
289+
setNameSubmitAttempted(false)
287290
setSaving(true)
288291
try {
289292
if (kind === 'wg') {
@@ -393,6 +396,10 @@ export default function CoreEditorPage() {
393396
t,
394397
])
395398

399+
const nameRequiredMessage = t('coreConfigModal.nameRequired', { defaultValue: 'Core name is required' })
400+
const showNameRequired = nameSubmitAttempted && coreName.trim() === ''
401+
const canSaveCore = isNew || hasActualChanges
402+
396403
const header = (
397404
<div className="flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
398405
<div className="flex min-w-0 flex-1 gap-2 items-center sm:gap-3">
@@ -411,9 +418,15 @@ export default function CoreEditorPage() {
411418
<div className="min-w-0 flex-1">
412419
<Input
413420
value={coreName}
414-
onChange={e => setCoreName(e.target.value)}
421+
onChange={e => {
422+
const nextName = e.target.value
423+
setCoreName(nextName)
424+
if (nextName.trim()) setNameSubmitAttempted(false)
425+
}}
426+
isError={showNameRequired}
415427
className="h-10 font-medium sm:max-w-md"
416-
placeholder={t('coreConfigModal.namePlaceholder', { defaultValue: 'Core name' })}
428+
placeholder={showNameRequired ? nameRequiredMessage : t('coreConfigModal.namePlaceholder', { defaultValue: 'Core name' })}
429+
aria-invalid={showNameRequired}
417430
/>
418431
</div>
419432
<Select
@@ -580,6 +593,8 @@ export default function CoreEditorPage() {
580593
</div>
581594
}
582595
dirty={hasActualChanges}
596+
canSave={canSaveCore}
597+
saveLabel={isNew ? t('create', { defaultValue: 'Create' }) : undefined}
583598
onSave={handleSave}
584599
onDiscard={() => discardDraft()}
585600
saving={saving || createMutation.isPending || modifyMutation.isPending}

0 commit comments

Comments
 (0)