Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
6b97a8be
Commit
6b97a8be
authored
Jan 09, 2026
by
Edric Li
Browse files
Merge branch 'main' into feat/api-key-ip-restriction
parents
90798f14
62dc0b95
Changes
70
Show whitespace changes
Inline
Side-by-side
frontend/src/types/index.ts
View file @
6b97a8be
...
...
@@ -73,6 +73,7 @@ export interface PublicSettings {
api_base_url
:
string
contact_info
:
string
doc_url
:
string
linuxdo_oauth_enabled
:
boolean
version
:
string
}
...
...
frontend/src/views/admin/AccountsView.vue
View file @
6b97a8be
...
...
@@ -7,7 +7,7 @@
v-model:searchQuery=
"params.search"
:filters=
"params"
@
update:filters=
"(newFilters) => Object.assign(params, newFilters)"
@
change=
"
r
eload"
@
change=
"
debouncedR
eload"
@
update:searchQuery=
"debouncedReload"
/>
<AccountTableActions
...
...
@@ -19,7 +19,7 @@
</div>
</
template
>
<
template
#table
>
<AccountBulkActionsBar
:selected-ids=
"selIds"
@
delete=
"handleBulkDelete"
@
edit=
"showBulkEdit = true"
@
clear=
"selIds = []"
@
select-page=
"selectPage"
/>
<AccountBulkActionsBar
:selected-ids=
"selIds"
@
delete=
"handleBulkDelete"
@
edit=
"showBulkEdit = true"
@
clear=
"selIds = []"
@
select-page=
"selectPage"
@
toggle-schedulable=
"handleBulkToggleSchedulable"
/>
<DataTable
:columns=
"cols"
:data=
"accounts"
:loading=
"loading"
>
<template
#cell-select
="
{ row }">
<input
type=
"checkbox"
:checked=
"selIds.includes(row.id)"
@
change=
"toggleSel(row.id)"
class=
"rounded border-gray-300 text-primary-600 focus:ring-primary-500"
/>
...
...
@@ -107,7 +107,7 @@
</
template
>
</DataTable>
</template>
<
template
#pagination
><Pagination
v-if=
"pagination.total > 0"
:page=
"pagination.page"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
/></
template
>
<
template
#pagination
><Pagination
v-if=
"pagination.total > 0"
:page=
"pagination.page"
:total=
"pagination.total"
:page-size=
"pagination.page_size"
@
update:page=
"handlePageChange"
@
update:pageSize=
"handlePageSizeChange"
/></
template
>
</TablePageLayout>
<CreateAccountModal
:show=
"showCreate"
:proxies=
"proxies"
:groups=
"groups"
@
close=
"showCreate = false"
@
created=
"reload"
/>
<EditAccountModal
:show=
"showEdit"
:account=
"edAcc"
:proxies=
"proxies"
:groups=
"groups"
@
close=
"showEdit = false"
@
updated=
"load"
/>
...
...
@@ -175,7 +175,7 @@ const statsAcc = ref<Account | null>(null)
const
togglingSchedulable
=
ref
<
number
|
null
>
(
null
)
const
menu
=
reactive
<
{
show
:
boolean
,
acc
:
Account
|
null
,
pos
:{
top
:
number
,
left
:
number
}
|
null
}
>
({
show
:
false
,
acc
:
null
,
pos
:
null
})
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
}
=
useTableLoader
<
Account
,
any
>
({
const
{
items
:
accounts
,
loading
,
params
,
pagination
,
load
,
reload
,
debouncedReload
,
handlePageChange
,
handlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
search
:
''
}
})
...
...
@@ -209,6 +209,21 @@ const openMenu = (a: Account, e: MouseEvent) => { menu.acc = a; menu.pos = { top
const
toggleSel
=
(
id
:
number
)
=>
{
const
i
=
selIds
.
value
.
indexOf
(
id
);
if
(
i
===
-
1
)
selIds
.
value
.
push
(
id
);
else
selIds
.
value
.
splice
(
i
,
1
)
}
const
selectPage
=
()
=>
{
selIds
.
value
=
[...
new
Set
([...
selIds
.
value
,
...
accounts
.
value
.
map
(
a
=>
a
.
id
)])]
}
const
handleBulkDelete
=
async
()
=>
{
if
(
!
confirm
(
t
(
'
common.confirm
'
)))
return
;
try
{
await
Promise
.
all
(
selIds
.
value
.
map
(
id
=>
adminAPI
.
accounts
.
delete
(
id
)));
selIds
.
value
=
[];
reload
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to bulk delete accounts:
'
,
error
)
}
}
const
handleBulkToggleSchedulable
=
async
(
schedulable
:
boolean
)
=>
{
const
count
=
selIds
.
value
.
length
try
{
const
result
=
await
adminAPI
.
accounts
.
bulkUpdate
(
selIds
.
value
,
{
schedulable
});
const
message
=
schedulable
?
t
(
'
admin.accounts.bulkSchedulableEnabled
'
,
{
count
:
result
.
success
||
count
})
:
t
(
'
admin.accounts.bulkSchedulableDisabled
'
,
{
count
:
result
.
success
||
count
});
appStore
.
showSuccess
(
message
);
selIds
.
value
=
[];
reload
()
}
catch
(
error
)
{
console
.
error
(
'
Failed to bulk toggle schedulable:
'
,
error
);
appStore
.
showError
(
t
(
'
common.error
'
))
}
}
const
handleBulkUpdated
=
()
=>
{
showBulkEdit
.
value
=
false
;
selIds
.
value
=
[];
reload
()
}
const
closeTestModal
=
()
=>
{
showTest
.
value
=
false
;
testingAcc
.
value
=
null
}
const
closeStatsModal
=
()
=>
{
showStats
.
value
=
false
;
statsAcc
.
value
=
null
}
...
...
frontend/src/views/admin/GroupsView.vue
View file @
6b97a8be
...
...
@@ -16,6 +16,7 @@
type=
"text"
:placeholder=
"t('admin.groups.searchGroups')"
class=
"input pl-10"
@
input=
"handleSearch"
/>
</div>
<Select
...
...
@@ -64,7 +65,7 @@
</
template
>
<
template
#table
>
<DataTable
:columns=
"columns"
:data=
"
displayedG
roups"
:loading=
"loading"
>
<DataTable
:columns=
"columns"
:data=
"
g
roups"
:loading=
"loading"
>
<template
#cell-name
="
{ value }">
<span
class=
"font-medium text-gray-900 dark:text-white"
>
{{
value
}}
</span>
</
template
>
...
...
@@ -932,16 +933,6 @@ const pagination = reactive({
let
abortController
:
AbortController
|
null
=
null
const
displayedGroups
=
computed
(()
=>
{
const
q
=
searchQuery
.
value
.
trim
().
toLowerCase
()
if
(
!
q
)
return
groups
.
value
return
groups
.
value
.
filter
((
group
)
=>
{
const
name
=
group
.
name
?.
toLowerCase
?.()
??
''
const
description
=
group
.
description
?.
toLowerCase
?.()
??
''
return
name
.
includes
(
q
)
||
description
.
includes
(
q
)
}
)
}
)
const
showCreateModal
=
ref
(
false
)
const
showEditModal
=
ref
(
false
)
const
showDeleteDialog
=
ref
(
false
)
...
...
@@ -1011,7 +1002,8 @@ const loadGroups = async () => {
const
response
=
await
adminAPI
.
groups
.
list
(
pagination
.
page
,
pagination
.
page_size
,
{
platform
:
(
filters
.
platform
as
GroupPlatform
)
||
undefined
,
status
:
filters
.
status
as
any
,
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
is_exclusive
:
filters
.
is_exclusive
?
filters
.
is_exclusive
===
'
true
'
:
undefined
,
search
:
searchQuery
.
value
.
trim
()
||
undefined
}
,
{
signal
}
)
if
(
signal
.
aborted
)
return
groups
.
value
=
response
.
items
...
...
@@ -1030,6 +1022,15 @@ const loadGroups = async () => {
}
}
let
searchTimeout
:
ReturnType
<
typeof
setTimeout
>
const
handleSearch
=
()
=>
{
clearTimeout
(
searchTimeout
)
searchTimeout
=
setTimeout
(()
=>
{
pagination
.
page
=
1
loadGroups
()
}
,
300
)
}
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
loadGroups
()
...
...
frontend/src/views/admin/ProxiesView.vue
View file @
6b97a8be
...
...
@@ -519,7 +519,7 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
adminAPI
}
from
'
@/api/admin
'
...
...
@@ -942,4 +942,9 @@ const confirmDelete = async () => {
onMounted
(()
=>
{
loadProxies
()
}
)
onUnmounted
(()
=>
{
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
}
)
<
/script
>
frontend/src/views/admin/RedeemView.vue
View file @
6b97a8be
...
...
@@ -364,7 +364,7 @@
<
/template
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
...
...
@@ -693,4 +693,9 @@ onMounted(() => {
loadCodes
()
loadSubscriptionGroups
()
}
)
onUnmounted
(()
=>
{
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
}
)
<
/script
>
frontend/src/views/admin/SettingsView.vue
View file @
6b97a8be
...
...
@@ -261,6 +261,106 @@
</div>
</div>
<!-- LinuxDo Connect OAuth 登录 -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
<h2
class=
"text-lg font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.settings.linuxdo.title
'
)
}}
</h2>
<p
class=
"mt-1 text-sm text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.description
'
)
}}
</p>
</div>
<div
class=
"space-y-5 p-6"
>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"font-medium text-gray-900 dark:text-white"
>
{{
t
(
'
admin.settings.linuxdo.enable
'
)
}}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.enableHint
'
)
}}
</p>
</div>
<Toggle
v-model=
"form.linuxdo_connect_enabled"
/>
</div>
<div
v-if=
"form.linuxdo_connect_enabled"
class=
"border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div
class=
"grid grid-cols-1 gap-6"
>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.clientId
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_client_id"
type=
"text"
class=
"input font-mono text-sm"
:placeholder=
"t('admin.settings.linuxdo.clientIdPlaceholder')"
/>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.clientIdHint
'
)
}}
</p>
</div>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.clientSecret
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_client_secret"
type=
"password"
class=
"input font-mono text-sm"
:placeholder=
"
form.linuxdo_connect_client_secret_configured
? t('admin.settings.linuxdo.clientSecretConfiguredPlaceholder')
: t('admin.settings.linuxdo.clientSecretPlaceholder')
"
/>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
form
.
linuxdo_connect_client_secret_configured
?
t
(
'
admin.settings.linuxdo.clientSecretConfiguredHint
'
)
:
t
(
'
admin.settings.linuxdo.clientSecretHint
'
)
}}
</p>
</div>
<div>
<label
class=
"mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
{{
t
(
'
admin.settings.linuxdo.redirectUrl
'
)
}}
</label>
<input
v-model=
"form.linuxdo_connect_redirect_url"
type=
"url"
class=
"input font-mono text-sm"
:placeholder=
"t('admin.settings.linuxdo.redirectUrlPlaceholder')"
/>
<div
class=
"mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm w-fit"
@
click=
"setAndCopyLinuxdoRedirectUrl"
>
{{
t
(
'
admin.settings.linuxdo.quickSetCopy
'
)
}}
</button>
<code
v-if=
"linuxdoRedirectUrlSuggestion"
class=
"select-all break-all rounded bg-gray-50 px-2 py-1 font-mono text-xs text-gray-600 dark:bg-dark-800 dark:text-gray-300"
>
{{
linuxdoRedirectUrlSuggestion
}}
</code>
</div>
<p
class=
"mt-1.5 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.settings.linuxdo.redirectUrlHint
'
)
}}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Default Settings -->
<div
class=
"card"
>
<div
class=
"border-b border-gray-100 px-6 py-4 dark:border-dark-700"
>
...
...
@@ -692,17 +792,19 @@
</
template
>
<
script
setup
lang=
"ts"
>
import
{
ref
,
reactive
,
onMounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
adminAPI
}
from
'
@/api
'
import
type
{
SystemSettings
,
UpdateSettingsRequest
}
from
'
@/api/admin/settings
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
{
useAppStore
}
from
'
@/stores
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
const
{
copyToClipboard
}
=
useClipboard
()
const
loading
=
ref
(
true
)
const
saving
=
ref
(
false
)
...
...
@@ -721,6 +823,7 @@ const newAdminApiKey = ref('')
type
SettingsForm
=
SystemSettings
&
{
smtp_password
:
string
turnstile_secret_key
:
string
linuxdo_connect_client_secret
:
string
}
const
form
=
reactive
<
SettingsForm
>
({
...
...
@@ -747,11 +850,32 @@ const form = reactive<SettingsForm>({
turnstile_site_key
:
''
,
turnstile_secret_key
:
''
,
turnstile_secret_key_configured
:
false
,
// LinuxDo Connect OAuth(终端用户登录)
linuxdo_connect_enabled
:
false
,
linuxdo_connect_client_id
:
''
,
linuxdo_connect_client_secret
:
''
,
linuxdo_connect_client_secret_configured
:
false
,
linuxdo_connect_redirect_url
:
''
,
// Identity patch (Claude -> Gemini)
enable_identity_patch
:
true
,
identity_patch_prompt
:
''
})
const
linuxdoRedirectUrlSuggestion
=
computed
(()
=>
{
if
(
typeof
window
===
'
undefined
'
)
return
''
const
origin
=
window
.
location
.
origin
||
`
${
window
.
location
.
protocol
}
//
${
window
.
location
.
host
}
`
return
`
${
origin
}
/api/v1/auth/oauth/linuxdo/callback`
})
async
function
setAndCopyLinuxdoRedirectUrl
()
{
const
url
=
linuxdoRedirectUrlSuggestion
.
value
if
(
!
url
)
return
form
.
linuxdo_connect_redirect_url
=
url
await
copyToClipboard
(
url
,
t
(
'
admin.settings.linuxdo.redirectUrlSetAndCopied
'
))
}
function
handleLogoUpload
(
event
:
Event
)
{
const
input
=
event
.
target
as
HTMLInputElement
const
file
=
input
.
files
?.[
0
]
...
...
@@ -797,6 +921,7 @@ async function loadSettings() {
Object
.
assign
(
form
,
settings
)
form
.
smtp_password
=
''
form
.
turnstile_secret_key
=
''
form
.
linuxdo_connect_client_secret
=
''
}
catch
(
error
:
any
)
{
appStore
.
showError
(
t
(
'
admin.settings.failedToLoad
'
)
+
'
:
'
+
(
error
.
message
||
t
(
'
common.unknownError
'
))
...
...
@@ -829,12 +954,17 @@ async function saveSettings() {
smtp_use_tls
:
form
.
smtp_use_tls
,
turnstile_enabled
:
form
.
turnstile_enabled
,
turnstile_site_key
:
form
.
turnstile_site_key
,
turnstile_secret_key
:
form
.
turnstile_secret_key
||
undefined
turnstile_secret_key
:
form
.
turnstile_secret_key
||
undefined
,
linuxdo_connect_enabled
:
form
.
linuxdo_connect_enabled
,
linuxdo_connect_client_id
:
form
.
linuxdo_connect_client_id
,
linuxdo_connect_client_secret
:
form
.
linuxdo_connect_client_secret
||
undefined
,
linuxdo_connect_redirect_url
:
form
.
linuxdo_connect_redirect_url
}
const
updated
=
await
adminAPI
.
settings
.
updateSettings
(
payload
)
Object
.
assign
(
form
,
updated
)
form
.
smtp_password
=
''
form
.
turnstile_secret_key
=
''
form
.
linuxdo_connect_client_secret
=
''
// Refresh cached public settings so sidebar/header update immediately
await
appStore
.
fetchPublicSettings
(
true
)
appStore
.
showSuccess
(
t
(
'
admin.settings.settingsSaved
'
))
...
...
frontend/src/views/admin/UsersView.vue
View file @
6b97a8be
...
...
@@ -893,12 +893,13 @@ const loadUsers = async () => {
}
}
}
}
catch
(
error
)
{
}
catch
(
error
:
any
)
{
const
errorInfo
=
error
as
{
name
?:
string
;
code
?:
string
}
if
(
errorInfo
?.
name
===
'
AbortError
'
||
errorInfo
?.
name
===
'
CanceledError
'
||
errorInfo
?.
code
===
'
ERR_CANCELED
'
)
{
return
}
appStore
.
showError
(
t
(
'
admin.users.failedToLoad
'
))
const
message
=
error
.
response
?.
data
?.
detail
||
error
.
message
||
t
(
'
admin.users.failedToLoad
'
)
appStore
.
showError
(
message
)
console
.
error
(
'
Error loading users:
'
,
error
)
}
finally
{
if
(
abortController
===
currentAbortController
)
{
...
...
@@ -917,7 +918,9 @@ const handleSearch = () => {
}
const
handlePageChange
=
(
page
:
number
)
=>
{
pagination
.
page
=
page
// 确保页码在有效范围内
const
validPage
=
Math
.
max
(
1
,
Math
.
min
(
page
,
pagination
.
pages
||
1
))
pagination
.
page
=
validPage
loadUsers
()
}
...
...
@@ -943,6 +946,7 @@ const toggleBuiltInFilter = (key: string) => {
visibleFilters
.
add
(
key
)
}
saveFiltersToStorage
()
pagination
.
page
=
1
loadUsers
()
}
...
...
@@ -957,6 +961,7 @@ const toggleAttributeFilter = (attr: UserAttributeDefinition) => {
activeAttributeFilters
[
attr
.
id
]
=
''
}
saveFiltersToStorage
()
pagination
.
page
=
1
loadUsers
()
}
...
...
@@ -1059,5 +1064,7 @@ onMounted(async () => {
onUnmounted
(()
=>
{
document
.
removeEventListener
(
'
click
'
,
handleClickOutside
)
clearTimeout
(
searchTimeout
)
abortController
?.
abort
()
})
</
script
>
frontend/src/views/auth/LinuxDoCallbackView.vue
0 → 100644
View file @
6b97a8be
<
template
>
<AuthLayout>
<div
class=
"space-y-6"
>
<div
class=
"text-center"
>
<h2
class=
"text-2xl font-bold text-gray-900 dark:text-white"
>
{{
t
(
'
auth.linuxdo.callbackTitle
'
)
}}
</h2>
<p
class=
"mt-2 text-sm text-gray-500 dark:text-dark-400"
>
{{
isProcessing
?
t
(
'
auth.linuxdo.callbackProcessing
'
)
:
t
(
'
auth.linuxdo.callbackHint
'
)
}}
</p>
</div>
<transition
name=
"fade"
>
<div
v-if=
"errorMessage"
class=
"rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-shrink-0"
>
<Icon
name=
"exclamationCircle"
size=
"md"
class=
"text-red-500"
/>
</div>
<div
class=
"space-y-2"
>
<p
class=
"text-sm text-red-700 dark:text-red-400"
>
{{
errorMessage
}}
</p>
<router-link
to=
"/login"
class=
"btn btn-primary"
>
{{
t
(
'
auth.linuxdo.backToLogin
'
)
}}
</router-link>
</div>
</div>
</div>
</transition>
</div>
</AuthLayout>
</
template
>
<
script
setup
lang=
"ts"
>
import
{
onMounted
,
ref
}
from
'
vue
'
import
{
useRoute
,
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
const
route
=
useRoute
()
const
router
=
useRouter
()
const
{
t
}
=
useI18n
()
const
authStore
=
useAuthStore
()
const
appStore
=
useAppStore
()
const
isProcessing
=
ref
(
true
)
const
errorMessage
=
ref
(
''
)
function
parseFragmentParams
():
URLSearchParams
{
const
raw
=
typeof
window
!==
'
undefined
'
?
window
.
location
.
hash
:
''
const
hash
=
raw
.
startsWith
(
'
#
'
)
?
raw
.
slice
(
1
)
:
raw
return
new
URLSearchParams
(
hash
)
}
function
sanitizeRedirectPath
(
path
:
string
|
null
|
undefined
):
string
{
if
(
!
path
)
return
'
/dashboard
'
if
(
!
path
.
startsWith
(
'
/
'
))
return
'
/dashboard
'
if
(
path
.
startsWith
(
'
//
'
))
return
'
/dashboard
'
if
(
path
.
includes
(
'
://
'
))
return
'
/dashboard
'
if
(
path
.
includes
(
'
\n
'
)
||
path
.
includes
(
'
\r
'
))
return
'
/dashboard
'
return
path
}
onMounted
(
async
()
=>
{
const
params
=
parseFragmentParams
()
const
token
=
params
.
get
(
'
access_token
'
)
||
''
const
redirect
=
sanitizeRedirectPath
(
params
.
get
(
'
redirect
'
)
||
(
route
.
query
.
redirect
as
string
|
undefined
)
||
'
/dashboard
'
)
const
error
=
params
.
get
(
'
error
'
)
const
errorDesc
=
params
.
get
(
'
error_description
'
)
||
params
.
get
(
'
error_message
'
)
||
''
if
(
error
)
{
errorMessage
.
value
=
errorDesc
||
error
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
if
(
!
token
)
{
errorMessage
.
value
=
t
(
'
auth.linuxdo.callbackMissingToken
'
)
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
return
}
try
{
await
authStore
.
setToken
(
token
)
appStore
.
showSuccess
(
t
(
'
auth.loginSuccess
'
))
await
router
.
replace
(
redirect
)
}
catch
(
e
:
unknown
)
{
const
err
=
e
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
}
}
}
errorMessage
.
value
=
err
.
response
?.
data
?.
detail
||
err
.
message
||
t
(
'
auth.loginFailed
'
)
appStore
.
showError
(
errorMessage
.
value
)
isProcessing
.
value
=
false
}
})
</
script
>
<
style
scoped
>
.fade-enter-active
,
.fade-leave-active
{
transition
:
all
0.3s
ease
;
}
.fade-enter-from
,
.fade-leave-to
{
opacity
:
0
;
transform
:
translateY
(
-8px
);
}
</
style
>
frontend/src/views/auth/LoginView.vue
View file @
6b97a8be
...
...
@@ -11,6 +11,9 @@
</p>
</div>
<!-- LinuxDo Connect OAuth 登录 -->
<LinuxDoOAuthSection
v-if=
"linuxdoOAuthEnabled"
:disabled=
"isLoading"
/>
<!-- Login Form -->
<form
@
submit.prevent=
"handleLogin"
class=
"space-y-5"
>
<!-- Email Input -->
...
...
@@ -157,6 +160,7 @@ import { ref, reactive, onMounted } from 'vue'
import
{
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
LinuxDoOAuthSection
from
'
@/components/auth/LinuxDoOAuthSection.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
...
...
@@ -179,6 +183,7 @@ const showPassword = ref<boolean>(false)
// Public settings
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
linuxdoOAuthEnabled
=
ref
<
boolean
>
(
false
)
// Turnstile
const
turnstileRef
=
ref
<
InstanceType
<
typeof
TurnstileWidget
>
|
null
>
(
null
)
...
...
@@ -210,6 +215,7 @@ onMounted(async () => {
const
settings
=
await
getPublicSettings
()
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
linuxdoOAuthEnabled
.
value
=
settings
.
linuxdo_oauth_enabled
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
)
}
...
...
frontend/src/views/auth/RegisterView.vue
View file @
6b97a8be
...
...
@@ -11,6 +11,9 @@
<
/p
>
<
/div
>
<!--
LinuxDo
Connect
OAuth
登录
-->
<
LinuxDoOAuthSection
v
-
if
=
"
linuxdoOAuthEnabled
"
:
disabled
=
"
isLoading
"
/>
<!--
Registration
Disabled
Message
-->
<
div
v
-
if
=
"
!registrationEnabled && settingsLoaded
"
...
...
@@ -181,6 +184,7 @@ import { ref, reactive, onMounted } from 'vue'
import
{
useRouter
}
from
'
vue-router
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
AuthLayout
}
from
'
@/components/layout
'
import
LinuxDoOAuthSection
from
'
@/components/auth/LinuxDoOAuthSection.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
...
...
@@ -207,6 +211,7 @@ const emailVerifyEnabled = ref<boolean>(false)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
const
linuxdoOAuthEnabled
=
ref
<
boolean
>
(
false
)
// Turnstile
const
turnstileRef
=
ref
<
InstanceType
<
typeof
TurnstileWidget
>
|
null
>
(
null
)
...
...
@@ -233,6 +238,7 @@ onMounted(async () => {
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
linuxdoOAuthEnabled
.
value
=
settings
.
linuxdo_oauth_enabled
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
)
}
finally
{
...
...
Prev
1
2
3
4
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment