mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-18 18:51:32 +10:00
🚀 release v3.0.0
This commit is contained in:
23
server/src/users/dto/create-google-user.dto.ts
Normal file
23
server/src/users/dto/create-google-user.dto.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsString, Matches, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateGoogleUserDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@MinLength(3)
|
||||
@Transform(({ value }) => (value as string).toLowerCase().replace(/[ ]/gi, '-'))
|
||||
@Matches(/^[a-z0-9-]+$/, {
|
||||
message: 'username can contain only lowercase characters, numbers and hyphens',
|
||||
})
|
||||
username: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
provider: 'google';
|
||||
}
|
||||
30
server/src/users/dto/create-user.dto.ts
Normal file
30
server/src/users/dto/create-user.dto.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsEmail, IsNotEmpty, IsString, Matches, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@MinLength(3)
|
||||
@Transform(({ value }) => (value as string).toLowerCase().replace(/[ ]/gi, '-'))
|
||||
@Matches(/^[a-z0-9-]+$/, {
|
||||
message: 'username can contain only lowercase characters, numbers and hyphens',
|
||||
})
|
||||
username: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
provider: 'email';
|
||||
|
||||
@IsString()
|
||||
resetToken?: string;
|
||||
}
|
||||
5
server/src/users/dto/update-user.dto.ts
Normal file
5
server/src/users/dto/update-user.dto.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
43
server/src/users/entities/user.entity.ts
Normal file
43
server/src/users/entities/user.entity.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Exclude } from 'class-transformer';
|
||||
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
import { Resume } from '@/resume/entities/resume.entity';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
username: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Exclude()
|
||||
password?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Exclude()
|
||||
resetToken?: string;
|
||||
|
||||
@OneToMany(() => Resume, (resume) => resume.user)
|
||||
resumes: Resume[];
|
||||
|
||||
@Column()
|
||||
provider: 'email' | 'google';
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
constructor(partial: Partial<User>) {
|
||||
Object.assign(this, partial);
|
||||
}
|
||||
}
|
||||
14
server/src/users/users.module.ts
Normal file
14
server/src/users/users.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MailModule } from '@/mail/mail.module';
|
||||
|
||||
import { User } from './entities/user.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User]), MailModule],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
127
server/src/users/users.service.ts
Normal file
127
server/src/users/users.service.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { Connection, Repository } from 'typeorm';
|
||||
|
||||
import { MailService } from '@/mail/mail.service';
|
||||
|
||||
import { CreateGoogleUserDto } from './dto/create-google-user.dto';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
export const DELETION_TIME = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private userRepository: Repository<User>,
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
private mailService: MailService,
|
||||
private connection: Connection
|
||||
) {}
|
||||
|
||||
async findById(id: number): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ id });
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new HttpException('A user with this username/email does not exist.', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ email });
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new HttpException('A user with this email does not exist.', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
async findByIdentifier(identifier: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: [{ username: identifier }, { email: identifier }],
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new HttpException('A user with this username/email does not exist.', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
async findByResetToken(resetToken: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ resetToken });
|
||||
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new HttpException('The reset token provided may be invalid or expired.', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
async create(createUserDto: CreateUserDto | CreateGoogleUserDto): Promise<User> {
|
||||
const user = this.userRepository.create(createUserDto);
|
||||
|
||||
await this.userRepository.save(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id: number, updateUserDto: UpdateUserDto) {
|
||||
const user = await this.findById(id);
|
||||
const updatedUser = {
|
||||
...user,
|
||||
...updateUserDto,
|
||||
};
|
||||
|
||||
await this.userRepository.save(updatedUser);
|
||||
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
await this.userRepository.delete(id);
|
||||
}
|
||||
|
||||
async generateResetToken(email: string): Promise<void> {
|
||||
try {
|
||||
const user = await this.findByEmail(email);
|
||||
|
||||
const resetToken = randomBytes(32).toString('hex');
|
||||
const queryRunner = this.connection.createQueryRunner();
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
await this.userRepository.update(user.id, { resetToken: null });
|
||||
}, DELETION_TIME);
|
||||
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
await queryRunner.manager.update(User, user.id, { resetToken });
|
||||
|
||||
this.schedulerRegistry.addTimeout(`clear-resetToken-${user.id}`, timeout);
|
||||
|
||||
await this.mailService.sendForgotPasswordEmail(user, resetToken);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
throw new HttpException(
|
||||
'Please wait at least 30 minutes before resetting your password again.',
|
||||
HttpStatus.TOO_MANY_REQUESTS
|
||||
);
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
} catch {
|
||||
// pass through
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user