限制初始密码强制范围并修复后台图片加载
This commit is contained in:
@@ -133,6 +133,13 @@ func (r *Repository) FindByID(ctx context.Context, id uint64) (*AdminDTO, error)
|
|||||||
return &dto, nil
|
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 {
|
func (r *Repository) UpdateSupportStatus(ctx context.Context, adminID uint64, status string) error {
|
||||||
if r.db == nil {
|
if r.db == nil {
|
||||||
return ErrDependencyUnavailable
|
return ErrDependencyUnavailable
|
||||||
@@ -168,6 +175,7 @@ func (r *Repository) loadRolesAndPerms(ctx context.Context, dto *AdminDTO) {
|
|||||||
Where("aur.admin_user_id = ?", dto.ID).
|
Where("aur.admin_user_id = ?", dto.ID).
|
||||||
Find(&roles)
|
Find(&roles)
|
||||||
dto.Roles = roles
|
dto.Roles = roles
|
||||||
|
dto.PasswordMustChange = dto.PasswordMustChange && rolesRequireInitialPasswordChange(roles)
|
||||||
|
|
||||||
// 加载权限
|
// 加载权限
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
@@ -191,6 +199,15 @@ func (r *Repository) loadRolesAndPerms(ctx context.Context, dto *AdminDTO) {
|
|||||||
cachePermissions(ctx, r, dto.ID, permCodes)
|
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) {
|
func cachePermissions(ctx context.Context, r *Repository, adminID uint64, permCodes []string) {
|
||||||
if r.redis == nil || len(permCodes) == 0 {
|
if r.redis == nil || len(permCodes) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -315,7 +315,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
var validateAdminToken middleware.AdminTokenValidatorFunc
|
var validateAdminToken middleware.AdminTokenValidatorFunc
|
||||||
if adminAuthRepo != nil {
|
if adminAuthRepo != nil {
|
||||||
validateAdminToken = func(ctx context.Context, adminID uint64, tokenVersion int64) (middleware.AdminTokenContext, error) {
|
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 {
|
if err != nil {
|
||||||
return middleware.AdminTokenContext{}, err
|
return middleware.AdminTokenContext{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
|
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
|
||||||
|
import { isAdminPath } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
source: string
|
source: string
|
||||||
@@ -9,6 +10,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const objectURL = ref('')
|
const objectURL = ref('')
|
||||||
const failed = ref(false)
|
const failed = ref(false)
|
||||||
|
const effectiveAdmin = computed(() => props.admin || isAdminPath(window.location.pathname))
|
||||||
|
|
||||||
function extractObjectKey(value: string) {
|
function extractObjectKey(value: string) {
|
||||||
try {
|
try {
|
||||||
@@ -35,7 +37,9 @@ async function loadImage() {
|
|||||||
try {
|
try {
|
||||||
const key = extractObjectKey(props.source)
|
const key = extractObjectKey(props.source)
|
||||||
const blob =
|
const blob =
|
||||||
props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(props.source)
|
effectiveAdmin.value && key
|
||||||
|
? await fetchAdminFileBlob(key)
|
||||||
|
: await fetchFileBlobByURL(props.source)
|
||||||
objectURL.value = URL.createObjectURL(blob)
|
objectURL.value = URL.createObjectURL(blob)
|
||||||
} catch {
|
} catch {
|
||||||
failed.value = true
|
failed.value = true
|
||||||
@@ -47,7 +51,7 @@ function openImage() {
|
|||||||
window.open(objectURL.value, '_blank')
|
window.open(objectURL.value, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => [props.source, props.admin] as const, loadImage, { immediate: true })
|
watch(() => [props.source, effectiveAdmin.value] as const, loadImage, { immediate: true })
|
||||||
|
|
||||||
onBeforeUnmount(revokeCurrentURL)
|
onBeforeUnmount(revokeCurrentURL)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ const passwordForm = reactive({
|
|||||||
new_password: '',
|
new_password: '',
|
||||||
confirm_password: '',
|
confirm_password: '',
|
||||||
})
|
})
|
||||||
|
const shouldForcePasswordChange = computed(
|
||||||
|
() => adminSession.passwordMustChange && adminSession.isSuperAdmin
|
||||||
|
)
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
label: string
|
label: string
|
||||||
@@ -394,7 +397,7 @@ async function handleForcedPasswordChange() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="adminSession.passwordMustChange"
|
:model-value="shouldForcePasswordChange"
|
||||||
title="修改初始密码"
|
title="修改初始密码"
|
||||||
width="420px"
|
width="420px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
|
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
|
||||||
|
import { isAdminPath } from '@/shared/utils/adminPath'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -33,6 +34,7 @@ const failed = ref(false)
|
|||||||
const createdURLs: string[] = []
|
const createdURLs: string[] = []
|
||||||
|
|
||||||
const usePreview = computed(() => !!props.previewSrcList?.length)
|
const usePreview = computed(() => !!props.previewSrcList?.length)
|
||||||
|
const effectiveAdmin = computed(() => props.admin || isAdminPath(window.location.pathname))
|
||||||
const fallbackText = computed(() => (failed.value ? '图片加载失败' : '图片加载中'))
|
const fallbackText = computed(() => (failed.value ? '图片加载失败' : '图片加载中'))
|
||||||
const imageStyleValue = computed(() => {
|
const imageStyleValue = computed(() => {
|
||||||
if (!props.fit) return props.imageStyle
|
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')
|
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<string> {
|
async function resolveURL(url: string): Promise<string> {
|
||||||
if (!url || !shouldFetchWithAuth(url)) return url
|
if (!url || !shouldFetchWithAuth(url)) return url
|
||||||
const key = extractObjectKey(url)
|
const key = extractObjectKey(url)
|
||||||
const blob =
|
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)
|
const blobURL = URL.createObjectURL(blob)
|
||||||
createdURLs.push(blobURL)
|
createdURLs.push(blobURL)
|
||||||
return blobURL
|
return blobURL
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
createdURLs.forEach((u) => URL.revokeObjectURL(u))
|
createdURLs.forEach(u => URL.revokeObjectURL(u))
|
||||||
createdURLs.length = 0
|
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,
|
immediate: true,
|
||||||
deep: true,
|
deep: true,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user