feat:完善租户端注册时的用户协议功能
This commit is contained in:
1 parent
a1f637fb2a
commit
73ab89d551
8 files changed
+79
-147
No files matched your search
@@ -35,7 +35,7 @@ type TenantAgreementAcceptRes struct{}
|
||||
// --- 公开端点(登录/注册页面使用,无需认证) ---
|
||||
|
||||
type AgreementCurrentListReq struct {
|
||||
g.Meta `path:"/agreements/current" method:"get" mime:"json" tags:"公开-用户协议" summary:"所有当前生效的协议列表" group:"public" middleware:"-"`
|
||||
g.Meta `path:"/agreements/current" method:"get" mime:"json" tags:"公开-用户协议" summary:"所有当前生效的协议列表" middleware:"-"`
|
||||
}
|
||||
|
||||
type PublicAgreementItem struct {
|
||||
@@ -52,7 +52,7 @@ type AgreementCurrentListRes struct {
|
||||
}
|
||||
|
||||
type AgreementCurrentGetReq struct {
|
||||
g.Meta `path:"/agreements/current/{code}" method:"get" mime:"json" tags:"公开-用户协议" summary:"按标识码获取当前协议详情" group:"public" middleware:"-"`
|
||||
g.Meta `path:"/agreements/current/{code}" method:"get" mime:"json" tags:"公开-用户协议" summary:"按标识码获取当前协议详情" middleware:"-"`
|
||||
Code string `json:"code" in:"path" v:"required|length:1,50"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
@@ -21,6 +23,12 @@ var tenantPublicPaths = map[string]bool{
|
||||
"/api/tenant/email/reset-password": true,
|
||||
"/api/tenant/members/join": true,
|
||||
"/api/tenant/members/invite-info": true,
|
||||
"/api/tenant/agreements/current": true,
|
||||
}
|
||||
|
||||
// tenantPublicPrefixes lists path prefixes that skip JWT auth (for dynamic routes like /current/{code}).
|
||||
var tenantPublicPrefixes = []string{
|
||||
"/api/tenant/agreements/current/",
|
||||
}
|
||||
|
||||
// TenantAuth is JWT authentication middleware for tenant console.
|
||||
@@ -31,6 +39,12 @@ func TenantAuth(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
for _, prefix := range tenantPublicPrefixes {
|
||||
if strings.HasPrefix(r.URL.Path, prefix) {
|
||||
r.Middleware.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tokenStr := extractBearerToken(r)
|
||||
if tokenStr == "" {
|
||||
|
||||
@@ -14,6 +14,7 @@ import ContentFilterTab from './settings/ContentFilterTab.vue'
|
||||
import ChannelTab from './settings/ChannelTab.vue'
|
||||
import StorageTab from './settings/StorageTab.vue'
|
||||
import DataGovernanceTab from './settings/DataGovernanceTab.vue'
|
||||
import AgreementTab from './settings/AgreementTab.vue'
|
||||
|
||||
interface SettingCategory {
|
||||
key: string
|
||||
@@ -34,6 +35,7 @@ const categoryMap: Record<string, Component> = {
|
||||
channel: ChannelTab,
|
||||
storage: StorageTab,
|
||||
data_governance: DataGovernanceTab,
|
||||
agreement: AgreementTab,
|
||||
}
|
||||
|
||||
const categories = ref<SettingCategory[]>([])
|
||||
|
||||
@@ -32,12 +32,6 @@ const tenantRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/auth/ForgotPasswordPage.vue'),
|
||||
meta: { requiresAuth: false, title: '找回密码' },
|
||||
},
|
||||
{
|
||||
path: '/tenant/agreement/:code',
|
||||
name: 'TenantAgreement',
|
||||
component: () => import('@/views/auth/AgreementPage.vue'),
|
||||
meta: { requiresAuth: false, title: '协议详情' },
|
||||
},
|
||||
{
|
||||
path: '/tenant/join',
|
||||
name: 'TenantJoin',
|
||||
|
||||
@@ -104,7 +104,7 @@ async function doRefresh(): Promise<TokenPair> {
|
||||
return newTokens
|
||||
}
|
||||
|
||||
const PUBLIC_PATHS = ['/settings/', '/captcha', '/captcha/', '/auth/login', '/auth/register']
|
||||
const PUBLIC_PATHS = ['/settings/', '/captcha', '/captcha/', '/auth/login', '/auth/register', '/agreements/']
|
||||
|
||||
function isPublicPath(url?: string): boolean {
|
||||
if (!url) return false
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { marked } from 'marked'
|
||||
import request from '@/utils/request'
|
||||
import AuthLayout from '@/components/layout/AuthLayout.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const code = computed(() => route.params.code as string)
|
||||
|
||||
const title = ref('')
|
||||
const version = ref('')
|
||||
const content = ref('')
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
const contentHtml = computed(() => {
|
||||
if (!content.value) return ''
|
||||
return marked(content.value) as string
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await request.get(`/tenant/agreements/current/${code.value}`)
|
||||
const data = res.data?.data
|
||||
if (data) {
|
||||
title.value = data.title
|
||||
version.value = data.version
|
||||
content.value = data.content || ''
|
||||
} else {
|
||||
error.value = '未找到该协议'
|
||||
}
|
||||
} catch {
|
||||
error.value = '加载协议内容失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<div class="animate-slide-up">
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||
<div class="spinner h-6 w-6 border-primary-500"></div>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gray-100">
|
||||
<Icon name="document" size="lg" class="text-gray-400" />
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else>
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-900">{{ title }}</h2>
|
||||
<p v-if="version" class="mt-1 text-xs text-gray-400">版本 {{ version }}</p>
|
||||
</div>
|
||||
<div class="agreement-content rounded-xl border border-gray-200 bg-gray-50 p-5" v-html="contentHtml" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<p class="text-gray-500">
|
||||
<router-link to="/tenant/login" class="text-primary-600 font-medium hover:text-primary-700 transition-colors">
|
||||
返回登录
|
||||
</router-link>
|
||||
</p>
|
||||
</template>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agreement-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.agreement-content :deep(h1),
|
||||
.agreement-content :deep(h2),
|
||||
.agreement-content :deep(h3) {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agreement-content :deep(h1) { font-size: 20px; }
|
||||
.agreement-content :deep(h2) { font-size: 17px; }
|
||||
.agreement-content :deep(h3) { font-size: 15px; }
|
||||
|
||||
.agreement-content :deep(ul),
|
||||
.agreement-content :deep(ol) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.agreement-content :deep(p) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import Icon from '@/components/common/Icon.vue'
|
||||
import SlideCaptcha from '@/components/common/SlideCaptcha.vue'
|
||||
import Turnstile from '@/components/common/Turnstile.vue'
|
||||
import AgreementAcceptModal from '@/components/common/AgreementAcceptModal.vue'
|
||||
import AgreementViewModal from '@/components/common/AgreementViewModal.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -46,6 +47,15 @@ const accountInput = ref<HTMLInputElement | null>(null)
|
||||
// Pending agreements
|
||||
const showAgreements = ref(false)
|
||||
|
||||
// Agreement view modal
|
||||
const showAgreementModal = ref(false)
|
||||
const agreementModalCode = ref('')
|
||||
|
||||
function openAgreement(code: string) {
|
||||
agreementModalCode.value = code
|
||||
showAgreementModal.value = true
|
||||
}
|
||||
|
||||
function proceedAfterLogin() {
|
||||
const pending = authStore.pendingAgreements
|
||||
if (pending.length > 0) {
|
||||
@@ -360,6 +370,11 @@ async function handleOAuthLogin(provider: string) {
|
||||
<div v-if="loading" class="spinner h-4 w-4 border-white"></div>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
<p class="mt-4 text-center text-xs text-gray-400">
|
||||
<button type="button" class="hover:text-gray-500 transition-colors" @click="openAgreement('terms')">服务条款</button>
|
||||
<span class="mx-1">·</span>
|
||||
<button type="button" class="hover:text-gray-500 transition-colors" @click="openAgreement('privacy')">隐私政策</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- RAM Login Form -->
|
||||
@@ -452,6 +467,11 @@ async function handleOAuthLogin(provider: string) {
|
||||
<div v-if="loading" class="spinner h-4 w-4 border-white"></div>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
<p class="mt-4 text-center text-xs text-gray-400">
|
||||
<button type="button" class="hover:text-gray-500 transition-colors" @click="openAgreement('terms')">服务条款</button>
|
||||
<span class="mx-1">·</span>
|
||||
<button type="button" class="hover:text-gray-500 transition-colors" @click="openAgreement('privacy')">隐私政策</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<!-- OAuth Login -->
|
||||
@@ -494,11 +514,6 @@ async function handleOAuthLogin(provider: string) {
|
||||
立即创建
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-gray-400">
|
||||
<router-link to="/tenant/agreement/terms" target="_blank" class="hover:text-gray-500 transition-colors">服务条款</router-link>
|
||||
<span class="mx-1">·</span>
|
||||
<router-link to="/tenant/agreement/privacy" target="_blank" class="hover:text-gray-500 transition-colors">隐私政策</router-link>
|
||||
</p>
|
||||
</template>
|
||||
</AuthLayout>
|
||||
|
||||
@@ -507,6 +522,12 @@ async function handleOAuthLogin(provider: string) {
|
||||
:agreements="authStore.pendingAgreements"
|
||||
@accepted="onAgreementsAccepted"
|
||||
/>
|
||||
|
||||
<AgreementViewModal
|
||||
:show="showAgreementModal"
|
||||
:code="agreementModalCode"
|
||||
@close="showAgreementModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -7,7 +7,7 @@ import AuthLayout from '@/components/layout/AuthLayout.vue'
|
||||
import SlideCaptcha from '@/components/common/SlideCaptcha.vue'
|
||||
import Turnstile from '@/components/common/Turnstile.vue'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import AgreementAcceptModal from '@/components/common/AgreementAcceptModal.vue'
|
||||
import AgreementViewModal from '@/components/common/AgreementViewModal.vue'
|
||||
import request from '@/utils/request'
|
||||
import { extractApiError } from '@/utils/request'
|
||||
|
||||
@@ -21,6 +21,7 @@ const codeSending = ref(false)
|
||||
const countdown = ref(0)
|
||||
const emailVerification = ref(true)
|
||||
const captcha = ref<{ captchaKey: string; captchaX: number }>({ captchaKey: '', captchaX: 0 })
|
||||
const captchaRef = ref<InstanceType<typeof SlideCaptcha> | null>(null)
|
||||
const turnstileToken = ref('')
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
@@ -31,26 +32,24 @@ const userForm = reactive({
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
username: '',
|
||||
agreed: false,
|
||||
})
|
||||
|
||||
const orgErrors = reactive<Record<string, string>>({})
|
||||
const userErrors = reactive<Record<string, string>>({})
|
||||
|
||||
// Pending agreements
|
||||
const showAgreements = ref(false)
|
||||
|
||||
function proceedAfterRegister() {
|
||||
const pending = authStore.pendingAgreements
|
||||
if (pending.length > 0) {
|
||||
showAgreements.value = true
|
||||
return
|
||||
}
|
||||
router.push('/tenant/dashboard')
|
||||
// Agreement view modal
|
||||
const showAgreementModal = ref(false)
|
||||
const agreementModalCode = ref('')
|
||||
|
||||
function openAgreement(code: string) {
|
||||
agreementModalCode.value = code
|
||||
showAgreementModal.value = true
|
||||
}
|
||||
|
||||
function onAgreementsAccepted() {
|
||||
authStore.clearPendingAgreements()
|
||||
showAgreements.value = false
|
||||
function proceedAfterRegister() {
|
||||
router.push('/tenant/dashboard')
|
||||
}
|
||||
|
||||
@@ -200,6 +199,7 @@ async function handleRegister() {
|
||||
await authStore.register(payload)
|
||||
proceedAfterRegister()
|
||||
} catch (err: any) {
|
||||
captchaRef.value?.resetCaptcha()
|
||||
const apiErr = extractApiError(err)
|
||||
const msg = apiErr?.message || '注册失败'
|
||||
if (msg.includes('邮箱') || msg.includes('email')) {
|
||||
@@ -389,7 +389,7 @@ async function handleRegister() {
|
||||
|
||||
<!-- Slide Captcha (shown when email verification is disabled) -->
|
||||
<template v-else>
|
||||
<SlideCaptcha v-model="captcha" />
|
||||
<SlideCaptcha ref="captchaRef" v-model="captcha" />
|
||||
<p v-if="userErrors.code" class="input-error-text">{{ userErrors.code }}</p>
|
||||
</template>
|
||||
|
||||
@@ -419,16 +419,25 @@ async function handleRegister() {
|
||||
<p v-if="userErrors.confirmPassword" class="input-error-text">{{ userErrors.confirmPassword }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Agreement Notice -->
|
||||
<p class="text-xs text-gray-400 text-center">
|
||||
注册即表示您同意我们的
|
||||
<router-link to="/tenant/agreement/terms" target="_blank" class="text-primary-600 hover:text-primary-700 underline underline-offset-2">服务条款</router-link>
|
||||
和
|
||||
<router-link to="/tenant/agreement/privacy" target="_blank" class="text-primary-600 hover:text-primary-700 underline underline-offset-2">隐私政策</router-link>
|
||||
</p>
|
||||
<!-- Agreement Checkbox -->
|
||||
<div class="flex items-start gap-2.5">
|
||||
<label class="mt-0.5 cursor-pointer">
|
||||
<input
|
||||
v-model="userForm.agreed"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500/30 transition-colors cursor-pointer"
|
||||
/>
|
||||
</label>
|
||||
<span class="text-xs text-gray-500 leading-relaxed">
|
||||
我已阅读并同意
|
||||
<button type="button" class="text-primary-600 hover:text-primary-700 underline underline-offset-2" @click.prevent="openAgreement('terms')">服务条款</button>
|
||||
和
|
||||
<button type="button" class="text-primary-600 hover:text-primary-700 underline underline-offset-2" @click.prevent="openAgreement('privacy')">隐私政策</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit" :disabled="loading" class="btn btn-primary btn-lg w-full">
|
||||
<button type="submit" :disabled="loading || !userForm.agreed" class="btn btn-primary btn-lg w-full disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<div v-if="loading" class="spinner h-4 w-4 border-white"></div>
|
||||
{{ loading ? '创建中...' : '创建组织' }}
|
||||
</button>
|
||||
@@ -445,9 +454,9 @@ async function handleRegister() {
|
||||
</template>
|
||||
</AuthLayout>
|
||||
|
||||
<AgreementAcceptModal
|
||||
:show="showAgreements"
|
||||
:agreements="authStore.pendingAgreements"
|
||||
@accepted="onAgreementsAccepted"
|
||||
<AgreementViewModal
|
||||
:show="showAgreementModal"
|
||||
:code="agreementModalCode"
|
||||
@close="showAgreementModal = false"
|
||||
/>
|
||||
</template>
|
||||
Reference in new issue
Block a user