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,
})