diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index 82a65b9..13025ee 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -4,14 +4,9 @@ import { BadRequestException, Logger, InternalServerErrorException, - } from '@nestjs/common'; import { StaffService } from '../models/staff/staff.service'; -import { - db, - AuthSchema, - JwtPayload, -} from '@nice/common'; +import { db, AuthSchema, JwtPayload } from '@nice/common'; import * as argon2 from 'argon2'; import { JwtService } from '@nestjs/jwt'; import { redis } from '@server/utils/redis/redis.service'; @@ -24,14 +19,12 @@ import { TusService } from '@server/upload/tus.service'; import { extractFileIdFromNginxUrl } from '@server/upload/utils'; @Injectable() export class AuthService { - private logger = new Logger(AuthService.name) + private logger = new Logger(AuthService.name); constructor( private readonly staffService: StaffService, private readonly jwtService: JwtService, - private readonly sessionService: SessionService - ) { - - } + private readonly sessionService: SessionService, + ) {} async validateFileRequest(params: FileRequest): Promise { try { // 基础参数验证 @@ -39,27 +32,32 @@ export class AuthService { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFileIdFromNginxUrl(params.originalUri); - console.log(params.originalUri, fileId) + console.log(params.originalUri, fileId); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 if (!resource) { - return { isValid: false, error: FileValidationErrorType.RESOURCE_NOT_FOUND }; + return { + isValid: false, + error: FileValidationErrorType.RESOURCE_NOT_FOUND, + }; } // 处理公开资源 if (resource.isPublic) { - return { isValid: true, - resourceType: resource.type || 'unknown' + resourceType: resource.type || 'unknown', }; } // 处理私有资源 const token = extractTokenFromAuthorization(params.authorization); if (!token) { - return { isValid: false, error: FileValidationErrorType.AUTHORIZATION_REQUIRED }; + return { + isValid: false, + error: FileValidationErrorType.AUTHORIZATION_REQUIRED, + }; } - const payload: JwtPayload = await this.jwtService.verify(token) + const payload: JwtPayload = await this.jwtService.verify(token); if (!payload.sub) { return { isValid: false, error: FileValidationErrorType.INVALID_TOKEN }; } @@ -67,9 +65,8 @@ export class AuthService { return { isValid: true, userId: payload.sub, - resourceType: resource.type || 'unknown' + resourceType: resource.type || 'unknown', }; - } catch (error) { this.logger.error('File validation error:', error); return { isValid: false, error: FileValidationErrorType.UNKNOWN_ERROR }; @@ -93,7 +90,9 @@ export class AuthService { return { accessToken, refreshToken }; } - async signIn(data: z.infer): Promise { + async signIn( + data: z.infer, + ): Promise { const { username, password, phoneNumber } = data; let staff = await db.staff.findFirst({ @@ -113,7 +112,8 @@ export class AuthService { if (!staff.enabled) { throw new UnauthorizedException('帐号已禁用'); } - const isPasswordMatch = phoneNumber || await argon2.verify(staff.password, password); + const isPasswordMatch = + phoneNumber || (await argon2.verify(staff.password, password)); if (!isPasswordMatch) { throw new UnauthorizedException('帐号或密码错误'); } @@ -138,12 +138,20 @@ export class AuthService { } } async signUp(data: z.infer) { - const { username, phoneNumber, officerId } = data; - + const { + username, + phoneNumber, + password, + officerId, + showname, + photoUrl, + deptId, + ...others + } = data; const existingUser = await db.staff.findFirst({ where: { OR: [{ username }, { officerId }, { phoneNumber }], - deletedAt: null + deletedAt: null, }, }); @@ -153,9 +161,22 @@ export class AuthService { return this.staffService.create({ data: { - ...data, - domainId: data.deptId, - } + username, + phoneNumber, + password, + officerId, + showname, + department: { + connect: { id: deptId }, + }, + domain: { + connect: { id: deptId }, + }, + // domainId: data.deptId, + meta: { + photoUrl, + }, + }, }); } async refreshToken(data: z.infer) { @@ -168,12 +189,17 @@ export class AuthService { throw new UnauthorizedException('用户会话已过期'); } - const session = await this.sessionService.getSession(payload.sub, sessionId); + const session = await this.sessionService.getSession( + payload.sub, + sessionId, + ); if (!session || session.refresh_token !== refreshToken) { throw new UnauthorizedException('用户会话已过期'); } - const user = await db.staff.findUnique({ where: { id: payload.sub, deletedAt: null } }); + const user = await db.staff.findUnique({ + where: { id: payload.sub, deletedAt: null }, + }); if (!user) { throw new UnauthorizedException('用户不存在'); } @@ -186,14 +212,17 @@ export class AuthService { const updatedSession = { ...session, access_token: accessToken, - access_token_expires_at: Date.now() + tokenConfig.accessToken.expirationMs, + access_token_expires_at: + Date.now() + tokenConfig.accessToken.expirationMs, }; await this.sessionService.saveSession( payload.sub, updatedSession, tokenConfig.accessToken.expirationTTL, ); - await redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub)); + await redis.del( + UserProfileService.instance.getProfileCacheKey(payload.sub), + ); return { access_token: accessToken, access_token_expires_at: updatedSession.access_token_expires_at, @@ -212,7 +241,7 @@ export class AuthService { where: { id: user?.id }, data: { password: newPassword, - } + }, }); return { message: '密码已修改' }; @@ -232,5 +261,4 @@ export class AuthService { return { message: '注销成功' }; } - -} \ No newline at end of file +} diff --git a/apps/server/src/auth/config.ts b/apps/server/src/auth/config.ts index 0edaf5d..64d6776 100644 --- a/apps/server/src/auth/config.ts +++ b/apps/server/src/auth/config.ts @@ -1,9 +1,9 @@ export const tokenConfig = { - accessToken: { - expirationMs: 7 * 24 * 3600000, // 7 days - expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds - }, - refreshToken: { - expirationMs: 30 * 24 * 3600000, // 30 days - }, -}; \ No newline at end of file + accessToken: { + expirationMs: 7 * 24 * 3600000, // 7 days + expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds + }, + refreshToken: { + expirationMs: 30 * 24 * 3600000, // 30 days + }, +}; diff --git a/apps/server/src/models/staff/staff.row.service.ts b/apps/server/src/models/staff/staff.row.service.ts index c2e92c2..7347725 100644 --- a/apps/server/src/models/staff/staff.row.service.ts +++ b/apps/server/src/models/staff/staff.row.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { - db, - ObjectType, - StaffMethodSchema, - UserProfile, - RolePerms, - ResPerm, - Staff, - RowModelRequest, + db, + ObjectType, + StaffMethodSchema, + UserProfile, + RolePerms, + ResPerm, + Staff, + RowModelRequest, } from '@nice/common'; import { DepartmentService } from '../department/department.service'; import { RowCacheService } from '../base/row-cache.service'; @@ -15,121 +15,116 @@ import { z } from 'zod'; import { isFieldCondition } from '../base/sql-builder'; @Injectable() export class StaffRowService extends RowCacheService { - constructor( - private readonly departmentService: DepartmentService, - ) { - super(ObjectType.STAFF, false); + constructor(private readonly departmentService: DepartmentService) { + super(ObjectType.STAFF, false); + } + createUnGroupingRowSelect(request?: RowModelRequest): string[] { + const result = super + .createUnGroupingRowSelect(request) + .concat([ + `${this.tableName}.id AS id`, + `${this.tableName}.username AS username`, + `${this.tableName}.showname AS showname`, + `${this.tableName}.avatar AS avatar`, + `${this.tableName}.officer_id AS officer_id`, + `${this.tableName}.phone_number AS phone_number`, + `${this.tableName}.order AS order`, + `${this.tableName}.enabled AS enabled`, + 'dept.name AS dept_name', + 'domain.name AS domain_name', + ]); + return result; + } + createJoinSql(request?: RowModelRequest): string[] { + return [ + `LEFT JOIN department dept ON ${this.tableName}.dept_id = dept.id`, + `LEFT JOIN department domain ON ${this.tableName}.domain_id = domain.id`, + ]; + } + protected createGetRowsFilters( + request: z.infer, + staff: UserProfile, + ) { + const condition = super.createGetRowsFilters(request); + const { domainId, includeDeleted = false } = request; + if (isFieldCondition(condition)) { + return; } - createUnGroupingRowSelect(request?: RowModelRequest): string[] { - const result = super.createUnGroupingRowSelect(request).concat([ - `${this.tableName}.id AS id`, - `${this.tableName}.username AS username`, - `${this.tableName}.showname AS showname`, - `${this.tableName}.avatar AS avatar`, - `${this.tableName}.officer_id AS officer_id`, - `${this.tableName}.phone_number AS phone_number`, - `${this.tableName}.order AS order`, - `${this.tableName}.enabled AS enabled`, - 'dept.name AS dept_name', - 'domain.name AS domain_name', - ]); - return result + if (domainId) { + condition.AND.push({ + field: `${this.tableName}.domain_id`, + value: domainId, + op: 'equals', + }); + } else { + condition.AND.push({ + field: `${this.tableName}.domain_id`, + op: 'blank', + }); } - createJoinSql(request?: RowModelRequest): string[] { - return [ - `LEFT JOIN department dept ON ${this.tableName}.dept_id = dept.id`, - `LEFT JOIN department domain ON ${this.tableName}.domain_id = domain.id`, - ]; + if (!includeDeleted) { + condition.AND.push({ + field: `${this.tableName}.deleted_at`, + type: 'date', + op: 'blank', + }); } - protected createGetRowsFilters( - request: z.infer, - staff: UserProfile, - ) { - const condition = super.createGetRowsFilters(request); - const { domainId, includeDeleted = false } = request; - if (isFieldCondition(condition)) { - return; - } - if (domainId) { - condition.AND.push({ - field: `${this.tableName}.domain_id`, - value: domainId, - op: 'equals', - }); - } else { - condition.AND.push({ - field: `${this.tableName}.domain_id`, - op: 'blank', - }); - } - if (!includeDeleted) { - condition.AND.push({ - field: `${this.tableName}.deleted_at`, - type: 'date', - op: 'blank', - }); - } - condition.OR = []; - if (!staff.permissions.includes(RolePerms.MANAGE_ANY_STAFF)) { - if (staff.permissions.includes(RolePerms.MANAGE_DOM_STAFF)) { - condition.OR.push({ - field: 'dept.id', - value: staff.domainId, - op: 'equals', - }); - } - } - - return condition; - } - - async getPermissionContext(id: string, staff: UserProfile) { - const data = await db.staff.findUnique({ - where: { id }, - select: { - deptId: true, - domainId: true, - }, + condition.OR = []; + if (!staff.permissions.includes(RolePerms.MANAGE_ANY_STAFF)) { + if (staff.permissions.includes(RolePerms.MANAGE_DOM_STAFF)) { + condition.OR.push({ + field: 'dept.id', + value: staff.domainId, + op: 'equals', }); - const deptId = data?.deptId; - const isFromSameDept = staff.deptIds?.includes(deptId); - const domainChildDeptIds = await this.departmentService.getDescendantIds( - staff.domainId, true - ); - const belongsToDomain = domainChildDeptIds.includes( - deptId, - ); - return { isFromSameDept, belongsToDomain }; - } - protected async setResPermissions( - data: Staff, - staff: UserProfile, - ) { - const permissions: ResPerm = {}; - const { isFromSameDept, belongsToDomain } = await this.getPermissionContext( - data.id, - staff, - ); - const setManagePermissions = (permissions: ResPerm) => { - Object.assign(permissions, { - read: true, - delete: true, - edit: true, - }); - }; - staff.permissions.forEach((permission) => { - switch (permission) { - case RolePerms.MANAGE_ANY_STAFF: - setManagePermissions(permissions); - break; - case RolePerms.MANAGE_DOM_STAFF: - if (belongsToDomain) { - setManagePermissions(permissions); - } - break; - } - }); - return { ...data, perm: permissions }; + } } + return condition; + } + + async getPermissionContext(id: string, staff: UserProfile) { + const data = await db.staff.findUnique({ + where: { id }, + select: { + deptId: true, + domainId: true, + }, + }); + const deptId = data?.deptId; + const isFromSameDept = staff.deptIds?.includes(deptId); + const domainChildDeptIds = await this.departmentService.getDescendantIds( + staff.domainId, + true, + ); + const belongsToDomain = domainChildDeptIds.includes(deptId); + return { isFromSameDept, belongsToDomain }; + } + protected async setResPermissions(data: Staff, staff: UserProfile) { + const permissions: ResPerm = {}; + const { isFromSameDept, belongsToDomain } = await this.getPermissionContext( + data.id, + staff, + ); + const setManagePermissions = (permissions: ResPerm) => { + Object.assign(permissions, { + read: true, + delete: true, + edit: true, + }); + }; + staff.permissions.forEach((permission) => { + switch (permission) { + case RolePerms.MANAGE_ANY_STAFF: + setManagePermissions(permissions); + break; + case RolePerms.MANAGE_DOM_STAFF: + if (belongsToDomain) { + setManagePermissions(permissions); + } + break; + } + }); + return { ...data, perm: permissions }; + } } diff --git a/apps/web/src/app/auth/register.tsx b/apps/web/src/app/auth/register.tsx index 57c4b0b..b1c277e 100644 --- a/apps/web/src/app/auth/register.tsx +++ b/apps/web/src/app/auth/register.tsx @@ -1,131 +1,138 @@ +import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; import DepartmentSelect from "@web/src/components/models/department/department-select"; import { Form, Input, Button, Select } from "antd"; import { motion } from "framer-motion"; export interface RegisterFormData { - deptId: string; - username: string; - showname: string; - officerId: string; - password: string; - repeatPass: string; + deptId: string; + username: string; + showname: string; + officerId: string; + password: string; + repeatPass: string; } interface RegisterFormProps { - onSubmit: (data: RegisterFormData) => void; - isLoading: boolean; + onSubmit: (data: RegisterFormData) => void; + isLoading: boolean; } export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => { - const [form] = Form.useForm(); + const [form] = Form.useForm(); - return ( - + return ( + +
+
+
+ + + +
+
+ + + + + + + + + +
+
- - - + + + - + + + -
- - - + ({ + validator(_, value) { + if ( + !value || + getFieldValue("password") === value + ) { + return Promise.resolve(); + } + return Promise.reject( + new Error("两次输入的密码不一致") + ); + }, + }), + ]}> + + - - - -
- - - - - - - - - - ({ - validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } - return Promise.reject(new Error('两次输入的密码不一致')); - }, - }), - ]} - > - - - - - - -
-
- ); + + + + +
+ ); }; diff --git a/apps/web/src/components/layout/element/usermenu.tsx b/apps/web/src/components/layout/element/usermenu.tsx index 2fab536..037facd 100644 --- a/apps/web/src/components/layout/element/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu.tsx @@ -1,7 +1,7 @@ import { useClickOutside } from "@web/src/hooks/useClickOutside"; import { useAuth } from "@web/src/providers/auth-provider"; import { motion, AnimatePresence } from "framer-motion"; -import { useState, useRef, useCallback, useMemo } from "react"; +import React,{ useState, useRef, useCallback, useMemo, createContext } from "react"; import { Avatar } from "../../common/element/Avatar"; import { UserOutlined, @@ -9,9 +9,10 @@ import { QuestionCircleOutlined, LogoutOutlined, } from "@ant-design/icons"; -import { Spin } from "antd"; +import { FormInstance, Spin } from "antd"; import { useNavigate } from "react-router-dom"; import { MenuItemType } from "./types"; +import { RolePerms } from "@nice/common"; const menuVariants = { hidden: { opacity: 0, scale: 0.95, y: -10 }, visible: { @@ -34,42 +35,69 @@ const menuVariants = { }, }; +export const UserEditorContext = createContext<{ + domainId: string; + modalOpen: boolean; + setDomainId: React.Dispatch>; + setModalOpen: React.Dispatch>; + editId: string; + setEditId: React.Dispatch>; + form: FormInstance; + formLoading: boolean; + setFormLoading: React.Dispatch>; + canManageAnyStaff: boolean; +}>({ + domainId: undefined, + modalOpen: false, + setDomainId: undefined, + setModalOpen: undefined, + editId: undefined, + setEditId: undefined, + form: undefined, + formLoading: undefined, + setFormLoading: undefined, + canManageAnyStaff: false, +}); + export function UserMenu() { const [showMenu, setShowMenu] = useState(false); const menuRef = useRef(null); - const { user, logout, isLoading } = useAuth(); + const { user, logout, isLoading, hasSomePermissions } = useAuth(); const navigate = useNavigate(); useClickOutside(menuRef, () => setShowMenu(false)); - + const [modalOpen, setModalOpen] = useState(false); const toggleMenu = useCallback(() => { setShowMenu((prev) => !prev); }, []); - + const canManageAnyStaff = useMemo(() => { + return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF); + }, [user]); const menuItems: MenuItemType[] = useMemo( - () => [ - { - icon: , - label: "个人信息", - action: () => {}, - }, - { - icon: , - label: "设置", - action: () => { - navigate("/admin/staff"); + () => + [ + { + icon: , + label: "个人信息", + action: () => {}, }, - }, - // { - // icon: , - // label: '帮助', - // action: () => { }, - // }, - { - icon: , - label: "注销", - action: () => logout(), - }, - ], + canManageAnyStaff && { + icon: , + label: "设置", + action: () => { + navigate("/admin/staff"); + }, + }, + // { + // icon: , + // label: '帮助', + // action: () => { }, + // }, + { + icon: , + label: "注销", + action: () => logout(), + }, + ].filter(Boolean), [logout] ); @@ -87,99 +115,100 @@ export function UserMenu() { } return ( -
- - {/* Avatar 容器,相对定位 */} -
- + + {/* Avatar 容器,相对定位 */} +
+ - {/* 小绿点 */} -
+ aria-hidden="true" + /> +
- {/* 用户信息,显示在 Avatar 右侧 */} -
- - {user?.showname || user?.username} - - - {user?.department?.name} - -
-
+ {/* 用户信息,显示在 Avatar 右侧 */} +
+ + {user?.showname || user?.username} + + + {user?.department?.name} + +
+ - - {showMenu && ( - - {/* User Profile Section */} -
-
- -
- - {user?.showname || user?.username} - - - - 在线 - +
+ +
+ + {user?.showname || user?.username} + + + + 在线 + +
-
- {/* Menu Items */} -
- {menuItems.map((item, index) => ( - - ))} -
- - )} - -
+ {item.icon} + + {item.label} + + ))} +
+ + )} + + + ); } diff --git a/apps/web/src/components/models/staff/staff-form.tsx b/apps/web/src/components/models/staff/staff-form.tsx index 070a9f5..340b141 100644 --- a/apps/web/src/components/models/staff/staff-form.tsx +++ b/apps/web/src/components/models/staff/staff-form.tsx @@ -124,11 +124,7 @@ export default function StaffForm() { onFinish={handleFinish}>
- +