Skip to content

Commit

Permalink
feat: Add project frontend API with tests and fix backend project API
Browse files Browse the repository at this point in the history
This commit adds the project frontend API along with its corresponding tests. It also fixes an issue in the backend project API where the user ID should be a number.

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
Sma1lboy and autofix-ci[bot] committed Oct 27, 2024
1 parent 08ceb20 commit 0455c94
Show file tree
Hide file tree
Showing 16 changed files with 565 additions and 43 deletions.
23 changes: 13 additions & 10 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { User } from './user/user.model';
import { join } from 'path';
import { AuthModule } from './auth/auth.module';
import { ChatModule } from './chat/chat.module';
import { ProjectModule } from './project/project.module';
import { TokenModule } from './token/token.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { JwtCacheService } from './auth/jwt-cache.service';
import { ChatModule } from './chat/chat.module';
import { UserModule } from './user/user.module';
import { InitModule } from './init/init.module';
import { RolesGuard } from './guard/roles.guard';

Check warning on line 13 in backend/src/app.module.ts

View workflow job for this annotation

GitHub Actions / autofix

'RolesGuard' is defined but never used. Allowed unused vars must match /^_/u
import { MenuGuard } from './guard/menu.guard';

Check warning on line 14 in backend/src/app.module.ts

View workflow job for this annotation

GitHub Actions / autofix

'MenuGuard' is defined but never used. Allowed unused vars must match /^_/u
import { User } from './user/user.model';
import { AppResolver } from './app.resolver';

@Module({
imports: [
Expand All @@ -32,12 +33,14 @@ import { ChatModule } from './chat/chat.module';
logging: true,
entities: [__dirname + '/**/*.model{.ts,.js}'],
}),
InitModule,
UserModule,
AuthModule,
ProjectModule,
TokenModule,
ChatModule,
TypeOrmModule.forFeature([User]),
],
providers: [AppService],
providers: [AppResolver],
})
export class AppModule {}
12 changes: 12 additions & 0 deletions backend/src/app.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';

Check warning on line 1 in backend/src/app.resolver.ts

View workflow job for this annotation

GitHub Actions / autofix

'Injectable' is defined but never used. Allowed unused vars must match /^_/u
import { Query, Resolver } from '@nestjs/graphql';
import { RequireRoles } from './decorator/auth.decorator';

@Resolver()
export class AppResolver {
@Query(() => String)
@RequireRoles('Admin')
getHello(): string {
return 'Hello World!';
}
}
8 changes: 0 additions & 8 deletions backend/src/app.service.ts

This file was deleted.

247 changes: 246 additions & 1 deletion backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ConflictException,
Injectable,
Logger,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
Expand All @@ -11,9 +12,11 @@ import { compare, hash } from 'bcrypt';
import { LoginUserInput } from 'src/user/dto/login-user.input';
import { RegisterUserInput } from 'src/user/dto/register-user.input';
import { User } from 'src/user/user.model';
import { Repository } from 'typeorm';
import { In, Repository } from 'typeorm';
import { CheckTokenInput } from './dto/check-token.input';
import { JwtCacheService } from 'src/auth/jwt-cache.service';
import { Menu } from './menu/menu.model';
import { Role } from './role/role.model';

@Injectable()
export class AuthService {
Expand All @@ -23,6 +26,10 @@ export class AuthService {
private jwtService: JwtService,
private jwtCacheService: JwtCacheService,
private configService: ConfigService,
@InjectRepository(Menu)
private menuRepository: Repository<Menu>,
@InjectRepository(Role)
private roleRepository: Repository<Role>,
) {}

async register(registerUserInput: RegisterUserInput): Promise<User> {
Expand Down Expand Up @@ -95,4 +102,242 @@ export class AuthService {
this.jwtCacheService.removeToken(token);
return true;
}

async assignMenusToRole(roleId: string, menuIds: string[]): Promise<Role> {
// Find the role with existing menus
const role = await this.roleRepository.findOne({
where: { id: roleId },
relations: ['menus'],
});

if (!role) {
throw new NotFoundException(`Role with ID "${roleId}" not found`);
}

// Find all menus
const menus = await this.menuRepository.findByIds(menuIds);

if (menus.length !== menuIds.length) {
throw new NotFoundException('Some menus were not found');
}

if (!role.menus) {
role.menus = [];
}

const newMenus = menus.filter(
(menu) => !role.menus.some((existingMenu) => existingMenu.id === menu.id),
);

if (newMenus.length === 0) {
throw new ConflictException(
'All specified menus are already assigned to this role',
);
}

role.menus.push(...newMenus);

try {
await this.roleRepository.save(role);
Logger.log(
`${newMenus.length} menus assigned to role ${role.name} successfully`,
);

return await this.roleRepository.findOne({
where: { id: roleId },
relations: ['menus'],
});
} catch (error) {
Logger.error(`Failed to assign menus to role: ${error.message}`);
throw error;
}
}

async removeMenuFromRole(roleId: string, menuId: string): Promise<Role> {
const role = await this.roleRepository.findOne({
where: { id: roleId },
relations: ['menus'],
});

if (!role) {
throw new NotFoundException(`Role with ID "${roleId}" not found`);
}

const menuIndex = role.menus?.findIndex((menu) => menu.id === menuId);

if (menuIndex === -1) {
throw new NotFoundException(
`Menu with ID "${menuId}" not found in role "${role.name}"`,
);
}

role.menus.splice(menuIndex, 1);

try {
await this.roleRepository.save(role);
Logger.log(`Menu removed from role ${role.name} successfully`);

return await this.roleRepository.findOne({
where: { id: roleId },
relations: ['menus'],
});
} catch (error) {
Logger.error(`Failed to remove menu from role: ${error.message}`);
throw error;
}
}

async assignRoles(userId: string, roleIds: string[]): Promise<User> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});

