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
642842c2
Commit
642842c2
authored
Dec 18, 2025
by
shaw
Browse files
First commit
parent
569f4882
Changes
201
Hide whitespace changes
Inline
Side-by-side
Too many changes to show.
To preserve performance only
201 of 201+
files are displayed.
Plain diff
Email patch
frontend/src/views/auth/EmailVerifyView.vue
0 → 100644
View file @
642842c2
<
template
>
<AuthLayout>
<div
class=
"space-y-6"
>
<!-- Title -->
<div
class=
"text-center"
>
<h2
class=
"text-2xl font-bold text-gray-900 dark:text-white"
>
Verify Your Email
</h2>
<p
class=
"mt-2 text-sm text-gray-500 dark:text-dark-400"
>
We'll send a verification code to
<span
class=
"font-medium text-gray-700 dark:text-gray-300"
>
{{
email
}}
</span>
</p>
</div>
<!-- No Data Warning -->
<div
v-if=
"!hasRegisterData"
class=
"p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800/50"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-shrink-0"
>
<svg
class=
"w-5 h-5 text-amber-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
</div>
<div
class=
"text-sm text-amber-700 dark:text-amber-400"
>
<p
class=
"font-medium"
>
Session expired
</p>
<p
class=
"mt-1"
>
Please go back to the registration page and start again.
</p>
</div>
</div>
</div>
<!-- Verification Form -->
<form
v-else
@
submit.prevent=
"handleVerify"
class=
"space-y-5"
>
<!-- Verification Code Input -->
<div>
<label
for=
"code"
class=
"input-label text-center"
>
Verification Code
</label>
<input
id=
"code"
v-model=
"verifyCode"
type=
"text"
required
autocomplete=
"one-time-code"
inputmode=
"numeric"
maxlength=
"6"
:disabled=
"isLoading"
class=
"input text-center text-xl tracking-[0.5em] font-mono py-3"
:class=
"
{ 'input-error': errors.code }"
placeholder="000000"
/>
<p
v-if=
"errors.code"
class=
"input-error-text text-center"
>
{{
errors
.
code
}}
</p>
<p
v-else
class=
"input-hint text-center"
>
Enter the 6-digit code sent to your email
</p>
</div>
<!-- Code Status -->
<div
v-if=
"codeSent"
class=
"p-4 rounded-xl bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-shrink-0"
>
<svg
class=
"w-5 h-5 text-green-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<p
class=
"text-sm text-green-700 dark:text-green-400"
>
Verification code sent! Please check your inbox.
</p>
</div>
</div>
<!-- Turnstile Widget for Resend -->
<div
v-if=
"turnstileEnabled && turnstileSiteKey && showResendTurnstile"
>
<TurnstileWidget
ref=
"turnstileRef"
:site-key=
"turnstileSiteKey"
@
verify=
"onTurnstileVerify"
@
expire=
"onTurnstileExpire"
@
error=
"onTurnstileError"
/>
<p
v-if=
"errors.turnstile"
class=
"input-error-text text-center mt-2"
>
{{
errors
.
turnstile
}}
</p>
</div>
<!-- Error Message -->
<transition
name=
"fade"
>
<div
v-if=
"errorMessage"
class=
"p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50"
>
<div
class=
"flex items-start gap-3"
>
<div
class=
"flex-shrink-0"
>
<svg
class=
"w-5 h-5 text-red-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
</div>
<p
class=
"text-sm text-red-700 dark:text-red-400"
>
{{
errorMessage
}}
</p>
</div>
</div>
</transition>
<!-- Submit Button -->
<button
type=
"submit"
:disabled=
"isLoading || !verifyCode"
class=
"btn btn-primary w-full"
>
<svg
v-if=
"isLoading"
class=
"animate-spin -ml-1 mr-2 h-4 w-4 text-white"
fill=
"none"
viewBox=
"0 0 24 24"
>
<circle
class=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
stroke-width=
"4"
></circle>
<path
class=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<svg
v-else
class=
"w-5 h-5 mr-2"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{{
isLoading
?
'
Verifying...
'
:
'
Verify & Create Account
'
}}
</button>
<!-- Resend Code -->
<div
class=
"text-center"
>
<button
v-if=
"countdown > 0"
type=
"button"
disabled
class=
"text-sm text-gray-400 dark:text-dark-500 cursor-not-allowed"
>
Resend code in
{{
countdown
}}
s
</button>
<button
v-else
type=
"button"
@
click=
"handleResendCode"
:disabled=
"isSendingCode || (turnstileEnabled && showResendTurnstile && !resendTurnstileToken)"
class=
"text-sm text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<span
v-if=
"isSendingCode"
>
Sending...
</span>
<span
v-else-if=
"turnstileEnabled && !showResendTurnstile"
>
Click to resend code
</span>
<span
v-else
>
Resend verification code
</span>
</button>
</div>
</form>
</div>
<!-- Footer -->
<template
#footer
>
<button
@
click=
"handleBack"
class=
"text-gray-500 dark:text-dark-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors flex items-center gap-2"
>
<svg
class=
"w-4 h-4"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
stroke-width=
"1.5"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
d=
"M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
Back to registration
</button>
</
template
>
</AuthLayout>
</template>
<
script
setup
lang=
"ts"
>
import
{
ref
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
;
import
{
useRouter
}
from
'
vue-router
'
;
import
{
AuthLayout
}
from
'
@/components/layout
'
;
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
;
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
;
import
{
getPublicSettings
,
sendVerifyCode
}
from
'
@/api/auth
'
;
// ==================== Router & Stores ====================
const
router
=
useRouter
();
const
authStore
=
useAuthStore
();
const
appStore
=
useAppStore
();
// ==================== State ====================
const
isLoading
=
ref
<
boolean
>
(
false
);
const
isSendingCode
=
ref
<
boolean
>
(
false
);
const
errorMessage
=
ref
<
string
>
(
''
);
const
codeSent
=
ref
<
boolean
>
(
false
);
const
verifyCode
=
ref
<
string
>
(
''
);
const
countdown
=
ref
<
number
>
(
0
);
let
countdownTimer
:
ReturnType
<
typeof
setInterval
>
|
null
=
null
;
// Registration data from sessionStorage
const
email
=
ref
<
string
>
(
''
);
const
password
=
ref
<
string
>
(
''
);
const
initialTurnstileToken
=
ref
<
string
>
(
''
);
const
hasRegisterData
=
ref
<
boolean
>
(
false
);
// Public settings
const
turnstileEnabled
=
ref
<
boolean
>
(
false
);
const
turnstileSiteKey
=
ref
<
string
>
(
''
);
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
);
// Turnstile for resend
const
turnstileRef
=
ref
<
InstanceType
<
typeof
TurnstileWidget
>
|
null
>
(
null
);
const
resendTurnstileToken
=
ref
<
string
>
(
''
);
const
showResendTurnstile
=
ref
<
boolean
>
(
false
);
const
errors
=
ref
({
code
:
''
,
turnstile
:
''
,
});
// ==================== Lifecycle ====================
onMounted
(
async
()
=>
{
// Load registration data from sessionStorage
const
registerDataStr
=
sessionStorage
.
getItem
(
'
register_data
'
);
if
(
registerDataStr
)
{
try
{
const
registerData
=
JSON
.
parse
(
registerDataStr
);
email
.
value
=
registerData
.
email
||
''
;
password
.
value
=
registerData
.
password
||
''
;
initialTurnstileToken
.
value
=
registerData
.
turnstile_token
||
''
;
hasRegisterData
.
value
=
!!
(
email
.
value
&&
password
.
value
);
}
catch
{
hasRegisterData
.
value
=
false
;
}
}
// Load public settings
try
{
const
settings
=
await
getPublicSettings
();
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
;
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
;
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
;
}
catch
(
error
)
{
console
.
error
(
'
Failed to load public settings:
'
,
error
);
}
// Auto-send verification code if we have valid data
if
(
hasRegisterData
.
value
)
{
await
sendCode
();
}
});
onUnmounted
(()
=>
{
if
(
countdownTimer
)
{
clearInterval
(
countdownTimer
);
countdownTimer
=
null
;
}
});
// ==================== Countdown ====================
function
startCountdown
(
seconds
:
number
):
void
{
countdown
.
value
=
seconds
;
if
(
countdownTimer
)
{
clearInterval
(
countdownTimer
);
}
countdownTimer
=
setInterval
(()
=>
{
if
(
countdown
.
value
>
0
)
{
countdown
.
value
--
;
}
else
{
if
(
countdownTimer
)
{
clearInterval
(
countdownTimer
);
countdownTimer
=
null
;
}
}
},
1000
);
}
// ==================== Turnstile Handlers ====================
function
onTurnstileVerify
(
token
:
string
):
void
{
resendTurnstileToken
.
value
=
token
;
errors
.
value
.
turnstile
=
''
;
}
function
onTurnstileExpire
():
void
{
resendTurnstileToken
.
value
=
''
;
errors
.
value
.
turnstile
=
'
Verification expired, please try again
'
;
}
function
onTurnstileError
():
void
{
resendTurnstileToken
.
value
=
''
;
errors
.
value
.
turnstile
=
'
Verification failed, please try again
'
;
}
// ==================== Send Code ====================
async
function
sendCode
():
Promise
<
void
>
{
isSendingCode
.
value
=
true
;
errorMessage
.
value
=
''
;
try
{
const
response
=
await
sendVerifyCode
({
email
:
email
.
value
,
// 优先使用重发时新获取的 token(因为初始 token 可能已被使用)
turnstile_token
:
resendTurnstileToken
.
value
||
initialTurnstileToken
.
value
||
undefined
,
});
codeSent
.
value
=
true
;
startCountdown
(
response
.
countdown
);
// Reset turnstile state(token 已使用,清除以避免重复使用)
initialTurnstileToken
.
value
=
''
;
showResendTurnstile
.
value
=
false
;
resendTurnstileToken
.
value
=
''
;
}
catch
(
error
:
unknown
)
{
const
err
=
error
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
}
}
};
if
(
err
.
response
?.
data
?.
detail
)
{
errorMessage
.
value
=
err
.
response
.
data
.
detail
;
}
else
if
(
err
.
message
)
{
errorMessage
.
value
=
err
.
message
;
}
else
{
errorMessage
.
value
=
'
Failed to send verification code. Please try again.
'
;
}
appStore
.
showError
(
errorMessage
.
value
);
}
finally
{
isSendingCode
.
value
=
false
;
}
}
// ==================== Handlers ====================
async
function
handleResendCode
():
Promise
<
void
>
{
// If turnstile is enabled and we haven't shown it yet, show it
if
(
turnstileEnabled
.
value
&&
!
showResendTurnstile
.
value
)
{
showResendTurnstile
.
value
=
true
;
return
;
}
// If turnstile is enabled but no token yet, wait
if
(
turnstileEnabled
.
value
&&
!
resendTurnstileToken
.
value
)
{
errors
.
value
.
turnstile
=
'
Please complete the verification
'
;
return
;
}
await
sendCode
();
}
function
validateForm
():
boolean
{
errors
.
value
.
code
=
''
;
if
(
!
verifyCode
.
value
.
trim
())
{
errors
.
value
.
code
=
'
Verification code is required
'
;
return
false
;
}
if
(
!
/^
\d{6}
$/
.
test
(
verifyCode
.
value
.
trim
()))
{
errors
.
value
.
code
=
'
Please enter a valid 6-digit code
'
;
return
false
;
}
return
true
;
}
async
function
handleVerify
():
Promise
<
void
>
{
errorMessage
.
value
=
''
;
if
(
!
validateForm
())
{
return
;
}
isLoading
.
value
=
true
;
try
{
// Register with verification code
await
authStore
.
register
({
email
:
email
.
value
,
password
:
password
.
value
,
verify_code
:
verifyCode
.
value
.
trim
(),
turnstile_token
:
initialTurnstileToken
.
value
||
undefined
,
});
// Clear session data
sessionStorage
.
removeItem
(
'
register_data
'
);
// Show success toast
appStore
.
showSuccess
(
'
Account created successfully! Welcome to
'
+
siteName
.
value
+
'
.
'
);
// Redirect to dashboard
await
router
.
push
(
'
/dashboard
'
);
}
catch
(
error
:
unknown
)
{
const
err
=
error
as
{
message
?:
string
;
response
?:
{
data
?:
{
detail
?:
string
}
}
};
if
(
err
.
response
?.
data
?.
detail
)
{
errorMessage
.
value
=
err
.
response
.
data
.
detail
;
}
else
if
(
err
.
message
)
{
errorMessage
.
value
=
err
.
message
;
}
else
{
errorMessage
.
value
=
'
Verification failed. Please try again.
'
;
}
appStore
.
showError
(
errorMessage
.
value
);
}
finally
{
isLoading
.
value
=
false
;
}
}
function
handleBack
():
void
{
// Clear session data
sessionStorage
.
removeItem
(
'
register_data
'
);
// Go back to registration
router
.
push
(
'
/register
'
);
}
</
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
>
Prev
1
…
7
8
9
10
11
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