diff --git a/.tmuxinator/dev.yml b/.tmuxinator/dev.yml
index f2059bd3..ee346a0c 100644
--- a/.tmuxinator/dev.yml
+++ b/.tmuxinator/dev.yml
@@ -32,3 +32,9 @@ windows:
- docker:
- echo "Docker Services (Ctrl+a 4 to focus)"
- docker compose up
+ - dashboard:
+ root: <%= ENV["PWD"] %>/dashboard
+ panes:
+ - dashboard:
+ - echo "Dashboard (Ctrl+a 5 to focus, Ctrl+a r to restart)"
+ - pnpm run start
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
index 9c4ac667..aeae32bd 100644
--- a/backend/src/app.module.ts
+++ b/backend/src/app.module.ts
@@ -20,6 +20,8 @@ import { MailModule } from './mail/mail.module';
import { GitHubModule } from './github/github.module';
import { AppConfigService } from './config/config.service';
import { getDatabaseConfig } from './database.config';
+import { DashboardModule } from './dashboard/dashboard.module';
+import { InterceptorModule } from './interceptor/interceptor.module';
@Module({
imports: [
@@ -56,6 +58,8 @@ import { getDatabaseConfig } from './database.config';
MailModule,
TypeOrmModule.forFeature([User]),
GitHubModule,
+ DashboardModule,
+ InterceptorModule,
],
providers: [
AppResolver,
diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts
index 1c37afdd..8cc95ff6 100644
--- a/backend/src/auth/auth.module.ts
+++ b/backend/src/auth/auth.module.ts
@@ -13,6 +13,10 @@ import { MailModule } from 'src/mail/mail.module';
import { GoogleStrategy } from './oauth/GoogleStrategy';
import { GoogleController } from './google.controller';
import { AppConfigModule } from 'src/config/config.module';
+import { RoleResolver } from './role/role.resolver';
+import { MenuResolver } from './menu/menu.resolver';
+import { RoleService } from './role/role.service';
+import { MenuService } from './menu/menu.service';
@Module({
imports: [
@@ -29,8 +33,16 @@ import { AppConfigModule } from 'src/config/config.module';
JwtCacheModule,
MailModule,
],
+ providers: [
+ AuthService,
+ AuthResolver,
+ RoleResolver,
+ MenuResolver,
+ RoleService,
+ MenuService,
+ GoogleStrategy,
+ ],
+ exports: [AuthService, RoleService, MenuService, JwtModule],
controllers: [GoogleController],
- providers: [AuthService, AuthResolver, GoogleStrategy],
- exports: [AuthService, JwtModule],
})
export class AuthModule {}
diff --git a/backend/src/auth/menu/dto/create-menu.input.ts b/backend/src/auth/menu/dto/create-menu.input.ts
new file mode 100644
index 00000000..e6e5ffc1
--- /dev/null
+++ b/backend/src/auth/menu/dto/create-menu.input.ts
@@ -0,0 +1,25 @@
+import { Field, InputType } from '@nestjs/graphql';
+import { IsString, IsNotEmpty, IsOptional, IsArray } from 'class-validator';
+
+@InputType()
+export class CreateMenuInput {
+ @Field()
+ @IsString()
+ @IsNotEmpty()
+ name: string;
+
+ @Field()
+ @IsString()
+ @IsNotEmpty()
+ path: string;
+
+ @Field()
+ @IsString()
+ @IsNotEmpty()
+ permission: string;
+
+ @Field(() => [String], { nullable: true })
+ @IsArray()
+ @IsOptional()
+ roleIds?: string[];
+}
diff --git a/backend/src/auth/menu/dto/update-menu.input.ts b/backend/src/auth/menu/dto/update-menu.input.ts
new file mode 100644
index 00000000..f53c6d97
--- /dev/null
+++ b/backend/src/auth/menu/dto/update-menu.input.ts
@@ -0,0 +1,32 @@
+import { Field, ID, InputType } from '@nestjs/graphql';
+import { IsString, IsOptional, IsArray } from 'class-validator';
+
+@InputType()
+export class UpdateMenuInput {
+ @Field(() => ID)
+ id: string;
+
+ @Field({ nullable: true })
+ @IsString()
+ @IsOptional()
+ name?: string;
+
+ @Field({ nullable: true })
+ @IsString()
+ @IsOptional()
+ path?: string;
+
+ @Field({ nullable: true })
+ @IsString()
+ @IsOptional()
+ permission?: string;
+
+ @Field({ nullable: true })
+ @IsOptional()
+ isActive?: boolean;
+
+ @Field(() => [String], { nullable: true })
+ @IsArray()
+ @IsOptional()
+ roleIds?: string[];
+}
diff --git a/backend/src/auth/menu/menu.model.ts b/backend/src/auth/menu/menu.model.ts
index 6b2f39c6..9039ebe8 100644
--- a/backend/src/auth/menu/menu.model.ts
+++ b/backend/src/auth/menu/menu.model.ts
@@ -1,13 +1,19 @@
import { ObjectType, Field, ID } from '@nestjs/graphql';
-import { SystemBaseModel } from 'src/system-base-model/system-base.model';
-import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToMany,
+ CreateDateColumn,
+ UpdateDateColumn,
+} from 'typeorm';
import { Role } from '../role/role.model';
@Entity()
@ObjectType()
-export class Menu extends SystemBaseModel {
+export class Menu {
@Field(() => ID)
- @PrimaryGeneratedColumn()
+ @PrimaryGeneratedColumn('uuid')
id: string;
@Field()
@@ -22,6 +28,27 @@ export class Menu extends SystemBaseModel {
@Column()
permission: string;
+ @Field(() => String, { nullable: true })
+ @Column({ nullable: true })
+ description?: string;
+
+ @Field()
+ @Column({ default: true })
+ isActive: boolean;
+
+ @Field()
+ @Column({ default: false })
+ isDeleted: boolean;
+
+ @Field()
+ @CreateDateColumn()
+ createdAt: Date;
+
+ @Field()
+ @UpdateDateColumn()
+ updatedAt: Date;
+
+ @Field(() => [Role], { nullable: true })
@ManyToMany(() => Role, (role) => role.menus)
roles: Role[];
}
diff --git a/backend/src/auth/menu/menu.resolver.ts b/backend/src/auth/menu/menu.resolver.ts
new file mode 100644
index 00000000..f20fd62a
--- /dev/null
+++ b/backend/src/auth/menu/menu.resolver.ts
@@ -0,0 +1,65 @@
+import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql';
+import { UseGuards } from '@nestjs/common';
+import { MenuService } from './menu.service';
+import { Menu } from './menu.model';
+import { CreateMenuInput } from './dto/create-menu.input';
+import { UpdateMenuInput } from './dto/update-menu.input';
+import { RequireAuth } from '../../decorator/auth.decorator';
+import { JWTAuthGuard } from 'src/guard/jwt-auth.guard';
+
+@UseGuards(JWTAuthGuard)
+@Resolver(() => Menu)
+export class MenuResolver {
+ constructor(private readonly menuService: MenuService) {}
+
+ @Query(() => [Menu])
+ @RequireAuth({
+ roles: ['Admin'],
+ menuPath: '/menu/list',
+ })
+ async menus(): Promise