if (!user) {
throw new NotFoundException(`User with ID "${userId}" not found`);
}

const roles = await this.roleRepository.findBy({ id: In(roleIds) });

if (roles.length !== roleIds.length) {
throw new NotFoundException('Some roles were not found');
}

if (!user.roles) {
user.roles = [];
}

const newRoles = roles.filter(
(role) => !user.roles.some((existingRole) => existingRole.id === role.id),
);

if (newRoles.length === 0) {
throw new ConflictException(
'All specified roles are already assigned to this user',
);
}

user.roles.push(...newRoles);

try {
await this.userRepository.save(user);
Logger.log(
`${newRoles.length} roles assigned to user ${user.username} successfully`,
);

return await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});
} catch (error) {
Logger.error(`Failed to assign roles to user: ${error.message}`);
throw error;
}
}

async removeRoleFromUser(userId: string, roleId: string): Promise<User> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});

if (!user) {
throw new NotFoundException(`User with ID "${userId}" not found`);
}

const roleIndex = user.roles?.findIndex((role) => role.id === roleId);

if (roleIndex === -1) {
throw new NotFoundException(
`Role with ID "${roleId}" not found in user's roles`,
);
}

user.roles.splice(roleIndex, 1);

try {
await this.userRepository.save(user);
Logger.log(`Role removed from user ${user.username} successfully`);

return await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});
} catch (error) {
Logger.error(`Failed to remove role from user: ${error.message}`);
throw error;
}
}

async getUserRolesAndMenus(userId: string): Promise<{
roles: Role[];
menus: Menu[];
}> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles', 'roles.menus'],
});

if (!user) {
throw new NotFoundException(`User with ID "${userId}" not found`);
}

const userRoles = user.roles || [];

// Get unique menus across all roles
const userMenus = Array.from(
new Map(
userRoles
.flatMap((role) => role.menus || [])
.map((menu) => [menu.id, menu]),
).values(),
);

return {
roles: userRoles,
menus: userMenus,
};
}

async getUserRoles(userId: string): Promise<{
roles: Role[];
}> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles'],
});

if (!user) {
throw new NotFoundException(`User with ID "${userId}" not found`);
}

return {
roles: user.roles || [],
};
}

async getUserMenus(userId: string): Promise<{
menus: Menu[];
}> {
const user = await this.userRepository.findOne({
where: { id: userId },
relations: ['roles', 'roles.menus'],
});

if (!user) {
throw new NotFoundException(`User with ID "${userId}" not found`);
}

const userMenus = Array.from(
new Map(
(user.roles || [])
.flatMap((role) => role.menus || [])
.map((menu) => [menu.id, menu]),
).values(),
);

return {
menus: userMenus,
};
}
}
35 changes: 25 additions & 10 deletions backend/src/auth/role/role.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ import {
} from 'typeorm';
import { Menu } from '../menu/menu.model';

@Entity()
@ObjectType()
export class Role extends SystemBaseModel {
@Field(() => ID)
@PrimaryGeneratedColumn()
@Entity()
export class Role {
@Field()
@PrimaryGeneratedColumn('uuid')
id: string;

@Field()
@Column()
@Column({ unique: true })
name: string;

@ManyToMany(() => User, (user) => user.roles)
users: User[];
@Field({ nullable: true })
@Column({ nullable: true })
description?: string;

@ManyToMany(() => Menu)
@Field(() => [Menu], { nullable: true })
@ManyToMany(() => Menu, { eager: true })
@JoinTable({
name: 'role_menus',
joinColumn: {
Expand All @@ -36,6 +38,19 @@ export class Role extends SystemBaseModel {
referencedColumnName: 'id',
},
})
@Field(() => [Menu])
menus: Menu[];
menus?: Menu[];

@ManyToMany(() => User, (user) => user.roles)
@JoinTable({
name: 'user_roles',
joinColumn: {
name: 'role_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'user_id',
referencedColumnName: 'id',
},
})
users?: User[];
}
3 changes: 3 additions & 0 deletions backend/src/common/enums/role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum DefaultRoles {
ADMIN = 'Admin',
}
Binary file modified backend/src/database.sqlite
Binary file not shown.
Loading

0 comments on commit 0455c94

Please sign in to comment.