first commit

This commit is contained in:
Bastian Wagner
2026-07-31 21:02:47 +02:00
commit 6bea4f766a
512 changed files with 64459 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
import { Transform } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { Role } from '../../roles/entities/role.entity';
import {
IsEmail,
IsNotEmpty,
IsOptional,
MinLength,
Validate,
} from 'class-validator';
import { Status } from '../../statuses/entities/status.entity';
import { IsNotExist } from '../../utils/validators/is-not-exists.validator';
import { FileEntity } from '../../files/entities/file.entity';
import { IsExist } from '../../utils/validators/is-exists.validator';
export class CreateUserDto {
@ApiProperty({ example: 'test1@example.com' })
@Transform(({ value }) => value?.toLowerCase().trim())
@IsNotEmpty()
@Validate(IsNotExist, ['User'], {
message: 'emailAlreadyExists',
})
@IsEmail()
email: string | null;
@ApiProperty()
@MinLength(6)
password?: string;
provider?: string;
socialId?: string | null;
@ApiProperty({ example: 'John' })
@IsNotEmpty()
firstName: string | null;
@ApiProperty({ example: 'Doe' })
@IsNotEmpty()
lastName: string | null;
@ApiProperty({ type: () => FileEntity })
@IsOptional()
@Validate(IsExist, ['FileEntity', 'id'], {
message: 'imageNotExists',
})
photo?: FileEntity | null;
@ApiProperty({ type: Role })
@Validate(IsExist, ['Role', 'id'], {
message: 'roleNotExists',
})
role?: Role | null;
@ApiProperty({ type: Status })
@Validate(IsExist, ['Status', 'id'], {
message: 'statusNotExists',
})
status?: Status;
hash?: string | null;
linkPlayerId?: number | null;
}

View File

@@ -0,0 +1,62 @@
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
import { Transform } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { Role } from '../../roles/entities/role.entity';
import { IsEmail, IsOptional, MinLength, Validate } from 'class-validator';
import { Status } from '../../statuses/entities/status.entity';
import { IsNotExist } from '../../utils/validators/is-not-exists.validator';
import { FileEntity } from '../../files/entities/file.entity';
import { IsExist } from '../../utils/validators/is-exists.validator';
export class UpdateUserDto extends PartialType(CreateUserDto) {
@ApiProperty({ example: 'test1@example.com' })
@Transform(({ value }) => value?.toLowerCase().trim())
@IsOptional()
@Validate(IsNotExist, ['User'], {
message: 'emailAlreadyExists',
})
@IsEmail()
email?: string | null;
@ApiProperty()
@IsOptional()
@MinLength(6)
password?: string;
provider?: string;
socialId?: string | null;
@ApiProperty({ example: 'John' })
@IsOptional()
firstName?: string | null;
@ApiProperty({ example: 'Doe' })
@IsOptional()
lastName?: string | null;
@ApiProperty({ type: () => FileEntity })
@IsOptional()
@Validate(IsExist, ['FileEntity', 'id'], {
message: 'imageNotExists',
})
photo?: FileEntity | null;
@ApiProperty({ type: Role })
@IsOptional()
@Validate(IsExist, ['Role', 'id'], {
message: 'roleNotExists',
})
role?: Role | null;
@ApiProperty({ type: Status })
@IsOptional()
@Validate(IsExist, ['Status', 'id'], {
message: 'statusNotExists',
})
status?: Status;
hash?: string | null;
}

View File

@@ -0,0 +1,100 @@
import {
Column,
AfterLoad,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
BeforeInsert,
BeforeUpdate,
OneToMany,
} from 'typeorm';
import { Role } from '../../roles/entities/role.entity';
import { Status } from '../../statuses/entities/status.entity';
import { FileEntity } from '../../files/entities/file.entity';
import * as bcrypt from 'bcryptjs';
import { EntityHelper } from 'src/utils/entity-helper';
import { AuthProvidersEnum } from 'src/auth/auth-providers.enum';
import { Exclude, Expose } from 'class-transformer';
import { Player } from 'src/players/entities/player.entity';
@Entity()
export class User extends EntityHelper {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true, nullable: true })
email: string | null;
@Column({ nullable: true })
@Exclude({ toPlainOnly: true })
password: string;
@Exclude({ toPlainOnly: true })
public previousPassword: string;
@AfterLoad()
public loadPreviousPassword(): void {
this.previousPassword = this.password;
}
@BeforeInsert()
@BeforeUpdate()
async setPassword() {
if (this.previousPassword !== this.password && this.password) {
const salt = await bcrypt.genSalt();
this.password = await bcrypt.hash(this.password, salt);
}
}
@Column({ default: AuthProvidersEnum.email })
@Expose({ groups: ['exposeProvider'] })
provider: string;
@Index()
@Column({ nullable: true })
socialId: string | null;
@Index()
@Column({ nullable: true })
firstName: string | null;
@Index()
@Column({ nullable: true })
lastName: string | null;
@ManyToOne(() => FileEntity, {
eager: true,
})
photo?: FileEntity | null;
@ManyToOne(() => Role, {
eager: true,
})
role?: Role | null;
@ManyToOne(() => Status, {
eager: true,
})
status?: Status;
@OneToMany(() => Player, (player) => player.user)
players: Player[];
@Column({ nullable: true })
@Index()
@Exclude({ toPlainOnly: true })
hash: string | null;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date;
}

