09091848
This commit is contained in:
parent
baa376d436
commit
b8c73030d8
|
@ -230,3 +230,6 @@ $RECYCLE.BIN/
|
||||||
*~
|
*~
|
||||||
|
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
.env
|
||||||
|
packages/common/prisma/migrations
|
||||||
|
volumes
|
|
@ -26,6 +26,7 @@
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/platform-socket.io": "^10.4.1",
|
||||||
"@nestjs/schedule": "^4.1.0",
|
"@nestjs/schedule": "^4.1.0",
|
||||||
"@nestjs/websockets": "^10.3.10",
|
"@nestjs/websockets": "^10.3.10",
|
||||||
"@nicestack/common": "workspace:^",
|
"@nicestack/common": "workspace:^",
|
||||||
|
|
|
@ -8,9 +8,10 @@ import { QueueModule } from './queue/queue.module';
|
||||||
import { TransformModule } from './transform/transform.module';
|
import { TransformModule } from './transform/transform.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule],
|
imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule],
|
||||||
providers: [RedisService, SocketGateway],
|
providers: [RedisService, SocketGateway, ConfigService],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthSchema, JwtPayload } from '@nicestack/common';
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { AuthGuard } from './auth.guard';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) { }
|
||||||
|
@Get("user-profile")
|
||||||
|
async getUserProfile(@Req() request: Request) {
|
||||||
|
const user: JwtPayload = (request as any).user
|
||||||
|
return this.authService.getUserProfile(user)
|
||||||
|
}
|
||||||
|
@Post('login')
|
||||||
|
async login(@Body() body: z.infer<typeof AuthSchema.signInRequset>) {
|
||||||
|
return this.authService.signIn(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('signup')
|
||||||
|
async signup(@Body() body: z.infer<typeof AuthSchema.signUpRequest>) {
|
||||||
|
return this.authService.signUp(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard) // Protecting the refreshToken endpoint with AuthGuard
|
||||||
|
@Post('refresh-token')
|
||||||
|
async refreshToken(@Body() body: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
||||||
|
return this.authService.refreshToken(body);
|
||||||
|
}
|
||||||
|
@UseGuards(AuthGuard) // Protecting the logout endpoint with AuthGuard
|
||||||
|
@Post('logout')
|
||||||
|
async logout(@Body() body: z.infer<typeof AuthSchema.logoutRequest>) {
|
||||||
|
return this.authService.logout(body);
|
||||||
|
}
|
||||||
|
@UseGuards(AuthGuard) // Protecting the changePassword endpoint with AuthGuard
|
||||||
|
@Post('change-password')
|
||||||
|
async changePassword(@Body() body: z.infer<typeof AuthSchema.changePassword>) {
|
||||||
|
return this.authService.changePassword(body);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { env } from '@server/env';
|
import { env } from '@server/env';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { JwtPayload } from '@nicestack/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
|
@ -18,7 +19,7 @@ export class AuthGuard implements CanActivate {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwtService.verifyAsync(
|
const payload: JwtPayload = await this.jwtService.verifyAsync(
|
||||||
token,
|
token,
|
||||||
{
|
{
|
||||||
secret: env.JWT_SECRET
|
secret: env.JWT_SECRET
|
||||||
|
|
|
@ -2,13 +2,18 @@ import { Module } from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { env } from '@server/env';
|
import { env } from '@server/env';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { StaffService } from '@server/models/staff/staff.service';
|
||||||
|
import { RoleMapService } from '@server/rbac/rolemap.service';
|
||||||
|
import { DepartmentService } from '@server/models/department/department.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [AuthService],
|
providers: [AuthService, StaffService, RoleMapService,DepartmentService],
|
||||||
imports: [JwtModule.register({
|
imports: [JwtModule.register({
|
||||||
global: true,
|
global: true,
|
||||||
secret: env.JWT_SECRET,
|
secret: env.JWT_SECRET,
|
||||||
signOptions: { expiresIn: '60s' },
|
signOptions: { expiresIn: '60s' },
|
||||||
}),]
|
}),],
|
||||||
|
controllers: [AuthController]
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule { }
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { AuthSchema } from '@nicestack/common';
|
|
||||||
@Injectable()
|
|
||||||
export class AuthRouter {
|
|
||||||
constructor(private readonly trpc: TrpcService, private readonly authService: AuthService) { }
|
|
||||||
router = this.trpc.router({
|
|
||||||
login: this.trpc.procedure.input(AuthSchema.signInRequset).mutation(({ input }) => {
|
|
||||||
return this.authService.signIn(input);
|
|
||||||
}),
|
|
||||||
signup: this.trpc.procedure.input(AuthSchema.signUpRequest).mutation(({ input }) => {
|
|
||||||
return this.authService.signUp(input);
|
|
||||||
}),
|
|
||||||
refreshToken: this.trpc.procedure.input(AuthSchema.refreshTokenRequest).mutation(({ input }) => {
|
|
||||||
return this.authService.refreshToken(input);
|
|
||||||
}),
|
|
||||||
logout: this.trpc.protectProcedure.input(AuthSchema.logoutRequest).mutation(({ input }) => {
|
|
||||||
return this.authService.logout(input);
|
|
||||||
}),
|
|
||||||
changePassword: this.trpc.protectProcedure.input(AuthSchema.changePassword).mutation(({ input }) => {
|
|
||||||
return this.authService.changePassword(input);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -8,29 +8,40 @@ import { JwtService } from '@nestjs/jwt';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { AuthSchema, db, z } from '@nicestack/common';
|
import { AuthSchema, db, z } from '@nicestack/common';
|
||||||
import { StaffService } from '@server/models/staff/staff.service';
|
import { StaffService } from '@server/models/staff/staff.service';
|
||||||
|
import { JwtPayload } from '@nicestack/common';
|
||||||
|
import { RoleMapService } from '@server/rbac/rolemap.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly staffService: StaffService,
|
private readonly staffService: StaffService,
|
||||||
|
private readonly roleMapService: RoleMapService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async signIn(data: z.infer<typeof AuthSchema.signInRequset>) {
|
async signIn(data: z.infer<typeof AuthSchema.signInRequset>) {
|
||||||
const { username, password } = data;
|
const { username, password } = data;
|
||||||
const staff = await db.staff.findUnique({ where: { username } });
|
const staff = await db.staff.findUnique({ where: { username } });
|
||||||
if (!staff) {
|
if (!staff) {
|
||||||
throw new UnauthorizedException('Invalid username or password');
|
throw new UnauthorizedException('Invalid username or password');
|
||||||
}
|
}
|
||||||
const isPasswordMatch = await bcrypt.compare(password, staff.password);
|
|
||||||
|
|
||||||
|
const isPasswordMatch = await bcrypt.compare(password, staff.password);
|
||||||
if (!isPasswordMatch) {
|
if (!isPasswordMatch) {
|
||||||
throw new UnauthorizedException('Invalid username or password');
|
throw new UnauthorizedException('Invalid username or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = { sub: staff.id, username: staff.username };
|
const payload: JwtPayload = { sub: staff.id, username: staff.username };
|
||||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
|
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
|
||||||
const refreshToken = await this.generateRefreshToken(staff.id);
|
const refreshToken = await this.generateRefreshToken(staff.id);
|
||||||
|
|
||||||
|
// Calculate expiration dates
|
||||||
|
const accessTokenExpiresAt = new Date();
|
||||||
|
accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
|
||||||
|
|
||||||
|
const refreshTokenExpiresAt = new Date();
|
||||||
|
refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7);
|
||||||
|
|
||||||
// Store the refresh token in the database
|
// Store the refresh token in the database
|
||||||
await db.refreshToken.create({
|
await db.refreshToken.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -38,9 +49,12 @@ export class AuthService {
|
||||||
token: refreshToken,
|
token: refreshToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
|
access_token_expires_at: accessTokenExpiresAt,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
|
refresh_token_expires_at: refreshTokenExpiresAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +65,8 @@ export class AuthService {
|
||||||
|
|
||||||
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
||||||
const { refreshToken } = data;
|
const { refreshToken } = data;
|
||||||
let payload;
|
|
||||||
|
let payload: JwtPayload;
|
||||||
try {
|
try {
|
||||||
payload = this.jwtService.verify(refreshToken);
|
payload = this.jwtService.verify(refreshToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -59,7 +74,6 @@ export class AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
|
const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
|
||||||
|
|
||||||
if (!storedToken) {
|
if (!storedToken) {
|
||||||
throw new UnauthorizedException('Invalid refresh token');
|
throw new UnauthorizedException('Invalid refresh token');
|
||||||
}
|
}
|
||||||
|
@ -74,8 +88,13 @@ export class AuthService {
|
||||||
{ expiresIn: '1h' },
|
{ expiresIn: '1h' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Calculate new expiration date
|
||||||
|
const accessTokenExpiresAt = new Date();
|
||||||
|
accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
access_token: newAccessToken,
|
access_token: newAccessToken,
|
||||||
|
access_token_expires_at: accessTokenExpiresAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +112,7 @@ export class AuthService {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
return staff
|
return staff;
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
||||||
|
@ -105,16 +124,34 @@ export class AuthService {
|
||||||
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
||||||
const { oldPassword, newPassword, username } = data;
|
const { oldPassword, newPassword, username } = data;
|
||||||
const user = await db.staff.findUnique({ where: { username } });
|
const user = await db.staff.findUnique({ where: { username } });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
|
const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
|
||||||
if (!isPasswordMatch) {
|
if (!isPasswordMatch) {
|
||||||
throw new UnauthorizedException('Old password is incorrect');
|
throw new UnauthorizedException('Old password is incorrect');
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||||
await this.staffService.update({ id: user.id, password: hashedNewPassword });
|
await this.staffService.update({ id: user.id, password: hashedNewPassword });
|
||||||
|
|
||||||
return { message: 'Password successfully changed' };
|
return { message: 'Password successfully changed' };
|
||||||
}
|
}
|
||||||
|
async getUserProfile(data: JwtPayload) {
|
||||||
|
const { sub } = data
|
||||||
|
const staff = await db.staff.findUnique({
|
||||||
|
where: { id: sub }, include: {
|
||||||
|
department: true,
|
||||||
|
domain: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const staffPerms = await this.roleMapService.getPermsForObject({
|
||||||
|
domainId: staff.domainId,
|
||||||
|
staffId: staff.id,
|
||||||
|
deptId: staff.deptId,
|
||||||
|
});
|
||||||
|
return { ...staff, permissions: staffPerms }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { StaffRouter } from './staff.router';
|
||||||
|
import { StaffService } from './staff.service';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
import { DepartmentService } from '../department/department.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({
|
||||||
|
providers: [StaffRouter, StaffService, TrpcService, DepartmentService],
|
||||||
|
exports: [StaffRouter, StaffService]
|
||||||
|
})
|
||||||
export class StaffModule { }
|
export class StaffModule { }
|
||||||
|
|
|
@ -43,11 +43,6 @@ export class StaffRouter {
|
||||||
.input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
.input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await this.staffService.findMany(input);
|
return await this.staffService.findMany(input);
|
||||||
}),
|
})
|
||||||
findUnique: this.trpc.procedure
|
|
||||||
.input(StaffSchema.findUnique)
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return await this.staffService.findUnique(input);
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RoleMapService } from './rolemap.service';
|
||||||
|
import { RoleRouter } from './role.router';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { RoleMapRouter } from './rolemap.router';
|
||||||
|
import { DepartmentModule } from '@server/models/department/department.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [DepartmentModule],
|
||||||
|
providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter],
|
||||||
|
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter]
|
||||||
|
})
|
||||||
|
export class RoleMapModule { }
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { TrpcService } from "@server/trpc/trpc.service";
|
||||||
|
import { RoleService } from "./role.service";
|
||||||
|
import { z, RoleSchema } from "@nicestack/common";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleRouter {
|
||||||
|
constructor(
|
||||||
|
private readonly trpc: TrpcService,
|
||||||
|
private readonly roleService: RoleService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
router = this.trpc.router({
|
||||||
|
create: this.trpc.protectProcedure.input(RoleSchema.create).mutation(async ({ ctx, input }) => {
|
||||||
|
const { staff } = ctx;
|
||||||
|
return await this.roleService.create(input);
|
||||||
|
}),
|
||||||
|
batchDelete: this.trpc.protectProcedure.input(RoleSchema.batchDelete).mutation(async ({ input }) => {
|
||||||
|
return await this.roleService.batchDelete(input);
|
||||||
|
}),
|
||||||
|
update: this.trpc.protectProcedure.input(RoleSchema.update).mutation(async ({ ctx, input }) => {
|
||||||
|
const { staff } = ctx;
|
||||||
|
return await this.roleService.update(input);
|
||||||
|
}),
|
||||||
|
paginate: this.trpc.protectProcedure.input(RoleSchema.paginate).query(async ({ ctx, input }) => {
|
||||||
|
const { staff } = ctx;
|
||||||
|
return await this.roleService.paginate(input);
|
||||||
|
}),
|
||||||
|
findMany: this.trpc.procedure
|
||||||
|
.input(RoleSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await this.roleService.findMany(input);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { db, z, RoleSchema, ObjectType, Role, RoleMap } from "@nicestack/common";
|
||||||
|
import { DepartmentService } from '@server/models/department/department.service';
|
||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleService {
|
||||||
|
constructor(
|
||||||
|
private readonly departmentService: DepartmentService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建角色
|
||||||
|
* @param data 包含创建角色所需信息的数据
|
||||||
|
* @returns 创建的角色
|
||||||
|
*/
|
||||||
|
async create(data: z.infer<typeof RoleSchema.create>) {
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
return await db.$transaction(async (prisma) => {
|
||||||
|
// 创建角色
|
||||||
|
return await prisma.role.create({ data });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
* @param data 包含更新角色所需信息的数据
|
||||||
|
* @returns 更新后的角色
|
||||||
|
*/
|
||||||
|
async update(data: z.infer<typeof RoleSchema.update>) {
|
||||||
|
const { id, ...others } = data;
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
return await db.$transaction(async (prisma) => {
|
||||||
|
// 更新角色
|
||||||
|
const updatedRole = await prisma.role.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...others }
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRole;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除角色
|
||||||
|
* @param data 包含要删除的角色ID列表的数据
|
||||||
|
* @returns 删除结果
|
||||||
|
* @throws 如果未提供ID,将抛出错误
|
||||||
|
*/
|
||||||
|
async batchDelete(data: z.infer<typeof RoleSchema.batchDelete>) {
|
||||||
|
const { ids } = data;
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'No IDs provided for deletion.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
return await db.$transaction(async (prisma) => {
|
||||||
|
const deletedRoles = await prisma.role.updateMany({
|
||||||
|
where: {
|
||||||
|
id: { in: ids }
|
||||||
|
},
|
||||||
|
data: { deletedAt: new Date() }
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.roleMap.deleteMany({
|
||||||
|
where: {
|
||||||
|
roleId: {
|
||||||
|
in: ids
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deletedRoles.count) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'No roles were found with the provided IDs.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, count: deletedRoles.count };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取角色
|
||||||
|
* @param data 包含分页信息的数据
|
||||||
|
* @returns 分页结果,包含角色列表和总数
|
||||||
|
*/
|
||||||
|
async paginate(data: z.infer<typeof RoleSchema.paginate>) {
|
||||||
|
const { page, pageSize } = data;
|
||||||
|
const [items, totalCount] = await Promise.all([
|
||||||
|
db.role.findMany({
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
where: { deletedAt: null },
|
||||||
|
include: {
|
||||||
|
roleMaps: true,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
db.role.count({ where: { deletedAt: null } }),
|
||||||
|
]);
|
||||||
|
const result = { items, totalCount };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据关键字查找多个角色
|
||||||
|
* @param data 包含关键字的数据
|
||||||
|
* @returns 查找到的角色列表
|
||||||
|
*/
|
||||||
|
async findMany(data: z.infer<typeof RoleSchema.findMany>) {
|
||||||
|
const { keyword } = data
|
||||||
|
return await db.role.findMany({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
contains: keyword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
take: 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
import { RoleMapSchema } from '@nicestack/common';
|
||||||
|
import { RoleMapService } from './rolemap.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleMapRouter {
|
||||||
|
constructor(
|
||||||
|
private readonly trpc: TrpcService,
|
||||||
|
private readonly roleMapService: RoleMapService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
router = this.trpc.router({
|
||||||
|
|
||||||
|
deleteAllRolesForObject: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.deleteWithObject)
|
||||||
|
.mutation(({ input }) =>
|
||||||
|
this.roleMapService.deleteAllRolesForObject(input),
|
||||||
|
),
|
||||||
|
|
||||||
|
setRoleForObject: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.create)
|
||||||
|
.mutation(({ input }) => this.roleMapService.setRoleForObject(input)),
|
||||||
|
|
||||||
|
createManyObjects: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.createManyObjects)
|
||||||
|
.mutation(({ input }) => this.roleMapService.createManyObjects(input)),
|
||||||
|
|
||||||
|
setRolesForObject: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.createManyRoles)
|
||||||
|
.mutation(({ input }) => this.roleMapService.setRolesForObject(input)),
|
||||||
|
|
||||||
|
getPermsForObject: this.trpc.procedure
|
||||||
|
.input(RoleMapSchema.getPermsForObject)
|
||||||
|
.query(({ input }) => this.roleMapService.getPermsForObject(input)),
|
||||||
|
batchDelete: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.batchDelete) // Assuming RoleMapSchema.batchDelete is the Zod schema for batch deleting staff
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
return await this.roleMapService.batchDelete(input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
paginate: this.trpc.procedure
|
||||||
|
.input(RoleMapSchema.paginate) // Define the input schema for pagination
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await this.roleMapService.paginate(input);
|
||||||
|
}),
|
||||||
|
update: this.trpc.protectProcedure
|
||||||
|
.input(RoleMapSchema.update)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const { staff } = ctx;
|
||||||
|
return await this.roleMapService.update(input);
|
||||||
|
}),
|
||||||
|
getRoleMapDetail: this.trpc.procedure
|
||||||
|
.input(RoleMapSchema.getRoleMapDetail)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await this.roleMapService.getRoleMapDetail(input);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { db, z, RoleMapSchema, ObjectType } from '@nicestack/common';
|
||||||
|
import { DepartmentService } from '@server/models/department/department.service';
|
||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleMapService {
|
||||||
|
constructor(private readonly departmentService: DepartmentService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除某对象的所有角色
|
||||||
|
* @param data 包含对象ID的数据
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteAllRolesForObject(
|
||||||
|
data: z.infer<typeof RoleMapSchema.deleteWithObject>,
|
||||||
|
) {
|
||||||
|
const { objectId } = data;
|
||||||
|
return await db.roleMap.deleteMany({
|
||||||
|
where: {
|
||||||
|
objectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为某对象设置一个角色
|
||||||
|
* @param data 角色映射数据
|
||||||
|
* @returns 创建的角色映射
|
||||||
|
*/
|
||||||
|
async setRoleForObject(data: z.infer<typeof RoleMapSchema.create>) {
|
||||||
|
return await db.roleMap.create({
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量为多个对象创建角色映射
|
||||||
|
* @param data 角色映射数据
|
||||||
|
* @returns 创建的角色映射列表
|
||||||
|
*/
|
||||||
|
async createManyObjects(
|
||||||
|
data: z.infer<typeof RoleMapSchema.createManyObjects>,
|
||||||
|
) {
|
||||||
|
const { domainId, roleId, objectIds, objectType } = data;
|
||||||
|
const roleMaps = objectIds.map((id) => ({
|
||||||
|
domainId,
|
||||||
|
objectId: id,
|
||||||
|
roleId,
|
||||||
|
objectType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
return await db.$transaction(async (prisma) => {
|
||||||
|
// 首先,删除现有的角色映射
|
||||||
|
await prisma.roleMap.deleteMany({
|
||||||
|
where: {
|
||||||
|
domainId,
|
||||||
|
roleId,
|
||||||
|
objectType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 然后,创建新的角色映射
|
||||||
|
return await prisma.roleMap.createMany({
|
||||||
|
data: roleMaps,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为某对象设置多个角色
|
||||||
|
* @param data 角色映射数据
|
||||||
|
* @returns 创建的角色映射列表
|
||||||
|
*/
|
||||||
|
async setRolesForObject(data: z.infer<typeof RoleMapSchema.createManyRoles>) {
|
||||||
|
const { domainId, objectId, roleIds, objectType } = data;
|
||||||
|
const roleMaps = roleIds.map((id) => ({
|
||||||
|
domainId,
|
||||||
|
objectId,
|
||||||
|
roleId: id,
|
||||||
|
objectType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return await db.roleMap.createMany({ data: roleMaps });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某对象的权限
|
||||||
|
* @param data 包含域ID、部门ID和对象ID的数据
|
||||||
|
* @returns 用户角色的权限列表
|
||||||
|
*/
|
||||||
|
async getPermsForObject(
|
||||||
|
data: z.infer<typeof RoleMapSchema.getPermsForObject>,
|
||||||
|
) {
|
||||||
|
const { domainId, deptId, staffId } = data;
|
||||||
|
|
||||||
|
let ancestorDeptIds = [];
|
||||||
|
if (deptId) {
|
||||||
|
ancestorDeptIds =
|
||||||
|
await this.departmentService.getAllParentDeptIds(deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRoles = await db.roleMap.findMany({
|
||||||
|
where: {
|
||||||
|
AND: {
|
||||||
|
domainId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
objectId: staffId,
|
||||||
|
objectType: ObjectType.STAFF
|
||||||
|
},
|
||||||
|
(deptId ? {
|
||||||
|
objectId: { in: [deptId, ...ancestorDeptIds] },
|
||||||
|
objectType: ObjectType.DEPARTMENT,
|
||||||
|
} : {}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { role: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return userRoles.flatMap((userRole) => userRole.role.permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除角色映射
|
||||||
|
* @param data 包含要删除的角色映射ID列表的数据
|
||||||
|
* @returns 删除结果
|
||||||
|
* @throws 如果未提供ID,将抛出错误
|
||||||
|
*/
|
||||||
|
async batchDelete(data: z.infer<typeof RoleMapSchema.batchDelete>) {
|
||||||
|
const { ids } = data;
|
||||||
|
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'No IDs provided for deletion.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedRoleMaps = await db.roleMap.deleteMany({
|
||||||
|
where: { id: { in: ids } },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deletedRoleMaps.count) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'No taxonomies were found with the provided IDs.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, count: deletedRoleMaps.count };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取角色映射
|
||||||
|
* @param data 包含分页信息的数据
|
||||||
|
* @returns 分页结果,包含角色映射列表和总数
|
||||||
|
*/
|
||||||
|
async paginate(data: z.infer<typeof RoleMapSchema.paginate>) {
|
||||||
|
const { page, pageSize, domainId, roleId } = data;
|
||||||
|
|
||||||
|
const [items, totalCount] = await Promise.all([
|
||||||
|
db.roleMap.findMany({
|
||||||
|
skip: (page - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
where: { domainId, roleId },
|
||||||
|
}),
|
||||||
|
db.roleMap.count({
|
||||||
|
where: { domainId, roleId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
||||||
|
return { items, totalCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色映射
|
||||||
|
* @param data 包含更新信息的数据
|
||||||
|
* @returns 更新后的角色映射
|
||||||
|
*/
|
||||||
|
async update(data: z.infer<typeof RoleMapSchema.update>) {
|
||||||
|
const { id, ...others } = data;
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
return await db.$transaction(async (prisma) => {
|
||||||
|
// 更新角色映射
|
||||||
|
const updatedRoleMap = await prisma.roleMap.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ...others },
|
||||||
|
});
|
||||||
|
return updatedRoleMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色映射详情
|
||||||
|
* @param data 包含角色ID和域ID的数据
|
||||||
|
* @returns 角色映射详情,包含部门ID和员工ID列表
|
||||||
|
*/
|
||||||
|
async getRoleMapDetail(data: z.infer<typeof RoleMapSchema.getRoleMapDetail>) {
|
||||||
|
const { roleId, domainId } = data;
|
||||||
|
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
||||||
|
|
||||||
|
const deptIds = res
|
||||||
|
.filter((item) => item.objectType === ObjectType.DEPARTMENT)
|
||||||
|
.map((item) => item.objectId);
|
||||||
|
const staffIds = res
|
||||||
|
.filter((item) => item.objectType === ObjectType.STAFF)
|
||||||
|
.map((item) => item.objectId);
|
||||||
|
|
||||||
|
return { deptIds, staffIds };
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TransformController } from './transform.controller';
|
|
||||||
import { TransformService } from './transform.service';
|
import { TransformService } from './transform.service';
|
||||||
|
import { TransformRouter } from './transform.router';
|
||||||
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [TransformController],
|
providers: [TransformService, TransformRouter, TrpcService],
|
||||||
providers: [TransformService]
|
exports: [TransformRouter]
|
||||||
})
|
})
|
||||||
export class TransformModule { }
|
export class TransformModule { }
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TrpcService } from './trpc.service';
|
import { TrpcService } from './trpc.service';
|
||||||
import { TrpcRouter } from './trpc.router';
|
import { TrpcRouter } from './trpc.router';
|
||||||
import { HelloService } from '@server/hello/hello.service';
|
import { DepartmentRouter } from '@server/models/department/department.router';
|
||||||
import { HelloRouter } from '@server/hello/hello.router';
|
import { TransformRouter } from '@server/transform/transform.router';
|
||||||
|
import { StaffRouter } from '@server/models/staff/staff.router';
|
||||||
|
import { StaffModule } from '../models/staff/staff.module';
|
||||||
|
import { DepartmentModule } from '@server/models/department/department.module';
|
||||||
|
import { TransformModule } from '@server/transform/transform.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [StaffModule, DepartmentModule, TransformModule],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, HelloRouter, HelloService],
|
providers: [TrpcService, TrpcRouter],
|
||||||
})
|
})
|
||||||
export class TrpcModule { }
|
export class TrpcModule { }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { INestApplication, Injectable } from '@nestjs/common';
|
import { INestApplication, Injectable } from '@nestjs/common';
|
||||||
import { AuthRouter } from '@server/auth/auth.router';
|
|
||||||
import { DepartmentRouter } from '@server/models/department/department.router';
|
import { DepartmentRouter } from '@server/models/department/department.router';
|
||||||
import { StaffRouter } from '@server/models/staff/staff.router';
|
import { StaffRouter } from '@server/models/staff/staff.router';
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
@ -8,13 +7,11 @@ import { TransformRouter } from '../transform/transform.router';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
constructor(private readonly trpc: TrpcService,
|
constructor(private readonly trpc: TrpcService,
|
||||||
private readonly auth: AuthRouter,
|
|
||||||
private readonly staff: StaffRouter,
|
private readonly staff: StaffRouter,
|
||||||
private readonly department: DepartmentRouter,
|
private readonly department: DepartmentRouter,
|
||||||
private readonly transform: TransformRouter
|
private readonly transform: TransformRouter
|
||||||
) { }
|
) { }
|
||||||
appRouter = this.trpc.router({
|
appRouter = this.trpc.router({
|
||||||
auth: this.auth.router,
|
|
||||||
staff: this.staff.router,
|
staff: this.staff.router,
|
||||||
department: this.department.router,
|
department: this.department.router,
|
||||||
transform: this.transform.router
|
transform: this.transform.router
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { initTRPC, TRPCError } from '@trpc/server';
|
||||||
import superjson from 'superjson-cjs';
|
import superjson from 'superjson-cjs';
|
||||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||||
import { env } from '@server/env';
|
import { env } from '@server/env';
|
||||||
import { db, Staff, TokenPayload } from "@nicestack/common"
|
import { db, Staff, JwtPayload } from "@nicestack/common"
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
type Context = Awaited<ReturnType<TrpcService['createContext']>>;
|
type Context = Awaited<ReturnType<TrpcService['createContext']>>;
|
||||||
|
@ -15,15 +15,15 @@ export class TrpcService {
|
||||||
res,
|
res,
|
||||||
}: trpcExpress.CreateExpressContextOptions) {
|
}: trpcExpress.CreateExpressContextOptions) {
|
||||||
const token = req.headers.authorization?.split(' ')[1];
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
let tokenData: TokenPayload | undefined = undefined;
|
let tokenData: JwtPayload | undefined = undefined;
|
||||||
let staff: Staff | undefined = undefined;
|
let staff: Staff | undefined = undefined;
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
// Verify JWT token and extract tokenData
|
// Verify JWT token and extract tokenData
|
||||||
tokenData = await this.jwtService.verifyAsync(token, { secret: env.JWT_SECRET }) as TokenPayload;
|
tokenData = await this.jwtService.verifyAsync(token, { secret: env.JWT_SECRET }) as JwtPayload;
|
||||||
if (tokenData) {
|
if (tokenData) {
|
||||||
// Fetch staff details from the database using tokenData.id
|
// Fetch staff details from the database using tokenData.id
|
||||||
staff = await db.staff.findUnique({ where: { id: tokenData.id } });
|
staff = await db.staff.findUnique({ where: { id: tokenData.sub } });
|
||||||
if (!staff) {
|
if (!staff) {
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" });
|
throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
|
<script>
|
||||||
|
window.env = {
|
||||||
|
VITE_APP_TUS_URL: "$VITE_APP_TUS_URL",
|
||||||
|
VITE_APP_API_URL: "$VITE_APP_API_URL"
|
||||||
|
};
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -10,22 +10,23 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-form": "^0.26.3",
|
"@nicestack/common": "workspace:^",
|
||||||
"@tanstack/react-query": "^5.50.1",
|
|
||||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||||
|
"@tanstack/react-query": "^5.51.1",
|
||||||
"@tanstack/react-query-persist-client": "^5.51.9",
|
"@tanstack/react-query-persist-client": "^5.51.9",
|
||||||
"@tanstack/react-virtual": "^3.8.3",
|
"@tanstack/react-virtual": "^3.8.3",
|
||||||
"@tanstack/zod-form-adapter": "^0.26.3",
|
"@tanstack/zod-form-adapter": "^0.26.3",
|
||||||
"@trpc/client": "11.0.0-rc.456",
|
"@trpc/client": "11.0.0-rc.456",
|
||||||
"@trpc/react-query": "11.0.0-rc.456",
|
"@trpc/react-query": "11.0.0-rc.456",
|
||||||
"@trpc/server": "11.0.0-rc.456",
|
"@trpc/server": "11.0.0-rc.456",
|
||||||
|
"axios": "^1.7.3",
|
||||||
|
"idb-keyval": "^6.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.24.1",
|
"react-router-dom": "^6.24.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"idb-keyval": "^6.2.1",
|
"zustand": "^4.5.5"
|
||||||
"@nicestack/common": "workspace:^"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|
|
@ -4,12 +4,15 @@ import {
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import QueryProvider from './providers/query-provider'
|
import QueryProvider from './providers/query-provider'
|
||||||
import { router } from './routes';
|
import { router } from './routes';
|
||||||
|
import { AuthProvider } from './providers/auth-provider';
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AuthProvider>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<RouterProvider router={router}></RouterProvider>
|
<RouterProvider router={router}></RouterProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
|
</AuthProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function LoginPage() {
|
||||||
|
return 'LoginPage'
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
export default function MainPage() {
|
export default function MainPage() {
|
||||||
return <>hello,world</>
|
return <>hello,world</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useAuth } from '@web/src/providers/auth-provider';
|
||||||
|
import { RolePerms } from '@nicestack/common';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
// Define a type for the props that the HOC will accept.
|
||||||
|
interface WithAuthProps {
|
||||||
|
permissions?: RolePerms[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the HOC function.
|
||||||
|
export default function WithAuth({ options = {}, children }: { children: ReactNode, options?: WithAuthProps }) {
|
||||||
|
const { isAuthenticated, user, isLoading } = useAuth();
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
// If the user is not authenticated, redirect them to the login page.
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to={'/login'}></Navigate>
|
||||||
|
|
||||||
|
}
|
||||||
|
if (options.permissions && user) {
|
||||||
|
const hasPermissions = options.permissions.every(permission => user.permissions.includes(permission));
|
||||||
|
if (!hasPermissions) {
|
||||||
|
return <div>You do not have the required permissions to view this page.</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return a new functional component.
|
||||||
|
return children
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const env: {
|
||||||
|
TUS_URL: string;
|
||||||
|
API_URL: string;
|
||||||
|
} = {
|
||||||
|
TUS_URL:
|
||||||
|
import.meta.env.PROD
|
||||||
|
? (window as any).env.VITE_APP_TUS_URL
|
||||||
|
: import.meta.env.VITE_APP_TUS_URL,
|
||||||
|
API_URL:
|
||||||
|
import.meta.env.PROD
|
||||||
|
? (window as any).env.VITE_APP_API_URL
|
||||||
|
: import.meta.env.VITE_APP_API_URL,
|
||||||
|
};
|
|
@ -0,0 +1,159 @@
|
||||||
|
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
||||||
|
import apiClient from '../utils/axios-client';
|
||||||
|
import { UserProfile } from '@nicestack/common';
|
||||||
|
|
||||||
|
interface AuthContextProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
refreshToken: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
user: any;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
refreshAccessToken: () => Promise<void>;
|
||||||
|
initializeAuth: () => void;
|
||||||
|
startTokenRefreshInterval: () => void;
|
||||||
|
fetchUserProfile: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
|
||||||
|
export const useAuth = (): AuthContextProps => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
interface AuthProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(localStorage.getItem('access_token'));
|
||||||
|
const [refreshToken, setRefreshToken] = useState<string | null>(localStorage.getItem('refresh_token'));
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(!!localStorage.getItem('access_token'));
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
const [user, setUser] = useState<UserProfile | null>(null);
|
||||||
|
const initializeAuth = useCallback(() => {
|
||||||
|
const storedAccessToken = localStorage.getItem('access_token');
|
||||||
|
const storedRefreshToken = localStorage.getItem('refresh_token');
|
||||||
|
setAccessToken(storedAccessToken);
|
||||||
|
setRefreshToken(storedRefreshToken);
|
||||||
|
setIsAuthenticated(!!storedAccessToken);
|
||||||
|
if (storedRefreshToken) {
|
||||||
|
startTokenRefreshInterval();
|
||||||
|
}
|
||||||
|
if (storedAccessToken) {
|
||||||
|
fetchUserProfile();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const refreshAccessToken = useCallback(async () => {
|
||||||
|
if (!refreshToken) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await apiClient.post(`/auth/refresh-token`, { refreshToken });
|
||||||
|
const { access_token, access_token_expires_at } = response.data;
|
||||||
|
localStorage.setItem('access_token', access_token);
|
||||||
|
localStorage.setItem('access_token_expires_at', access_token_expires_at);
|
||||||
|
setAccessToken(access_token);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
fetchUserProfile();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Token refresh failed", err);
|
||||||
|
logout();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [refreshToken]);
|
||||||
|
|
||||||
|
const startTokenRefreshInterval = useCallback(async () => {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshAccessToken();
|
||||||
|
|
||||||
|
const newIntervalId = setInterval(refreshAccessToken, 10 * 60 * 1000); // 10 minutes
|
||||||
|
|
||||||
|
setIntervalId(newIntervalId);
|
||||||
|
}, [intervalId, refreshAccessToken]);
|
||||||
|
|
||||||
|
const login = async (username: string, password: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await apiClient.post(`/auth/login`, { username, password });
|
||||||
|
const { access_token, refresh_token, access_token_expires_at, refresh_token_expires_at } = response.data;
|
||||||
|
localStorage.setItem('access_token', access_token);
|
||||||
|
localStorage.setItem('refresh_token', refresh_token);
|
||||||
|
localStorage.setItem('access_token_expires_at', access_token_expires_at);
|
||||||
|
localStorage.setItem('refresh_token_expires_at', refresh_token_expires_at);
|
||||||
|
setAccessToken(access_token);
|
||||||
|
setRefreshToken(refresh_token);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
startTokenRefreshInterval();
|
||||||
|
fetchUserProfile();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Login failed", err);
|
||||||
|
throw new Error("Login failed");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const storedRefreshToken = localStorage.getItem('refresh_token');
|
||||||
|
await apiClient.post(`/auth/logout`, { refreshToken: storedRefreshToken });
|
||||||
|
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('access_token_expires_at');
|
||||||
|
localStorage.removeItem('refresh_token_expires_at');
|
||||||
|
|
||||||
|
setAccessToken(null);
|
||||||
|
setRefreshToken(null);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
setIntervalId(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Logout failed", err);
|
||||||
|
throw new Error("Logout failed");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUserProfile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/auth/user-profile`);
|
||||||
|
setUser(response.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Fetching user profile failed", err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeAuth();
|
||||||
|
}, [initializeAuth]);
|
||||||
|
|
||||||
|
const value: AuthContextProps = {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading,
|
||||||
|
user,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
refreshAccessToken,
|
||||||
|
initializeAuth,
|
||||||
|
startTokenRefreshInterval,
|
||||||
|
fetchUserProfile
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
|
};
|
|
@ -1,25 +1,23 @@
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { unstable_httpBatchStreamLink, loggerLink } from '@trpc/client';
|
import { unstable_httpBatchStreamLink, loggerLink } from '@trpc/client';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { api } from '../utils/trpc';
|
import { api } from '../utils/trpc';
|
||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
||||||
import { createIDBPersister } from '../utils/idb';
|
import { createIDBPersister } from '../utils/idb';
|
||||||
|
import { env } from '../env';
|
||||||
|
import { useAuth } from './auth-provider';
|
||||||
export default function QueryProvider({ children }: { children: React.ReactNode }) {
|
export default function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
const [queryClient] = useState(() => new QueryClient());
|
||||||
const [trpcClient] = useState(() =>
|
const { accessToken } = useAuth()
|
||||||
|
const trpcClient = useMemo(() =>
|
||||||
api.createClient({
|
api.createClient({
|
||||||
|
|
||||||
links: [
|
links: [
|
||||||
unstable_httpBatchStreamLink({
|
unstable_httpBatchStreamLink({
|
||||||
url: 'http://localhost:3000/trpc',
|
url: `${env.API_URL}/trpc`,
|
||||||
// You can pass any HTTP headers you wish here
|
headers: async () => ({
|
||||||
async headers() {
|
...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}),
|
||||||
return {
|
}),
|
||||||
// authorization: getAuthCookie(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
transformer: superjson
|
transformer: superjson
|
||||||
}),
|
}),
|
||||||
loggerLink({
|
loggerLink({
|
||||||
|
@ -29,7 +27,7 @@ export default function QueryProvider({ children }: { children: React.ReactNode
|
||||||
(opts.direction === 'down' && opts.result instanceof Error),
|
(opts.direction === 'down' && opts.result instanceof Error),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}), [accessToken]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<api.Provider client={trpcClient} queryClient={queryClient}>
|
<api.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {
|
||||||
import MainPage from "../app/main/page";
|
import MainPage from "../app/main/page";
|
||||||
import ErrorPage from "../app/error";
|
import ErrorPage from "../app/error";
|
||||||
import LayoutPage from "../app/layout";
|
import LayoutPage from "../app/layout";
|
||||||
|
import LoginPage from "../app/login";
|
||||||
|
import WithAuth from "../components/auth/with-auth";
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
@ -13,8 +15,13 @@ export const router = createBrowserRouter([
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <MainPage></MainPage>
|
element: <WithAuth><MainPage></MainPage></WithAuth>
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
element: <LoginPage></LoginPage>
|
||||||
|
}
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { env } from '../env';
|
||||||
|
const BASE_URL = env.API_URL; // Replace with your backend URL
|
||||||
|
const apiClient = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
// Add a request interceptor to attach the access token
|
||||||
|
apiClient.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const accessToken = localStorage.getItem('access_token');
|
||||||
|
if (accessToken) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default apiClient;
|
|
@ -81,7 +81,6 @@ model Comment {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
createdBy String?
|
createdBy String?
|
||||||
|
|
||||||
createdStaff Staff? @relation(fields: [createdBy], references: [id])
|
createdStaff Staff? @relation(fields: [createdBy], references: [id])
|
||||||
|
|
||||||
@@map("comments")
|
@@map("comments")
|
||||||
|
|
|
@ -9,6 +9,8 @@ export enum RelationType {
|
||||||
READED = "READED",
|
READED = "READED",
|
||||||
MESSAGE = "MESSAGE",
|
MESSAGE = "MESSAGE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export enum RolePerms {
|
export enum RolePerms {
|
||||||
// Create Permissions 创建权限
|
// Create Permissions 创建权限
|
||||||
CREATE_ALERT = "CREATE_ALERT", // 创建警报
|
CREATE_ALERT = "CREATE_ALERT", // 创建警报
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { ObjectType } from "./enum";
|
||||||
export const AuthSchema = {
|
export const AuthSchema = {
|
||||||
signInRequset: z.object({
|
signInRequset: z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
|
@ -18,7 +19,7 @@ export const AuthSchema = {
|
||||||
}),
|
}),
|
||||||
logoutRequest: z.object({
|
logoutRequest: z.object({
|
||||||
refreshToken: z.string(),
|
refreshToken: z.string(),
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
export const StaffSchema = {
|
export const StaffSchema = {
|
||||||
create: z.object({
|
create: z.object({
|
||||||
|
@ -90,3 +91,73 @@ export const DepartmentSchema = {
|
||||||
ids: z.array(z.string()).nullish(),
|
ids: z.array(z.string()).nullish(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
export const RoleMapSchema = {
|
||||||
|
create: z.object({
|
||||||
|
objectId: z.string(),
|
||||||
|
roleId: z.string(),
|
||||||
|
domainId: z.string(),
|
||||||
|
objectType: z.nativeEnum(ObjectType),
|
||||||
|
}),
|
||||||
|
update: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
objectId: z.string().nullish(),
|
||||||
|
roleId: z.string().nullish(),
|
||||||
|
domainId: z.string().nullish(),
|
||||||
|
objectType: z.nativeEnum(ObjectType).nullish(),
|
||||||
|
}),
|
||||||
|
createManyRoles: z.object({
|
||||||
|
objectId: z.string(),
|
||||||
|
roleIds: z.array(z.string()),
|
||||||
|
domainId: z.string(),
|
||||||
|
objectType: z.nativeEnum(ObjectType),
|
||||||
|
}),
|
||||||
|
createManyObjects: z.object({
|
||||||
|
objectIds: z.array(z.string()),
|
||||||
|
roleId: z.string(),
|
||||||
|
domainId: z.string().nullish(),
|
||||||
|
objectType: z.nativeEnum(ObjectType),
|
||||||
|
}),
|
||||||
|
batchDelete: z.object({
|
||||||
|
ids: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
paginate: z.object({
|
||||||
|
page: z.number().min(1),
|
||||||
|
pageSize: z.number().min(1),
|
||||||
|
domainId: z.string().nullish(),
|
||||||
|
roleId: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
deleteWithObject: z.object({
|
||||||
|
objectId: z.string(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
getRoleMapDetail: z.object({
|
||||||
|
roleId: z.string(),
|
||||||
|
domainId: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
getPermsForObject: z.object({
|
||||||
|
domainId: z.string(),
|
||||||
|
staffId: z.string(),
|
||||||
|
deptId: z.string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
export const RoleSchema = {
|
||||||
|
create: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
permissions: z.array(z.string()).nullish(),
|
||||||
|
}),
|
||||||
|
update: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().nullish(),
|
||||||
|
permissions: z.array(z.string()).nullish(),
|
||||||
|
}),
|
||||||
|
batchDelete: z.object({
|
||||||
|
ids: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
paginate: z.object({
|
||||||
|
page: z.number().nullish(),
|
||||||
|
pageSize: z.number().nullish(),
|
||||||
|
}),
|
||||||
|
findMany: z.object({
|
||||||
|
keyword: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
|
@ -13,7 +13,29 @@ export type StaffDto = Staff & {
|
||||||
domain?: Department;
|
domain?: Department;
|
||||||
department?: Department;
|
department?: Department;
|
||||||
};
|
};
|
||||||
export interface TokenPayload {
|
export type UserProfile = Staff & {
|
||||||
id: string;
|
permissions: string[];
|
||||||
|
department?: Department;
|
||||||
|
domain?: Department;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
export interface GenPerms {
|
||||||
|
instruction?: boolean;
|
||||||
|
createProgress?: boolean;
|
||||||
|
requestCancel?: boolean;
|
||||||
|
acceptCancel?: boolean;
|
||||||
|
|
||||||
|
conclude?: boolean;
|
||||||
|
createRisk?: boolean;
|
||||||
|
editIndicator?: boolean;
|
||||||
|
editMethod?: boolean;
|
||||||
|
editOrg?: boolean;
|
||||||
|
|
||||||
|
edit?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
read?: boolean;
|
||||||
|
}
|
Loading…
Reference in New Issue