From 275663c4eacfc644d2be85785f9712389992c0ba Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 18 Jun 2026 15:44:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6=E5=88=9D=E5=A7=8B=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=BC=BA=E5=88=B6=E8=8C=83=E5=9B=B4=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=90=8E=E5=8F=B0=E5=9B=BE=E7=89=87=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/modules/adminauth/repository.go | 17 +++++++++ .../modules/adminauth/repository_test.go | 35 +++++++++++++++++++ backend/internal/router/router.go | 2 +- .../src/components/ChatAttachmentImage.vue | 10 ++++-- frontend/src/layouts/AdminLayout.vue | 5 ++- .../shared/components/business/AuthImage.vue | 12 +++++-- 6 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 backend/internal/modules/adminauth/repository_test.go diff --git a/backend/internal/modules/adminauth/repository.go b/backend/internal/modules/adminauth/repository.go index 63ce7d4..b282df3 100644 --- a/backend/internal/modules/adminauth/repository.go +++ b/backend/internal/modules/adminauth/repository.go @@ -133,6 +133,13 @@ func (r *Repository) FindByID(ctx context.Context, id uint64) (*AdminDTO, error) return &dto, nil } +func (r *Repository) FindActiveForPasswordGate(ctx context.Context, id uint64, tokenVersion int64) (*AdminDTO, error) { + if _, err := r.FindActiveForToken(ctx, id, tokenVersion); err != nil { + return nil, err + } + return r.FindByID(ctx, id) +} + func (r *Repository) UpdateSupportStatus(ctx context.Context, adminID uint64, status string) error { if r.db == nil { return ErrDependencyUnavailable @@ -168,6 +175,7 @@ func (r *Repository) loadRolesAndPerms(ctx context.Context, dto *AdminDTO) { Where("aur.admin_user_id = ?", dto.ID). Find(&roles) dto.Roles = roles + dto.PasswordMustChange = dto.PasswordMustChange && rolesRequireInitialPasswordChange(roles) // 加载权限 for _, role := range roles { @@ -191,6 +199,15 @@ func (r *Repository) loadRolesAndPerms(ctx context.Context, dto *AdminDTO) { cachePermissions(ctx, r, dto.ID, permCodes) } +func rolesRequireInitialPasswordChange(roles []RoleDTO) bool { + for _, role := range roles { + if role.Code == "super_admin" { + return true + } + } + return false +} + func cachePermissions(ctx context.Context, r *Repository, adminID uint64, permCodes []string) { if r.redis == nil || len(permCodes) == 0 { return diff --git a/backend/internal/modules/adminauth/repository_test.go b/backend/internal/modules/adminauth/repository_test.go new file mode 100644 index 0000000..b1345b5 --- /dev/null +++ b/backend/internal/modules/adminauth/repository_test.go @@ -0,0 +1,35 @@ +package adminauth + +import "testing" + +func TestRolesRequireInitialPasswordChange(t *testing.T) { + tests := []struct { + name string + roles []RoleDTO + want bool + }{ + { + name: "超级管理员需要修改初始密码", + roles: []RoleDTO{{Code: "super_admin", Name: "超级管理员"}}, + want: true, + }, + { + name: "客服不强制修改初始密码", + roles: []RoleDTO{{Code: "cs", Name: "客服"}}, + want: false, + }, + { + name: "运营不强制修改初始密码", + roles: []RoleDTO{{Code: "ops", Name: "运营"}}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := rolesRequireInitialPasswordChange(tt.roles); got != tt.want { + t.Fatalf("rolesRequireInitialPasswordChange() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index e731bea..d199fa7 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -315,7 +315,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { var validateAdminToken middleware.AdminTokenValidatorFunc if adminAuthRepo != nil { validateAdminToken = func(ctx context.Context, adminID uint64, tokenVersion int64) (middleware.AdminTokenContext, error) { - admin, err := adminAuthRepo.FindActiveForToken(ctx, adminID, tokenVersion) + admin, err := adminAuthRepo.FindActiveForPasswordGate(ctx, adminID, tokenVersion) if err != nil { return middleware.AdminTokenContext{}, err } diff --git a/frontend/src/components/ChatAttachmentImage.vue b/frontend/src/components/ChatAttachmentImage.vue index 63de32b..126f531 100644 --- a/frontend/src/components/ChatAttachmentImage.vue +++ b/frontend/src/components/ChatAttachmentImage.vue @@ -1,6 +1,7 @@ diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index cb7384a..ccbf14f 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -47,6 +47,9 @@ const passwordForm = reactive({ new_password: '', confirm_password: '', }) +const shouldForcePasswordChange = computed( + () => adminSession.passwordMustChange && adminSession.isSuperAdmin +) interface NavItem { label: string @@ -394,7 +397,7 @@ async function handleForcedPasswordChange() { import { computed, onBeforeUnmount, ref, watch } from 'vue' import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files' +import { isAdminPath } from '@/shared/utils/adminPath' const props = withDefaults( defineProps<{ @@ -33,6 +34,7 @@ const failed = ref(false) const createdURLs: string[] = [] const usePreview = computed(() => !!props.previewSrcList?.length) +const effectiveAdmin = computed(() => props.admin || isAdminPath(window.location.pathname)) const fallbackText = computed(() => (failed.value ? '图片加载失败' : '图片加载中')) const imageStyleValue = computed(() => { if (!props.fit) return props.imageStyle @@ -52,18 +54,22 @@ function shouldFetchWithAuth(value: string) { return value.includes('/api/files/object') || value.includes('/api/admin/files/object') } +function shouldFetchAsAdmin(value: string) { + return effectiveAdmin.value || value.includes('/api/admin/files/object') +} + async function resolveURL(url: string): Promise { if (!url || !shouldFetchWithAuth(url)) return url const key = extractObjectKey(url) const blob = - props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(url) + shouldFetchAsAdmin(url) && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(url) const blobURL = URL.createObjectURL(blob) createdURLs.push(blobURL) return blobURL } function cleanup() { - createdURLs.forEach((u) => URL.revokeObjectURL(u)) + createdURLs.forEach(u => URL.revokeObjectURL(u)) createdURLs.length = 0 } @@ -88,7 +94,7 @@ async function loadImage() { } } -watch(() => [props.source, props.admin, props.previewSrcList] as const, loadImage, { +watch(() => [props.source, effectiveAdmin.value, props.previewSrcList] as const, loadImage, { immediate: true, deep: true, })