View File

@@ -0,0 +1,89 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
UseGuards,
Query,
DefaultValuePipe,
ParseIntPipe,
HttpStatus,
HttpCode,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Roles } from 'src/roles/roles.decorator';
import { RoleEnum } from 'src/roles/roles.enum';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from 'src/roles/roles.guard';
import { infinityPagination } from 'src/utils/infinity-pagination';
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'), RolesGuard)
@ApiTags('Users')
@Controller({
path: 'users',
version: '1',
})
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Roles([RoleEnum.admin])
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createProfileDto: CreateUserDto) {
return this.usersService.create(createProfileDto);
}
@Roles([RoleEnum.admin])
@Get()
@HttpCode(HttpStatus.OK)
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
if (limit > 50) {
limit = 50;
}
return infinityPagination(
await this.usersService.findManyWithPagination({
page,
limit,
}),
{ page, limit },
);
}
@Roles([RoleEnum.admin])
@Get(':id')
@HttpCode(HttpStatus.OK)
findOne(@Param('id') id: string) {
return this.usersService.findOne({ id: +id });
}
@Roles([RoleEnum.user, RoleEnum.admin])
@Get(':id/teams')
@HttpCode(HttpStatus.OK)
findTeamsOfPlayer(@Param('id') id: string) {
return this.usersService.findTeams({ id: +id });
}
@Roles([RoleEnum.admin])
@Patch(':id')
@HttpCode(HttpStatus.OK)
update(@Param('id') id: number, @Body() updateProfileDto: UpdateUserDto) {
return this.usersService.update(id, updateProfileDto);
}
@Roles([RoleEnum.admin])
@Delete(':id')
remove(@Param('id') id: number) {
return this.usersService.softDelete(id);
}
}

View File

@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { IsExist } from 'src/utils/validators/is-exists.validator';
import { IsNotExist } from 'src/utils/validators/is-not-exists.validator';
import { Team } from 'src/teams/entities/team.entity';
import { Player } from 'src/players/entities/player.entity';
@Module({
imports: [TypeOrmModule.forFeature([User, Team, Player])],
controllers: [UsersController],
providers: [IsExist, IsNotExist, UsersService],
exports: [UsersService],
})
export class UsersModule {}

View File

@@ -0,0 +1,80 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { EntityCondition } from 'src/utils/types/entity-condition.type';
import { IPaginationOptions } from 'src/utils/types/pagination-options';
import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(Player)
private playersRepository: Repository<Player>,
) {}
create(createProfileDto: CreateUserDto) {
return this.usersRepository.save(
this.usersRepository.create(createProfileDto),
);
}
findManyWithPagination(paginationOptions: IPaginationOptions) {
return this.usersRepository.find({
skip: (paginationOptions.page - 1) * paginationOptions.limit,
take: paginationOptions.limit,
});
}
findOne(fields: EntityCondition<User>) {
return this.usersRepository.findOne({
where: fields,
});
}
update(id: number, updateProfileDto: UpdateUserDto) {
return this.usersRepository.save(
this.usersRepository.create({
id,
...updateProfileDto,
}),
);
}
async softDelete(id: number): Promise<void> {
await this.usersRepository.softDelete(id);
}
async findTeams(fields: EntityCondition<User>) {
const user = await this.findOne(fields);
if (!user) {
return [];
}
const players = await this.playersRepository.find({
where: {
user: {
id: user.id,
},
},
relations: ['team'],
});
return players;
}
async linkPlayerToUserId(user: User, playerId: number): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const player = await this.playersRepository.findOneByOrFail({
id: playerId,
});
player.user = user;
await this.playersRepository.save(player);
return resolve(true);
});
}
}