feat: add authentication token support

This commit is contained in:
Memory 2025-12-01 07:56:07 +08:00 committed by GitHub
parent 47fd7add5f
commit f541b5ead1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 160 additions and 100 deletions

View File

@ -142,6 +142,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
useProxy: item.useProxy || false, useProxy: item.useProxy || false,
allowFixedInterval: item.allowFixedInterval || false, allowFixedInterval: item.allowFixedInterval || false,
autoUpdate: item.autoUpdate ?? false, autoUpdate: item.autoUpdate ?? false,
authToken: item.authToken,
updated: new Date().getTime() updated: new Date().getTime()
} as IProfileItem } as IProfileItem
switch (newItem.type) { switch (newItem.type) {
@ -159,14 +160,24 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
} else { } else {
urlObj.searchParams.delete('proxy') urlObj.searchParams.delete('proxy')
} }
const headers: Record<string, string> = {
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
}
if (item.authToken) {
headers['Authorization'] = item.authToken
}
res = await chromeRequest.get(urlObj.toString(), { res = await chromeRequest.get(urlObj.toString(), {
headers: { headers,
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
},
responseType: 'text', responseType: 'text',
timeout: subscriptionTimeout timeout: subscriptionTimeout
}) })
} else { } else {
const headers: Record<string, string> = {
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
}
if (item.authToken) {
headers['Authorization'] = item.authToken
}
res = await chromeRequest.get(item.url, { res = await chromeRequest.get(item.url, {
proxy: newItem.useProxy proxy: newItem.useProxy
? { ? {
@ -175,9 +186,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
port: mixedPort port: mixedPort
} }
: false, : false,
headers: { headers,
'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
},
responseType: 'text', responseType: 'text',
timeout: subscriptionTimeout timeout: subscriptionTimeout
}) })

View File

@ -91,6 +91,18 @@ const EditInfoModal: React.FC<Props> = (props) => {
}} }}
/> />
</SettingItem> </SettingItem>
<SettingItem title={t('profiles.editInfo.authToken')}>
<Input
size="sm"
type="password"
className={cn(inputWidth)}
value={values.authToken || ''}
onValueChange={(v) => {
setValues({ ...values, authToken: v })
}}
placeholder={t('profiles.editInfo.authTokenPlaceholder')}
/>
</SettingItem>
<SettingItem title={t('profiles.editInfo.useProxy')}> <SettingItem title={t('profiles.editInfo.useProxy')}>
<Switch <Switch
size="sm" size="sm"

View File

@ -438,6 +438,8 @@
"profiles.editInfo.title": "Edit Information", "profiles.editInfo.title": "Edit Information",
"profiles.editInfo.name": "Name", "profiles.editInfo.name": "Name",
"profiles.editInfo.url": "Subscription URL", "profiles.editInfo.url": "Subscription URL",
"profiles.editInfo.authToken": "Authorization Token",
"profiles.editInfo.authTokenPlaceholder": "Bearer token or other auth header value",
"profiles.editInfo.useProxy": "Use Proxy to Update", "profiles.editInfo.useProxy": "Use Proxy to Update",
"profiles.editInfo.interval": "Upd. Interval", "profiles.editInfo.interval": "Upd. Interval",
"profiles.editInfo.intervalPlaceholder": "e.g.: 30 or '0 * * * *'", "profiles.editInfo.intervalPlaceholder": "e.g.: 30 or '0 * * * *'",

View File

@ -443,6 +443,8 @@
"profiles.editInfo.title": "编辑信息", "profiles.editInfo.title": "编辑信息",
"profiles.editInfo.name": "名称", "profiles.editInfo.name": "名称",
"profiles.editInfo.url": "订阅地址", "profiles.editInfo.url": "订阅地址",
"profiles.editInfo.authToken": "授权令牌",
"profiles.editInfo.authTokenPlaceholder": "Bearer token 或其他认证头值",
"profiles.editInfo.useProxy": "使用代理更新", "profiles.editInfo.useProxy": "使用代理更新",
"profiles.editInfo.interval": "更新间隔", "profiles.editInfo.interval": "更新间隔",
"profiles.editInfo.intervalPlaceholder": "例如30 或 '0 * * * *'", "profiles.editInfo.intervalPlaceholder": "例如30 或 '0 * * * *'",

