09091848
This commit is contained in:
parent
baa376d436
commit
b8c73030d8
|
@ -230,3 +230,6 @@ $RECYCLE.BIN/
|
|||
*~
|
||||
|
||||
docker-compose.yml
|
||||
.env
|
||||
packages/common/prisma/migrations
|
||||
volumes
|
|
@ -26,6 +26,7 @@
|
|||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.4.1",
|
||||
"@nestjs/schedule": "^4.1.0",
|
||||
"@nestjs/websockets": "^10.3.10",
|
||||
"@nicestack/common": "workspace:^",
|
||||
|
|
|
@ -8,9 +8,10 @@ import { QueueModule } from './queue/queue.module';
|
|||
import { TransformModule } from './transform/transform.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule],
|
||||
providers: [RedisService, SocketGateway],
|
||||
providers: [RedisService, SocketGateway, ConfigService],
|
||||
})
|
||||
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 { env } from '@server/env';
|
||||
import { Request } from 'express';
|
||||
import { JwtPayload } from '@nicestack/common';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
@ -18,7 +19,7 @@ export class AuthGuard implements CanActivate {
|
|||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(
|
||||
token,
|
||||
{
|
||||
secret: env.JWT_SECRET
|
||||
|
|
|
@ -2,13 +2,18 @@ import { Module } from '@nestjs/common';
|
|||
import { AuthService } from './auth.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
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({
|
||||
providers: [AuthService],
|
||||
providers: [AuthService, StaffService, RoleMapService,DepartmentService],
|
||||
imports: [JwtModule.register({
|
||||
global: true,
|
||||
secret: env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '60s' },
|
||||
}),]
|
||||
}),],
|
||||
controllers: [AuthController]
|
||||
})
|
||||
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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -1,120 +1,157 @@
|
|||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { AuthSchema, db, z } from '@nicestack/common';
|
||||
import { StaffService } from '@server/models/staff/staff.service';
|
||||
import { JwtPayload } from '@nicestack/common';
|
||||
import { RoleMapService } from '@server/rbac/rolemap.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly staffService: StaffService,
|
||||
) { }
|
||||
async signIn(data: z.infer<typeof AuthSchema.signInRequset>) {
|
||||
const { username, password } = data;
|
||||
const staff = await db.staff.findUnique({ where: { username } });
|
||||
if (!staff) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
const isPasswordMatch = await bcrypt.compare(password, staff.password);
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly staffService: StaffService,
|
||||
private readonly roleMapService: RoleMapService
|
||||
) { }
|
||||
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
const payload = { sub: staff.id, username: staff.username };
|
||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
|
||||
const refreshToken = await this.generateRefreshToken(staff.id);
|
||||
|
||||
// Store the refresh token in the database
|
||||
await db.refreshToken.create({
|
||||
data: {
|
||||
staffId: staff.id,
|
||||
token: refreshToken,
|
||||
},
|
||||
});
|
||||
return {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
};
|
||||
async signIn(data: z.infer<typeof AuthSchema.signInRequset>) {
|
||||
const { username, password } = data;
|
||||
const staff = await db.staff.findUnique({ where: { username } });
|
||||
if (!staff) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
async generateRefreshToken(userId: string): Promise<string> {
|
||||
const payload = { sub: userId };
|
||||
return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration
|
||||
const isPasswordMatch = await bcrypt.compare(password, staff.password);
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
||||
const { refreshToken } = data;
|
||||
let payload;
|
||||
try {
|
||||
payload = this.jwtService.verify(refreshToken);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
const payload: JwtPayload = { sub: staff.id, username: staff.username };
|
||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
|
||||
const refreshToken = await this.generateRefreshToken(staff.id);
|
||||
|
||||
const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
|
||||
// Calculate expiration dates
|
||||
const accessTokenExpiresAt = new Date();
|
||||
accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
|
||||
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
const refreshTokenExpiresAt = new Date();
|
||||
refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7);
|
||||
|
||||
const user = await db.staff.findUnique({ where: { id: payload.sub } });
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
// Store the refresh token in the database
|
||||
await db.refreshToken.create({
|
||||
data: {
|
||||
staffId: staff.id,
|
||||
token: refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
const newAccessToken = await this.jwtService.signAsync(
|
||||
{ sub: user.id, username: user.username },
|
||||
{ expiresIn: '1h' },
|
||||
);
|
||||
return {
|
||||
access_token: accessToken,
|
||||
access_token_expires_at: accessTokenExpiresAt,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token_expires_at: refreshTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: newAccessToken,
|
||||
};
|
||||
async generateRefreshToken(userId: string): Promise<string> {
|
||||
const payload = { sub: userId };
|
||||
return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration
|
||||
}
|
||||
|
||||
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
||||
const { refreshToken } = data;
|
||||
|
||||
let payload: JwtPayload;
|
||||
try {
|
||||
payload = this.jwtService.verify(refreshToken);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
async signUp(data: z.infer<typeof AuthSchema.signUpRequest>) {
|
||||
const { username, password } = data;
|
||||
const existingUser = await db.staff.findUnique({ where: { username } });
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('Username is already taken');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const staff = await this.staffService.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
return staff
|
||||
const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
||||
const { refreshToken } = data;
|
||||
await db.refreshToken.deleteMany({ where: { token: refreshToken } });
|
||||
return { message: 'Logout successful' };
|
||||
const user = await db.staff.findUnique({ where: { id: payload.sub } });
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
||||
const { oldPassword, newPassword, username } = data;
|
||||
const user = await db.staff.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Old password is incorrect');
|
||||
}
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.staffService.update({ id: user.id, password: hashedNewPassword });
|
||||
const newAccessToken = await this.jwtService.signAsync(
|
||||
{ sub: user.id, username: user.username },
|
||||
{ expiresIn: '1h' },
|
||||
);
|
||||
|
||||
return { message: 'Password successfully changed' };
|
||||
// Calculate new expiration date
|
||||
const accessTokenExpiresAt = new Date();
|
||||
accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
|
||||
|
||||
return {
|
||||
access_token: newAccessToken,
|
||||
access_token_expires_at: accessTokenExpiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
async signUp(data: z.infer<typeof AuthSchema.signUpRequest>) {
|
||||
const { username, password } = data;
|
||||
const existingUser = await db.staff.findUnique({ where: { username } });
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('Username is already taken');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const staff = await this.staffService.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
return staff;
|
||||
}
|
||||
|
||||
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
||||
const { refreshToken } = data;
|
||||
await db.refreshToken.deleteMany({ where: { token: refreshToken } });
|
||||
return { message: 'Logout successful' };
|
||||
}
|
||||
|
||||
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
||||
const { oldPassword, newPassword, username } = data;
|
||||
const user = await db.staff.findUnique({ where: { username } });
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
|
||||
const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Old password is incorrect');
|
||||
}
|
||||
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.staffService.update({ id: user.id, password: hashedNewPassword });
|
||||
|
||||
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 { StaffRouter } from './staff.router';
|
||||
import { StaffService } from './staff.service';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { DepartmentService } from '../department/department.service';
|
||||
|
||||
@Module({})
|
||||
export class StaffModule {}
|
||||
@Module({
|
||||
providers: [StaffRouter, StaffService, TrpcService, DepartmentService],
|
||||
exports: [StaffRouter, StaffService]
|
||||
})
|
||||
export class StaffModule { }
|
||||
|
|
|
@ -8,7 +8,7 @@ export class StaffRouter {
|
|||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly staffService: StaffService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
|
@ -43,11 +43,6 @@ export class StaffRouter {
|
|||
.input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ 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 { TransformController } from './transform.controller';
|
||||
import { TransformService } from './transform.service';
|
||||
import { TransformRouter } from './transform.router';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TransformController],
|
||||
providers: [TransformService]
|
||||
providers: [TransformService, TransformRouter, TrpcService],
|
||||
exports: [TransformRouter]
|
||||
})
|
||||
export class TransformModule {}
|
||||
export class TransformModule { }
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TrpcService } from './trpc.service';
|
||||
import { TrpcRouter } from './trpc.router';
|
||||
import { HelloService } from '@server/hello/hello.service';
|
||||
import { HelloRouter } from '@server/hello/hello.router';
|
||||
import { DepartmentRouter } from '@server/models/department/department.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({
|
||||
imports: [],
|
||||
imports: [StaffModule, DepartmentModule, TransformModule],
|
||||
controllers: [],
|
||||
providers: [TrpcService, TrpcRouter, HelloRouter, HelloService],
|
||||
providers: [TrpcService, TrpcRouter],
|
||||
})
|
||||
export class TrpcModule { }
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { INestApplication, Injectable } from '@nestjs/common';
|
||||
import { AuthRouter } from '@server/auth/auth.router';
|
||||
import { DepartmentRouter } from '@server/models/department/department.router';
|
||||
import { StaffRouter } from '@server/models/staff/staff.router';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
@ -8,13 +7,11 @@ import { TransformRouter } from '../transform/transform.router';
|
|||
@Injectable()
|
||||
export class TrpcRouter {
|
||||
constructor(private readonly trpc: TrpcService,
|
||||
private readonly auth: AuthRouter,
|
||||
private readonly staff: StaffRouter,
|
||||
private readonly department: DepartmentRouter,
|
||||
private readonly transform: TransformRouter
|
||||
) { }
|
||||
appRouter = this.trpc.router({
|
||||
auth: this.auth.router,
|
||||
staff: this.staff.router,
|
||||
department: this.department.router,
|
||||
transform: this.transform.router
|
||||
|
|
|
@ -3,7 +3,7 @@ import { initTRPC, TRPCError } from '@trpc/server';
|
|||
import superjson from 'superjson-cjs';
|
||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||
import { env } from '@server/env';
|
||||
import { db, Staff, TokenPayload } from "@nicestack/common"
|
||||
import { db, Staff, JwtPayload } from "@nicestack/common"
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
type Context = Awaited<ReturnType<TrpcService['createContext']>>;
|
||||
|
@ -15,15 +15,15 @@ export class TrpcService {
|
|||
res,
|
||||
}: trpcExpress.CreateExpressContextOptions) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
let tokenData: TokenPayload | undefined = undefined;
|
||||
let tokenData: JwtPayload | undefined = undefined;
|
||||
let staff: Staff | undefined = undefined;
|
||||
if (token) {
|
||||
try {
|
||||
// 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) {
|
||||
// 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) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" });
|
||||
}
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -10,22 +10,23 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-form": "^0.26.3",
|
||||
"@tanstack/react-query": "^5.50.1",
|
||||
"@nicestack/common": "workspace:^",
|
||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||
"@tanstack/react-query": "^5.51.1",
|
||||
"@tanstack/react-query-persist-client": "^5.51.9",
|
||||
"@tanstack/react-virtual": "^3.8.3",
|
||||
"@tanstack/zod-form-adapter": "^0.26.3",
|
||||
"@trpc/client": "11.0.0-rc.456",
|
||||
"@trpc/react-query": "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-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"superjson": "^2.2.1",
|
||||
"zod": "^3.23.8",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"@nicestack/common": "workspace:^"
|
||||
"zustand": "^4.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
|
|
|
@ -4,12 +4,15 @@ import {
|
|||
} from "react-router-dom";
|
||||
import QueryProvider from './providers/query-provider'
|
||||
import { router } from './routes';
|
||||
import { AuthProvider } from './providers/auth-provider';
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<QueryProvider>
|
||||
<RouterProvider router={router}></RouterProvider>
|
||||
</QueryProvider>
|
||||
<AuthProvider>
|
||||
<QueryProvider>
|
||||
<RouterProvider router={router}></RouterProvider>
|
||||
</QueryProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export default function LoginPage() {
|
||||
return 'LoginPage'
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
export default function MainPage() {
|
||||
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 { unstable_httpBatchStreamLink, loggerLink } from '@trpc/client';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { api } from '../utils/trpc';
|
||||
import superjson from 'superjson';
|
||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
||||
import { createIDBPersister } from '../utils/idb';
|
||||
|
||||
import { env } from '../env';
|
||||
import { useAuth } from './auth-provider';
|
||||
export default function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [trpcClient] = useState(() =>
|
||||
const { accessToken } = useAuth()
|
||||
const trpcClient = useMemo(() =>
|
||||
api.createClient({
|
||||
|
||||
links: [
|
||||
unstable_httpBatchStreamLink({
|
||||
url: 'http://localhost:3000/trpc',
|
||||
// You can pass any HTTP headers you wish here
|
||||
async headers() {
|
||||
return {
|
||||
// authorization: getAuthCookie(),
|
||||
};
|
||||
},
|
||||
url: `${env.API_URL}/trpc`,
|
||||
headers: async () => ({
|
||||
...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}),
|
||||
}),
|
||||
transformer: superjson
|
||||
}),
|
||||
loggerLink({
|
||||
|
@ -29,7 +27,7 @@ export default function QueryProvider({ children }: { children: React.ReactNode
|
|||
(opts.direction === 'down' && opts.result instanceof Error),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}), [accessToken]
|
||||
);
|
||||
return (
|
||||
<api.Provider client={trpcClient} queryClient={queryClient}>
|
||||
|
|
|
@ -4,6 +4,8 @@ import {
|
|||
import MainPage from "../app/main/page";
|
||||
import ErrorPage from "../app/error";
|
||||
import LayoutPage from "../app/layout";
|
||||
import LoginPage from "../app/login";
|
||||
import WithAuth from "../components/auth/with-auth";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
|
@ -13,8 +15,13 @@ export const router = createBrowserRouter([
|
|||
children: [
|
||||
{
|
||||
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;
|
|
@ -71,18 +71,17 @@ model TermAncestry {
|
|||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
style String
|
||||
link String?
|
||||
title String?
|
||||
content String
|
||||
attachments String[] @default([])
|
||||
createdAt DateTime? @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
createdBy String?
|
||||
|
||||
createdStaff Staff? @relation(fields: [createdBy], references: [id])
|
||||
id String @id @default(uuid())
|
||||
style String
|
||||
link String?
|
||||
title String?
|
||||
content String
|
||||
attachments String[] @default([])
|
||||
createdAt DateTime? @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
createdBy String?
|
||||
createdStaff Staff? @relation(fields: [createdBy], references: [id])
|
||||
|
||||
@@map("comments")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ export enum RelationType {
|
|||
READED = "READED",
|
||||
MESSAGE = "MESSAGE",
|
||||
}
|
||||
|
||||
|
||||
export enum RolePerms {
|
||||
// Create Permissions 创建权限
|
||||
CREATE_ALERT = "CREATE_ALERT", // 创建警报
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "zod"
|
||||
import { ObjectType } from "./enum";
|
||||
export const AuthSchema = {
|
||||
signInRequset: z.object({
|
||||
username: z.string(),
|
||||
|
@ -18,7 +19,7 @@ export const AuthSchema = {
|
|||
}),
|
||||
logoutRequest: z.object({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
})
|
||||
};
|
||||
export const StaffSchema = {
|
||||
create: z.object({
|
||||
|
@ -90,3 +91,73 @@ export const DepartmentSchema = {
|
|||
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;
|
||||
department?: Department;
|
||||
};
|
||||
export interface TokenPayload {
|
||||
id: string;
|
||||
export type UserProfile = Staff & {
|
||||
permissions: string[];
|
||||
department?: Department;
|
||||
domain?: Department;
|
||||
}
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: 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