Unverified Commit 3f05ef2a authored by Oliver Li's avatar Oliver Li Committed by GitHub
Browse files

Merge branch 'Wei-Shaw:main' into vertex

parents 6d11f9ed c056db74
...@@ -15,17 +15,20 @@ ...@@ -15,17 +15,20 @@
<LinuxDoOAuthSection <LinuxDoOAuthSection
v-if="linuxdoOAuthEnabled" v-if="linuxdoOAuthEnabled"
:disabled="isLoading" :disabled="isLoading"
:aff-code="formData.aff_code"
:show-divider="false" :show-divider="false"
/> />
<WechatOAuthSection <WechatOAuthSection
v-if="wechatOAuthEnabled" v-if="wechatOAuthEnabled"
:disabled="isLoading" :disabled="isLoading"
:aff-code="formData.aff_code"
:show-divider="false" :show-divider="false"
/> />
<OidcOAuthSection <OidcOAuthSection
v-if="oidcOAuthEnabled" v-if="oidcOAuthEnabled"
:disabled="isLoading" :disabled="isLoading"
:provider-name="oidcOAuthProviderName" :provider-name="oidcOAuthProviderName"
:aff-code="formData.aff_code"
:show-divider="false" :show-divider="false"
/> />
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
...@@ -293,6 +296,11 @@ import { ...@@ -293,6 +296,11 @@ import {
isRegistrationEmailSuffixAllowed, isRegistrationEmailSuffixAllowed,
normalizeRegistrationEmailSuffixWhitelist normalizeRegistrationEmailSuffixWhitelist
} from '@/utils/registrationEmailPolicy' } from '@/utils/registrationEmailPolicy'
import {
clearAffiliateReferralCode,
loadAffiliateReferralCode,
resolveAffiliateReferralCode
} from '@/utils/oauthAffiliate'
const { t, locale } = useI18n() const { t, locale } = useI18n()
...@@ -378,9 +386,19 @@ watch(validationToastMessage, (value, previousValue) => { ...@@ -378,9 +386,19 @@ watch(validationToastMessage, (value, previousValue) => {
} }
}) })
function syncAffiliateReferralCode(): string {
const code = resolveAffiliateReferralCode(route.query.aff, route.query.aff_code)
if (code) {
formData.aff_code = code
}
return code
}
// ==================== Lifecycle ==================== // ==================== Lifecycle ====================
onMounted(async () => { onMounted(async () => {
syncAffiliateReferralCode()
try { try {
const settings = await getPublicSettings() const settings = await getPublicSettings()
registrationEnabled.value = settings.registration_enabled registrationEnabled.value = settings.registration_enabled
...@@ -407,10 +425,7 @@ onMounted(async () => { ...@@ -407,10 +425,7 @@ onMounted(async () => {
await validatePromoCodeDebounced(promoParam) await validatePromoCodeDebounced(promoParam)
} }
} }
const affParam = (route.query.aff as string) || (route.query.aff_code as string) syncAffiliateReferralCode()
if (affParam) {
formData.aff_code = affParam.trim()
}
} catch (error) { } catch (error) {
console.error('Failed to load public settings:', error) console.error('Failed to load public settings:', error)
} finally { } finally {
...@@ -418,6 +433,13 @@ onMounted(async () => { ...@@ -418,6 +433,13 @@ onMounted(async () => {
} }
}) })
watch(
() => [route.query.aff, route.query.aff_code],
() => {
syncAffiliateReferralCode()
}
)
onUnmounted(() => { onUnmounted(() => {
if (promoValidateTimeout) { if (promoValidateTimeout) {
clearTimeout(promoValidateTimeout) clearTimeout(promoValidateTimeout)
...@@ -702,6 +724,11 @@ async function handleRegister(): Promise<void> { ...@@ -702,6 +724,11 @@ async function handleRegister(): Promise<void> {
isLoading.value = true isLoading.value = true
try { try {
const affCode = formData.aff_code.trim() || loadAffiliateReferralCode()
if (affCode) {
formData.aff_code = affCode
}
// If email verification is enabled, redirect to verification page // If email verification is enabled, redirect to verification page
if (emailVerifyEnabled.value) { if (emailVerifyEnabled.value) {
// Store registration data in sessionStorage // Store registration data in sessionStorage
...@@ -713,7 +740,7 @@ async function handleRegister(): Promise<void> { ...@@ -713,7 +740,7 @@ async function handleRegister(): Promise<void> {
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, invitation_code: formData.invitation_code || undefined,
...(formData.aff_code ? { aff_code: formData.aff_code } : {}) ...(affCode ? { aff_code: affCode } : {})
}) })
) )
...@@ -729,8 +756,9 @@ async function handleRegister(): Promise<void> { ...@@ -729,8 +756,9 @@ async function handleRegister(): Promise<void> {
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, invitation_code: formData.invitation_code || undefined,
...(formData.aff_code ? { aff_code: formData.aff_code } : {}) ...(affCode ? { aff_code: affCode } : {})
}) })
clearAffiliateReferralCode()
// Show success toast // Show success toast
appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value })) appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value }))
......
...@@ -340,6 +340,11 @@ import { ...@@ -340,6 +340,11 @@ import {
type OAuthTokenResponse, type OAuthTokenResponse,
type PendingOAuthExchangeResponse type PendingOAuthExchangeResponse
} from '@/api/auth' } from '@/api/auth'
import {
clearAllAffiliateReferralCodes,
loadOAuthAffiliateCode,
oauthAffiliatePayload
} from '@/utils/oauthAffiliate'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
...@@ -802,6 +807,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi ...@@ -802,6 +807,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
if (getOAuthCompletionKind(completion) === 'bind') { if (getOAuthCompletionKind(completion) === 'bind') {
const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile') const bindRedirect = sanitizeRedirectPath(completion.redirect || '/profile')
clearPendingAuthSession() clearPendingAuthSession()
clearAllAffiliateReferralCodes()
appStore.showSuccess(bindSuccessMessage) appStore.showSuccess(bindSuccessMessage)
await router.replace(bindRedirect) await router.replace(bindRedirect)
return return
...@@ -813,6 +819,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi ...@@ -813,6 +819,7 @@ async function finalizeCompletion(completion: PendingOAuthExchangeResponse, redi
persistOAuthTokenContext(completion) persistOAuthTokenContext(completion)
await authStore.setToken(completion.access_token) await authStore.setToken(completion.access_token)
clearAllAffiliateReferralCodes()
appStore.showSuccess(t('auth.loginSuccess')) appStore.showSuccess(t('auth.loginSuccess'))
await router.replace(redirect) await router.replace(redirect)
} }
...@@ -861,18 +868,20 @@ async function handleSubmitInvitation() { ...@@ -861,18 +868,20 @@ async function handleSubmitInvitation() {
isSubmitting.value = true isSubmitting.value = true
try { try {
const affCode = loadOAuthAffiliateCode()
const decision = currentAdoptionDecision()
const completion: PendingWeChatCompletion = legacyPendingOAuthToken.value const completion: PendingWeChatCompletion = legacyPendingOAuthToken.value
? ( ? (
await apiClient.post<PendingWeChatCompletion>('/auth/oauth/wechat/complete-registration', { await apiClient.post<PendingWeChatCompletion>('/auth/oauth/wechat/complete-registration', {
pending_oauth_token: legacyPendingOAuthToken.value, pending_oauth_token: legacyPendingOAuthToken.value,
invitation_code: invitationCode.value.trim(), invitation_code: invitationCode.value.trim(),
...serializeAdoptionDecision(currentAdoptionDecision()) ...oauthAffiliatePayload(affCode),
...serializeAdoptionDecision(decision)
}) })
).data ).data
: await completeWeChatOAuthRegistration( : affCode
invitationCode.value.trim(), ? await completeWeChatOAuthRegistration(invitationCode.value.trim(), decision, affCode)
currentAdoptionDecision() : await completeWeChatOAuthRegistration(invitationCode.value.trim(), decision)
)
await finalizePendingAccountResponse(completion) await finalizePendingAccountResponse(completion)
} catch (e: unknown) { } catch (e: unknown) {
const err = e as { message?: string; response?: { data?: { message?: string } } } const err = e as { message?: string; response?: { data?: { message?: string } } }
...@@ -907,6 +916,7 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) { ...@@ -907,6 +916,7 @@ async function handleCreateAccount(payload: PendingOAuthCreateAccountPayload) {
password: payload.password, password: payload.password,
verify_code: payload.verifyCode || undefined, verify_code: payload.verifyCode || undefined,
invitation_code: payload.invitationCode || undefined, invitation_code: payload.invitationCode || undefined,
...oauthAffiliatePayload(loadOAuthAffiliateCode()),
...serializeAdoptionDecision(currentAdoptionDecision()) ...serializeAdoptionDecision(currentAdoptionDecision())
}) })
await finalizePendingAccountResponse(data) await finalizePendingAccountResponse(data)
...@@ -955,6 +965,7 @@ async function handleSubmitTotpChallenge() { ...@@ -955,6 +965,7 @@ async function handleSubmitTotpChallenge() {
}) })
persistOAuthTokenContext(completion) persistOAuthTokenContext(completion)
await authStore.setToken(completion.access_token) await authStore.setToken(completion.access_token)
clearAllAffiliateReferralCodes()
appStore.showSuccess(t('auth.loginSuccess')) appStore.showSuccess(t('auth.loginSuccess'))
await router.replace(redirectTo.value) await router.replace(redirectTo.value)
} catch (e: unknown) { } catch (e: unknown) {
...@@ -1015,6 +1026,7 @@ onMounted(async () => { ...@@ -1015,6 +1026,7 @@ onMounted(async () => {
if (legacyLogin) { if (legacyLogin) {
persistOAuthTokenContext(legacyLogin) persistOAuthTokenContext(legacyLogin)
await authStore.setToken(legacyLogin.access_token) await authStore.setToken(legacyLogin.access_token)
clearAllAffiliateReferralCodes()
appStore.showSuccess(t('auth.loginSuccess')) appStore.showSuccess(t('auth.loginSuccess'))
await router.replace(redirect) await router.replace(redirect)
return return
......
...@@ -112,6 +112,7 @@ describe('EmailVerifyView', () => { ...@@ -112,6 +112,7 @@ describe('EmailVerifyView', () => {
apiClientPostMock.mockReset() apiClientPostMock.mockReset()
authStoreState.pendingAuthSession = null authStoreState.pendingAuthSession = null
sessionStorage.clear() sessionStorage.clear()
localStorage.clear()
getPublicSettingsMock.mockResolvedValue({ getPublicSettingsMock.mockResolvedValue({
turnstile_enabled: false, turnstile_enabled: false,
...@@ -136,6 +137,7 @@ describe('EmailVerifyView', () => { ...@@ -136,6 +137,7 @@ describe('EmailVerifyView', () => {
JSON.stringify({ JSON.stringify({
email: 'fresh@example.com', email: 'fresh@example.com',
password: 'secret-123', password: 'secret-123',
aff_code: 'AFF123',
}) })
) )
...@@ -334,6 +336,7 @@ describe('EmailVerifyView', () => { ...@@ -334,6 +336,7 @@ describe('EmailVerifyView', () => {
email: 'fresh@example.com', email: 'fresh@example.com',
password: 'secret-123', password: 'secret-123',
verify_code: '123456', verify_code: '123456',
aff_code: 'AFF123',
}) })
expect(persistOAuthTokenContextMock).toHaveBeenCalledWith({ expect(persistOAuthTokenContextMock).toHaveBeenCalledWith({
access_token: 'oauth-access-token', access_token: 'oauth-access-token',
......
...@@ -93,6 +93,7 @@ describe('LinuxDoCallbackView', () => { ...@@ -93,6 +93,7 @@ describe('LinuxDoCallbackView', () => {
}) })
window.location.hash = '' window.location.hash = ''
localStorage.clear() localStorage.clear()
sessionStorage.clear()
}) })
it('accepts the legacy fragment token success callback without pending-session exchange', async () => { it('accepts the legacy fragment token success callback without pending-session exchange', async () => {
......
...@@ -97,6 +97,7 @@ describe('OidcCallbackView', () => { ...@@ -97,6 +97,7 @@ describe('OidcCallbackView', () => {
}) })
window.location.hash = '' window.location.hash = ''
localStorage.clear() localStorage.clear()
sessionStorage.clear()
}) })
it('accepts the legacy fragment token success callback without pending-session exchange', async () => { it('accepts the legacy fragment token success callback without pending-session exchange', async () => {
......
...@@ -172,6 +172,7 @@ describe('WechatCallbackView', () => { ...@@ -172,6 +172,7 @@ describe('WechatCallbackView', () => {
appStoreState.cachedPublicSettings = null appStoreState.cachedPublicSettings = null
appStoreState.publicSettingsLoaded = false appStoreState.publicSettingsLoaded = false
localStorage.clear() localStorage.clear()
sessionStorage.clear()
locationState.current = { locationState.current = {
href: 'http://localhost/auth/wechat/callback', href: 'http://localhost/auth/wechat/callback',
hash: '', hash: '',
......
...@@ -9,21 +9,17 @@ ...@@ -9,21 +9,17 @@
<template v-else-if="detail"> <template v-else-if="detail">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- 返利比例:用主色突出,让用户一眼看到「能拿多少」 --> <div class="card p-5">
<div class="card relative overflow-hidden p-5"> <p class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-dark-400">
<div class="absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary-500/10"></div> <Icon name="dollar" size="sm" class="text-primary-500" />
<div class="relative"> {{ t('affiliate.stats.rebateRate') }}
<p class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-dark-400"> </p>
<Icon name="dollar" size="sm" class="text-primary-500" /> <p class="mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400">
{{ t('affiliate.stats.rebateRate') }} {{ formattedRebateRate }}<span class="ml-0.5 text-base font-medium">%</span>
</p> </p>
<p class="mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400"> <p class="mt-1 text-xs text-gray-400 dark:text-dark-500">
{{ formattedRebateRate }}<span class="ml-0.5 text-base font-medium">%</span> {{ t('affiliate.stats.rebateRateHint') }}
</p> </p>
<p class="mt-1 text-xs text-gray-400 dark:text-dark-500">
{{ t('affiliate.stats.rebateRateHint') }}
</p>
</div>
</div> </div>
<div class="card p-5"> <div class="card p-5">
<p class="text-sm text-gray-500 dark:text-dark-400">{{ t('affiliate.stats.invitedUsers') }}</p> <p class="text-sm text-gray-500 dark:text-dark-400">{{ t('affiliate.stats.invitedUsers') }}</p>
...@@ -42,6 +38,9 @@ ...@@ -42,6 +38,9 @@
<p class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white"> <p class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
{{ formatCurrency(detail.aff_history_quota) }} {{ formatCurrency(detail.aff_history_quota) }}
</p> </p>
<p v-if="detail.aff_frozen_quota > 0" class="mt-1 text-xs text-amber-600 dark:text-amber-400">
{{ t('affiliate.stats.frozenQuota') }}: {{ formatCurrency(detail.aff_frozen_quota) }}
</p>
</div> </div>
</div> </div>
...@@ -79,6 +78,7 @@ ...@@ -79,6 +78,7 @@
<li>1. {{ t('affiliate.tips.line1') }}</li> <li>1. {{ t('affiliate.tips.line1') }}</li>
<li>2. {{ t('affiliate.tips.line2', { rate: `${formattedRebateRate}%` }) }}</li> <li>2. {{ t('affiliate.tips.line2', { rate: `${formattedRebateRate}%` }) }}</li>
<li>3. {{ t('affiliate.tips.line3') }}</li> <li>3. {{ t('affiliate.tips.line3') }}</li>
<li v-if="detail.aff_frozen_quota > 0">4. {{ t('affiliate.tips.line4') }}</li>
</ul> </ul>
</div> </div>
</div> </div>
...@@ -115,6 +115,7 @@ ...@@ -115,6 +115,7 @@
<tr class="border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-dark-400"> <tr class="border-b border-gray-200 text-gray-500 dark:border-dark-700 dark:text-dark-400">
<th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.email') }}</th> <th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.email') }}</th>
<th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.username') }}</th> <th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.username') }}</th>
<th class="px-3 py-2 font-medium text-right">{{ t('affiliate.invitees.columns.rebate') }}</th>
<th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.joinedAt') }}</th> <th class="px-3 py-2 font-medium">{{ t('affiliate.invitees.columns.joinedAt') }}</th>
</tr> </tr>
</thead> </thead>
...@@ -126,6 +127,7 @@ ...@@ -126,6 +127,7 @@
> >
<td class="px-3 py-3 text-gray-900 dark:text-white">{{ item.email || '-' }}</td> <td class="px-3 py-3 text-gray-900 dark:text-white">{{ item.email || '-' }}</td>
<td class="px-3 py-3 text-gray-700 dark:text-gray-300">{{ item.username || '-' }}</td> <td class="px-3 py-3 text-gray-700 dark:text-gray-300">{{ item.username || '-' }}</td>
<td class="px-3 py-3 text-right font-medium text-emerald-600 dark:text-emerald-400">{{ formatCurrency(item.total_rebate) }}</td>
<td class="px-3 py-3 text-gray-700 dark:text-gray-300">{{ formatDateTime(item.created_at) || '-' }}</td> <td class="px-3 py-3 text-gray-700 dark:text-gray-300">{{ formatDateTime(item.created_at) || '-' }}</td>
</tr> </tr>
</tbody> </tbody>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment