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
7229b41f
Unverified
Commit
7229b41f
authored
Feb 03, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 03, 2026
Browse files
Merge pull request #420 from shuike/feat-invitation-code
feat: 增加邀请码注册功能
parents
a09478f3
0ed4a404
Changes
26
Show whitespace changes
Inline
Side-by-side
frontend/src/stores/app.ts
View file @
7229b41f
...
@@ -314,6 +314,7 @@ export const useAppStore = defineStore('app', () => {
...
@@ -314,6 +314,7 @@ export const useAppStore = defineStore('app', () => {
email_verify_enabled
:
false
,
email_verify_enabled
:
false
,
promo_code_enabled
:
true
,
promo_code_enabled
:
true
,
password_reset_enabled
:
false
,
password_reset_enabled
:
false
,
invitation_code_enabled
:
false
,
turnstile_enabled
:
false
,
turnstile_enabled
:
false
,
turnstile_site_key
:
''
,
turnstile_site_key
:
''
,
site_name
:
siteName
.
value
,
site_name
:
siteName
.
value
,
...
...
frontend/src/types/index.ts
View file @
7229b41f
...
@@ -55,6 +55,7 @@ export interface RegisterRequest {
...
@@ -55,6 +55,7 @@ export interface RegisterRequest {
verify_code
?:
string
verify_code
?:
string
turnstile_token
?:
string
turnstile_token
?:
string
promo_code
?:
string
promo_code
?:
string
invitation_code
?:
string
}
}
export
interface
SendVerifyCodeRequest
{
export
interface
SendVerifyCodeRequest
{
...
@@ -72,6 +73,7 @@ export interface PublicSettings {
...
@@ -72,6 +73,7 @@ export interface PublicSettings {
email_verify_enabled
:
boolean
email_verify_enabled
:
boolean
promo_code_enabled
:
boolean
promo_code_enabled
:
boolean
password_reset_enabled
:
boolean
password_reset_enabled
:
boolean
invitation_code_enabled
:
boolean
turnstile_enabled
:
boolean
turnstile_enabled
:
boolean
turnstile_site_key
:
string
turnstile_site_key
:
string
site_name
:
string
site_name
:
string
...
@@ -701,7 +703,7 @@ export interface UpdateProxyRequest {
...
@@ -701,7 +703,7 @@ export interface UpdateProxyRequest {
// ==================== Usage & Redeem Types ====================
// ==================== Usage & Redeem Types ====================
export
type
RedeemCodeType
=
'
balance
'
|
'
concurrency
'
|
'
subscription
'
export
type
RedeemCodeType
=
'
balance
'
|
'
concurrency
'
|
'
subscription
'
|
'
invitation
'
export
interface
UsageLog
{
export
interface
UsageLog
{
id
:
number
id
:
number
...
...
frontend/src/views/admin/RedeemView.vue
View file @
7229b41f
...
@@ -213,7 +213,7 @@
...
@@ -213,7 +213,7 @@
<
Select
v
-
model
=
"
generateForm.type
"
:
options
=
"
typeOptions
"
/>
<
Select
v
-
model
=
"
generateForm.type
"
:
options
=
"
typeOptions
"
/>
<
/div
>
<
/div
>
<!--
余额
/
并发类型
:
显示数值输入
-->
<!--
余额
/
并发类型
:
显示数值输入
-->
<
div
v
-
if
=
"
generateForm.type !== 'subscription'
"
>
<
div
v
-
if
=
"
generateForm.type !== 'subscription'
&& generateForm.type !== 'invitation'
"
>
<
label
class
=
"
input-label
"
>
<
label
class
=
"
input-label
"
>
{{
{{
generateForm
.
type
===
'
balance
'
generateForm
.
type
===
'
balance
'
...
@@ -230,6 +230,12 @@
...
@@ -230,6 +230,12 @@
class
=
"
input
"
class
=
"
input
"
/>
/>
<
/div
>
<
/div
>
<!--
邀请码类型
:
显示提示信息
-->
<
div
v
-
if
=
"
generateForm.type === 'invitation'
"
class
=
"
rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20
"
>
<
p
class
=
"
text-sm text-blue-700 dark:text-blue-300
"
>
{{
t
(
'
admin.redeem.invitationHint
'
)
}}
<
/p
>
<
/div
>
<!--
订阅类型
:
显示分组选择和有效天数
-->
<!--
订阅类型
:
显示分组选择和有效天数
-->
<
template
v
-
if
=
"
generateForm.type === 'subscription'
"
>
<
template
v
-
if
=
"
generateForm.type === 'subscription'
"
>
<
div
>
<
div
>
...
@@ -387,7 +393,7 @@
...
@@ -387,7 +393,7 @@
<
/template
>
<
/template
>
<
script
setup
lang
=
"
ts
"
>
<
script
setup
lang
=
"
ts
"
>
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
}
from
'
vue
'
import
{
ref
,
reactive
,
computed
,
onMounted
,
onUnmounted
,
watch
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useAppStore
}
from
'
@/stores/app
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
import
{
useClipboard
}
from
'
@/composables/useClipboard
'
...
@@ -499,14 +505,16 @@ const columns = computed<Column[]>(() => [
...
@@ -499,14 +505,16 @@ const columns = computed<Column[]>(() => [
const
typeOptions
=
computed
(()
=>
[
const
typeOptions
=
computed
(()
=>
[
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
,
{
value
:
'
invitation
'
,
label
:
t
(
'
admin.redeem.invitation
'
)
}
])
])
const
filterTypeOptions
=
computed
(()
=>
[
const
filterTypeOptions
=
computed
(()
=>
[
{
value
:
''
,
label
:
t
(
'
admin.redeem.allTypes
'
)
}
,
{
value
:
''
,
label
:
t
(
'
admin.redeem.allTypes
'
)
}
,
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
balance
'
,
label
:
t
(
'
admin.redeem.balance
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
concurrency
'
,
label
:
t
(
'
admin.redeem.concurrency
'
)
}
,
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
{
value
:
'
subscription
'
,
label
:
t
(
'
admin.redeem.subscription
'
)
}
,
{
value
:
'
invitation
'
,
label
:
t
(
'
admin.redeem.invitation
'
)
}
])
])
const
filterStatusOptions
=
computed
(()
=>
[
const
filterStatusOptions
=
computed
(()
=>
[
...
@@ -546,6 +554,18 @@ const generateForm = reactive({
...
@@ -546,6 +554,18 @@ const generateForm = reactive({
validity_days
:
30
validity_days
:
30
}
)
}
)
// 监听类型变化,邀请码类型时自动设置 value 为 0
watch
(
()
=>
generateForm
.
type
,
(
newType
)
=>
{
if
(
newType
===
'
invitation
'
)
{
generateForm
.
value
=
0
}
else
if
(
generateForm
.
value
===
0
)
{
generateForm
.
value
=
10
}
}
)
const
loadCodes
=
async
()
=>
{
const
loadCodes
=
async
()
=>
{
if
(
abortController
)
{
if
(
abortController
)
{
abortController
.
abort
()
abortController
.
abort
()
...
...
frontend/src/views/admin/SettingsView.vue
View file @
7229b41f
...
@@ -339,6 +339,21 @@
...
@@ -339,6 +339,21 @@
<Toggle
v-model=
"form.promo_code_enabled"
/>
<Toggle
v-model=
"form.promo_code_enabled"
/>
</div>
</div>
<!-- Invitation Code -->
<div
class=
"flex items-center justify-between border-t border-gray-100 pt-4 dark:border-dark-700"
>
<div>
<label
class=
"font-medium text-gray-900 dark:text-white"
>
{{
t('admin.settings.registration.invitationCode')
}}
</label>
<p
class=
"text-sm text-gray-500 dark:text-gray-400"
>
{{ t('admin.settings.registration.invitationCodeHint') }}
</p>
</div>
<Toggle
v-model=
"form.invitation_code_enabled"
/>
</div>
<!-- Password Reset - Only show when email verification is enabled -->
<!-- Password Reset - Only show when email verification is enabled -->
<div
<div
v-if=
"form.email_verify_enabled"
v-if=
"form.email_verify_enabled"
...
@@ -1115,6 +1130,7 @@ const form = reactive<SettingsForm>({
...
@@ -1115,6 +1130,7 @@ const form = reactive<SettingsForm>({
registration_enabled
:
true
,
registration_enabled
:
true
,
email_verify_enabled
:
false
,
email_verify_enabled
:
false
,
promo_code_enabled
:
true
,
promo_code_enabled
:
true
,
invitation_code_enabled
:
false
,
password_reset_enabled
:
false
,
password_reset_enabled
:
false
,
totp_enabled
:
false
,
totp_enabled
:
false
,
totp_encryption_key_configured
:
false
,
totp_encryption_key_configured
:
false
,
...
@@ -1243,6 +1259,7 @@ async function saveSettings() {
...
@@ -1243,6 +1259,7 @@ async function saveSettings() {
registration_enabled
:
form
.
registration_enabled
,
registration_enabled
:
form
.
registration_enabled
,
email_verify_enabled
:
form
.
email_verify_enabled
,
email_verify_enabled
:
form
.
email_verify_enabled
,
promo_code_enabled
:
form
.
promo_code_enabled
,
promo_code_enabled
:
form
.
promo_code_enabled
,
invitation_code_enabled
:
form
.
invitation_code_enabled
,
password_reset_enabled
:
form
.
password_reset_enabled
,
password_reset_enabled
:
form
.
password_reset_enabled
,
totp_enabled
:
form
.
totp_enabled
,
totp_enabled
:
form
.
totp_enabled
,
default_balance
:
form
.
default_balance
,
default_balance
:
form
.
default_balance
,
...
...
frontend/src/views/auth/EmailVerifyView.vue
View file @
7229b41f
...
@@ -201,6 +201,7 @@ const email = ref<string>('')
...
@@ -201,6 +201,7 @@ const email = ref<string>('')
const
password
=
ref
<
string
>
(
''
)
const
password
=
ref
<
string
>
(
''
)
const
initialTurnstileToken
=
ref
<
string
>
(
''
)
const
initialTurnstileToken
=
ref
<
string
>
(
''
)
const
promoCode
=
ref
<
string
>
(
''
)
const
promoCode
=
ref
<
string
>
(
''
)
const
invitationCode
=
ref
<
string
>
(
''
)
const
hasRegisterData
=
ref
<
boolean
>
(
false
)
const
hasRegisterData
=
ref
<
boolean
>
(
false
)
// Public settings
// Public settings
...
@@ -230,6 +231,7 @@ onMounted(async () => {
...
@@ -230,6 +231,7 @@ onMounted(async () => {
password
.
value
=
registerData
.
password
||
''
password
.
value
=
registerData
.
password
||
''
initialTurnstileToken
.
value
=
registerData
.
turnstile_token
||
''
initialTurnstileToken
.
value
=
registerData
.
turnstile_token
||
''
promoCode
.
value
=
registerData
.
promo_code
||
''
promoCode
.
value
=
registerData
.
promo_code
||
''
invitationCode
.
value
=
registerData
.
invitation_code
||
''
hasRegisterData
.
value
=
!!
(
email
.
value
&&
password
.
value
)
hasRegisterData
.
value
=
!!
(
email
.
value
&&
password
.
value
)
}
catch
{
}
catch
{
hasRegisterData
.
value
=
false
hasRegisterData
.
value
=
false
...
@@ -384,7 +386,8 @@ async function handleVerify(): Promise<void> {
...
@@ -384,7 +386,8 @@ async function handleVerify(): Promise<void> {
password
:
password
.
value
,
password
:
password
.
value
,
verify_code
:
verifyCode
.
value
.
trim
(),
verify_code
:
verifyCode
.
value
.
trim
(),
turnstile_token
:
initialTurnstileToken
.
value
||
undefined
,
turnstile_token
:
initialTurnstileToken
.
value
||
undefined
,
promo_code
:
promoCode
.
value
||
undefined
promo_code
:
promoCode
.
value
||
undefined
,
invitation_code
:
invitationCode
.
value
||
undefined
})
})
// Clear session data
// Clear session data
...
...
frontend/src/views/auth/RegisterView.vue
View file @
7229b41f
...
@@ -95,6 +95,59 @@
...
@@ -95,6 +95,59 @@
<
/p
>
<
/p
>
<
/div
>
<
/div
>
<!--
Invitation
Code
Input
(
Required
when
enabled
)
-->
<
div
v
-
if
=
"
invitationCodeEnabled
"
>
<
label
for
=
"
invitation_code
"
class
=
"
input-label
"
>
{{
t
(
'
auth.invitationCodeLabel
'
)
}}
<
/label
>
<
div
class
=
"
relative
"
>
<
div
class
=
"
pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5
"
>
<
Icon
name
=
"
key
"
size
=
"
md
"
:
class
=
"
invitationValidation.valid ? 'text-green-500' : 'text-gray-400 dark:text-dark-500'
"
/>
<
/div
>
<
input
id
=
"
invitation_code
"
v
-
model
=
"
formData.invitation_code
"
type
=
"
text
"
:
disabled
=
"
isLoading
"
class
=
"
input pl-11 pr-10
"
:
class
=
"
{
'border-green-500 focus:border-green-500 focus:ring-green-500': invitationValidation.valid,
'border-red-500 focus:border-red-500 focus:ring-red-500': invitationValidation.invalid || errors.invitation_code
}
"
:
placeholder
=
"
t('auth.invitationCodePlaceholder')
"
@
input
=
"
handleInvitationCodeInput
"
/>
<!--
Validation
indicator
-->
<
div
v
-
if
=
"
invitationValidating
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
svg
class
=
"
h-4 w-4 animate-spin text-gray-400
"
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
>
<
/div
>
<
div
v
-
else
-
if
=
"
invitationValidation.valid
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
checkCircle
"
size
=
"
md
"
class
=
"
text-green-500
"
/>
<
/div
>
<
div
v
-
else
-
if
=
"
invitationValidation.invalid || errors.invitation_code
"
class
=
"
absolute inset-y-0 right-0 flex items-center pr-3.5
"
>
<
Icon
name
=
"
exclamationCircle
"
size
=
"
md
"
class
=
"
text-red-500
"
/>
<
/div
>
<
/div
>
<!--
Invitation
code
validation
result
-->
<
transition
name
=
"
fade
"
>
<
div
v
-
if
=
"
invitationValidation.valid
"
class
=
"
mt-2 flex items-center gap-2 rounded-lg bg-green-50 px-3 py-2 dark:bg-green-900/20
"
>
<
Icon
name
=
"
checkCircle
"
size
=
"
sm
"
class
=
"
text-green-600 dark:text-green-400
"
/>
<
span
class
=
"
text-sm text-green-700 dark:text-green-400
"
>
{{
t
(
'
auth.invitationCodeValid
'
)
}}
<
/span
>
<
/div
>
<
p
v
-
else
-
if
=
"
invitationValidation.invalid
"
class
=
"
input-error-text
"
>
{{
invitationValidation
.
message
}}
<
/p
>
<
p
v
-
else
-
if
=
"
errors.invitation_code
"
class
=
"
input-error-text
"
>
{{
errors
.
invitation_code
}}
<
/p
>
<
/transition
>
<
/div
>
<!--
Promo
Code
Input
(
Optional
)
-->
<!--
Promo
Code
Input
(
Optional
)
-->
<
div
v
-
if
=
"
promoCodeEnabled
"
>
<
div
v
-
if
=
"
promoCodeEnabled
"
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
<
label
for
=
"
promo_code
"
class
=
"
input-label
"
>
...
@@ -239,7 +292,7 @@ import LinuxDoOAuthSection from '@/components/auth/LinuxDoOAuthSection.vue'
...
@@ -239,7 +292,7 @@ import LinuxDoOAuthSection from '@/components/auth/LinuxDoOAuthSection.vue'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
TurnstileWidget
from
'
@/components/TurnstileWidget.vue
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
useAuthStore
,
useAppStore
}
from
'
@/stores
'
import
{
getPublicSettings
,
validatePromoCode
}
from
'
@/api/auth
'
import
{
getPublicSettings
,
validatePromoCode
,
validateInvitationCode
}
from
'
@/api/auth
'
const
{
t
}
=
useI18n
()
const
{
t
}
=
useI18n
()
...
@@ -261,6 +314,7 @@ const showPassword = ref<boolean>(false)
...
@@ -261,6 +314,7 @@ const showPassword = ref<boolean>(false)
const
registrationEnabled
=
ref
<
boolean
>
(
true
)
const
registrationEnabled
=
ref
<
boolean
>
(
true
)
const
emailVerifyEnabled
=
ref
<
boolean
>
(
false
)
const
emailVerifyEnabled
=
ref
<
boolean
>
(
false
)
const
promoCodeEnabled
=
ref
<
boolean
>
(
true
)
const
promoCodeEnabled
=
ref
<
boolean
>
(
true
)
const
invitationCodeEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileEnabled
=
ref
<
boolean
>
(
false
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
turnstileSiteKey
=
ref
<
string
>
(
''
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
const
siteName
=
ref
<
string
>
(
'
Sub2API
'
)
...
@@ -280,16 +334,27 @@ const promoValidation = reactive({
...
@@ -280,16 +334,27 @@ const promoValidation = reactive({
}
)
}
)
let
promoValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
let
promoValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
// Invitation code validation
const
invitationValidating
=
ref
<
boolean
>
(
false
)
const
invitationValidation
=
reactive
({
valid
:
false
,
invalid
:
false
,
message
:
''
}
)
let
invitationValidateTimeout
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
formData
=
reactive
({
const
formData
=
reactive
({
email
:
''
,
email
:
''
,
password
:
''
,
password
:
''
,
promo_code
:
''
promo_code
:
''
,
invitation_code
:
''
}
)
}
)
const
errors
=
reactive
({
const
errors
=
reactive
({
email
:
''
,
email
:
''
,
password
:
''
,
password
:
''
,
turnstile
:
''
turnstile
:
''
,
invitation_code
:
''
}
)
}
)
// ==================== Lifecycle ====================
// ==================== Lifecycle ====================
...
@@ -300,6 +365,7 @@ onMounted(async () => {
...
@@ -300,6 +365,7 @@ onMounted(async () => {
registrationEnabled
.
value
=
settings
.
registration_enabled
registrationEnabled
.
value
=
settings
.
registration_enabled
emailVerifyEnabled
.
value
=
settings
.
email_verify_enabled
emailVerifyEnabled
.
value
=
settings
.
email_verify_enabled
promoCodeEnabled
.
value
=
settings
.
promo_code_enabled
promoCodeEnabled
.
value
=
settings
.
promo_code_enabled
invitationCodeEnabled
.
value
=
settings
.
invitation_code_enabled
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileEnabled
.
value
=
settings
.
turnstile_enabled
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
turnstileSiteKey
.
value
=
settings
.
turnstile_site_key
||
''
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
siteName
.
value
=
settings
.
site_name
||
'
Sub2API
'
...
@@ -325,6 +391,9 @@ onUnmounted(() => {
...
@@ -325,6 +391,9 @@ onUnmounted(() => {
if
(
promoValidateTimeout
)
{
if
(
promoValidateTimeout
)
{
clearTimeout
(
promoValidateTimeout
)
clearTimeout
(
promoValidateTimeout
)
}
}
if
(
invitationValidateTimeout
)
{
clearTimeout
(
invitationValidateTimeout
)
}
}
)
}
)
// ==================== Promo Code Validation ====================
// ==================== Promo Code Validation ====================
...
@@ -400,6 +469,70 @@ function getPromoErrorMessage(errorCode?: string): string {
...
@@ -400,6 +469,70 @@ function getPromoErrorMessage(errorCode?: string): string {
}
}
}
}
// ==================== Invitation Code Validation ====================
function
handleInvitationCodeInput
():
void
{
const
code
=
formData
.
invitation_code
.
trim
()
// Clear previous validation
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
false
invitationValidation
.
message
=
''
errors
.
invitation_code
=
''
if
(
!
code
)
{
return
}
// Debounce validation
if
(
invitationValidateTimeout
)
{
clearTimeout
(
invitationValidateTimeout
)
}
invitationValidateTimeout
=
setTimeout
(()
=>
{
validateInvitationCodeDebounced
(
code
)
}
,
500
)
}
async
function
validateInvitationCodeDebounced
(
code
:
string
):
Promise
<
void
>
{
invitationValidating
.
value
=
true
try
{
const
result
=
await
validateInvitationCode
(
code
)
if
(
result
.
valid
)
{
invitationValidation
.
valid
=
true
invitationValidation
.
invalid
=
false
invitationValidation
.
message
=
''
}
else
{
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
true
invitationValidation
.
message
=
getInvitationErrorMessage
(
result
.
error_code
)
}
}
catch
{
invitationValidation
.
valid
=
false
invitationValidation
.
invalid
=
true
invitationValidation
.
message
=
t
(
'
auth.invitationCodeInvalid
'
)
}
finally
{
invitationValidating
.
value
=
false
}
}
function
getInvitationErrorMessage
(
errorCode
?:
string
):
string
{
switch
(
errorCode
)
{
case
'
INVITATION_CODE_NOT_FOUND
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_INVALID
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_USED
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
case
'
INVITATION_CODE_DISABLED
'
:
return
t
(
'
auth.invitationCodeInvalid
'
)
default
:
return
t
(
'
auth.invitationCodeInvalid
'
)
}
}
// ==================== Turnstile Handlers ====================
// ==================== Turnstile Handlers ====================
function
onTurnstileVerify
(
token
:
string
):
void
{
function
onTurnstileVerify
(
token
:
string
):
void
{
...
@@ -429,6 +562,7 @@ function validateForm(): boolean {
...
@@ -429,6 +562,7 @@ function validateForm(): boolean {
errors
.
email
=
''
errors
.
email
=
''
errors
.
password
=
''
errors
.
password
=
''
errors
.
turnstile
=
''
errors
.
turnstile
=
''
errors
.
invitation_code
=
''
let
isValid
=
true
let
isValid
=
true
...
@@ -450,6 +584,14 @@ function validateForm(): boolean {
...
@@ -450,6 +584,14 @@ function validateForm(): boolean {
isValid
=
false
isValid
=
false
}
}
// Invitation code validation (required when enabled)
if
(
invitationCodeEnabled
.
value
)
{
if
(
!
formData
.
invitation_code
.
trim
())
{
errors
.
invitation_code
=
t
(
'
auth.invitationCodeRequired
'
)
isValid
=
false
}
}
// Turnstile validation
// Turnstile validation
if
(
turnstileEnabled
.
value
&&
!
turnstileToken
.
value
)
{
if
(
turnstileEnabled
.
value
&&
!
turnstileToken
.
value
)
{
errors
.
turnstile
=
t
(
'
auth.completeVerification
'
)
errors
.
turnstile
=
t
(
'
auth.completeVerification
'
)
...
@@ -484,6 +626,30 @@ async function handleRegister(): Promise<void> {
...
@@ -484,6 +626,30 @@ async function handleRegister(): Promise<void> {
}
}
}
}
// Check invitation code validation status (if enabled and code provided)
if
(
invitationCodeEnabled
.
value
)
{
// If still validating, wait
if
(
invitationValidating
.
value
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeValidating
'
)
return
}
// If invitation code is invalid, block submission
if
(
invitationValidation
.
invalid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeInvalidCannotRegister
'
)
return
}
// If invitation code is required but not validated yet
if
(
formData
.
invitation_code
.
trim
()
&&
!
invitationValidation
.
valid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeValidating
'
)
// Trigger validation
await
validateInvitationCodeDebounced
(
formData
.
invitation_code
.
trim
())
if
(
!
invitationValidation
.
valid
)
{
errorMessage
.
value
=
t
(
'
auth.invitationCodeInvalidCannotRegister
'
)
return
}
}
}
isLoading
.
value
=
true
isLoading
.
value
=
true
try
{
try
{
...
@@ -496,7 +662,8 @@ async function handleRegister(): Promise<void> {
...
@@ -496,7 +662,8 @@ async function handleRegister(): Promise<void> {
email
:
formData
.
email
,
email
:
formData
.
email
,
password
:
formData
.
password
,
password
:
formData
.
password
,
turnstile_token
:
turnstileToken
.
value
,
turnstile_token
:
turnstileToken
.
value
,
promo_code
:
formData
.
promo_code
||
undefined
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
}
)
}
)
)
)
...
@@ -510,7 +677,8 @@ async function handleRegister(): Promise<void> {
...
@@ -510,7 +677,8 @@ async function handleRegister(): Promise<void> {
email
:
formData
.
email
,
email
:
formData
.
email
,
password
:
formData
.
password
,
password
:
formData
.
password
,
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
,
turnstile_token
:
turnstileEnabled
.
value
?
turnstileToken
.
value
:
undefined
,
promo_code
:
formData
.
promo_code
||
undefined
promo_code
:
formData
.
promo_code
||
undefined
,
invitation_code
:
formData
.
invitation_code
||
undefined
}
)
}
)
// Show success toast
// Show success toast
...
...
Prev
1
2
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