View File

@ -7,7 +7,8 @@ import {
DropdownItem, DropdownItem,
DropdownMenu, DropdownMenu,
DropdownTrigger, DropdownTrigger,
Input Input,
Tooltip
} from '@heroui/react' } from '@heroui/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import ProfileItem from '@renderer/components/profiles/profile-item' import ProfileItem from '@renderer/components/profiles/profile-item'
@ -50,6 +51,8 @@ const Profiles: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [sortedItems, setSortedItems] = useState(items) const [sortedItems, setSortedItems] = useState(items)
const [useProxy, setUseProxy] = useState(false) const [useProxy, setUseProxy] = useState(false)
const [authToken, setAuthToken] = useState('')
const [showAdvanced, setShowAdvanced] = useState(false)
const [subStoreImporting, setSubStoreImporting] = useState(false) const [subStoreImporting, setSubStoreImporting] = useState(false)
const [importing, setImporting] = useState(false) const [importing, setImporting] = useState(false)
const [updating, setUpdating] = useState(false) const [updating, setUpdating] = useState(false)
@ -125,8 +128,15 @@ const Profiles: React.FC = () => {
}, [subs, collections]) }, [subs, collections])
const handleImport = async (): Promise<void> => { const handleImport = async (): Promise<void> => {
setImporting(true) setImporting(true)
await addProfileItem({ name: '', type: 'remote', url, useProxy }) await addProfileItem({
name: '',
type: 'remote',
url,
useProxy,
authToken: authToken || undefined
})
setUrl('') setUrl('')
setAuthToken('')
setImporting(false) setImporting(false)
} }
const pageRef = useRef<HTMLDivElement>(null) const pageRef = useRef<HTMLDivElement>(null)
@ -225,68 +235,79 @@ const Profiles: React.FC = () => {
} }
> >
<div className="sticky profiles-sticky top-0 z-40 bg-background"> <div className="sticky profiles-sticky top-0 z-40 bg-background">
<div className="flex p-2"> <div className="flex flex-col gap-2 p-2">
<Input <div className="flex gap-2">
size="sm" <Input
placeholder={t('profiles.input.placeholder')} size="sm"
value={url} placeholder={t('profiles.input.placeholder')}
onValueChange={setUrl} value={url}
onKeyUp={handleInputKeyUp} onValueChange={setUrl}
endContent={ onKeyUp={handleInputKeyUp}
<> className="flex-1"
<Button endContent={
size="md" <>
isIconOnly <Button
variant="light" size="md"
onPress={() => { isIconOnly
navigator.clipboard.readText().then((text) => { variant="light"
setUrl(text) onPress={() => {
}) navigator.clipboard.readText().then((text) => {
}} setUrl(text)
className="mr-2" })
> }}
<MdContentPaste className="text-lg" /> className="mr-2"
</Button> >
<Checkbox <MdContentPaste className="text-lg" />
className="whitespace-nowrap" </Button>
checked={useProxy} <Checkbox
onValueChange={setUseProxy} className="whitespace-nowrap"
> checked={useProxy}
{t('profiles.useProxy')} onValueChange={setUseProxy}
</Checkbox> >
</> {t('profiles.useProxy')}
} </Checkbox>
/> </>
}
/>
<Button <Tooltip content={t('profiles.editInfo.authToken')} placement="bottom">
size="sm" <Button
color="primary" size="sm"
className="ml-2" variant={showAdvanced ? 'solid' : 'light'}
isDisabled={isUrlEmpty} isIconOnly
isLoading={importing} onPress={() => setShowAdvanced(!showAdvanced)}
onPress={handleImport} >
> 🔑
{t('profiles.import')} </Button>
</Button> </Tooltip>
{useSubStore && ( <Button
<Dropdown size="sm"
onOpenChange={() => { color="primary"
mutateSubs() isDisabled={isUrlEmpty}
mutateCollections() isLoading={importing}
}} onPress={handleImport}
> >
<DropdownTrigger> {t('profiles.import')}
<Button </Button>
isLoading={subStoreImporting} {useSubStore && (
title="Sub-Store" <Dropdown
className="ml-2 substore-import" onOpenChange={() => {
size="sm" mutateSubs()
isIconOnly mutateCollections()
color="primary" }}
> >
<SubStoreIcon className="text-lg" /> <DropdownTrigger>
</Button> <Button
</DropdownTrigger> isLoading={subStoreImporting}
title="Sub-Store"
className="substore-import"
size="sm"
isIconOnly
color="primary"
>
<SubStoreIcon className="text-lg" />
</Button>
</DropdownTrigger>
<DropdownMenu <DropdownMenu
className="max-h-[calc(100vh-200px)] overflow-y-auto" className="max-h-[calc(100vh-200px)] overflow-y-auto"
onAction={async (key) => { onAction={async (key) => {
@ -342,40 +363,53 @@ const Profiles: React.FC = () => {
</DropdownItem> </DropdownItem>
))} ))}
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
)} )}
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button className="ml-2 new-profile" size="sm" isIconOnly color="primary"> <Button className="new-profile" size="sm" isIconOnly color="primary">
<FaPlus /> <FaPlus />
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
onAction={async (key) => { onAction={async (key) => {
if (key === 'open') { if (key === 'open') {
try { try {
const files = await getFilePath(['yml', 'yaml']) const files = await getFilePath(['yml', 'yaml'])
if (files?.length) { if (files?.length) {
const content = await readTextFile(files[0]) const content = await readTextFile(files[0])
const fileName = files[0].split('/').pop()?.split('\\').pop() const fileName = files[0].split('/').pop()?.split('\\').pop()
await addProfileItem({ name: fileName, type: 'local', file: content }) await addProfileItem({ name: fileName, type: 'local', file: content })
}
} catch (e) {
alert(e)
} }
} catch (e) { } else if (key === 'new') {
alert(e) await addProfileItem({
name: t('profiles.newProfile'),
type: 'local',
file: 'proxies: []\nproxy-groups: []\nrules: []'
})
} }
} else if (key === 'new') { }}
await addProfileItem({ >
name: t('profiles.newProfile'), <DropdownItem key="open">{t('profiles.open')}</DropdownItem>
type: 'local', <DropdownItem key="new">{t('profiles.new')}</DropdownItem>
file: 'proxies: []\nproxy-groups: []\nrules: []' </DropdownMenu>
}) </Dropdown>
} </div>
}} {showAdvanced && (
> <Input
<DropdownItem key="open">{t('profiles.open')}</DropdownItem> size="sm"
<DropdownItem key="new">{t('profiles.new')}</DropdownItem> type="password"
</DropdownMenu> placeholder={t('profiles.editInfo.authTokenPlaceholder')}
</Dropdown> value={authToken}
onValueChange={setAuthToken}
onKeyUp={handleInputKeyUp}
className="w-full"
startContent={<span className="text-default-400 text-sm">🔑</span>}
/>
)}
</div> </div>
<Divider /> <Divider />
</div> </div>

View File

@ -492,6 +492,7 @@ interface IProfileItem {
substore?: boolean substore?: boolean
allowFixedInterval?: boolean allowFixedInterval?: boolean
autoUpdate?: boolean autoUpdate?: boolean
authToken?: string
} }
interface ISubStoreSub { interface ISubStoreSub {