mirror of
https://github.com/docmost/docmost.git
synced 2025-11-19 10:31:12 +10:00
feat(backend): forgot password (#250)
* feat(backend): forgot password * feat: apply feedback from code review * chore(auth): validate the minimum length of 'newPassword' * chore(auth): make token has an expiry of 1 hour * chore: rename all occurrences of 'code' to 'token' * chore(backend): provide value on nanoIdGen method
This commit is contained in:
@ -19,6 +19,7 @@ import { AuthUser } from '../../common/decorators/auth-user.decorator';
|
||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { ForgotPasswordDto } from './dto/forgot-password.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@ -33,6 +34,18 @@ export class AuthController {
|
||||
return this.authService.login(loginInput, req.raw.workspaceId);
|
||||
}
|
||||
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('forgot-password')
|
||||
async forgotPassword(
|
||||
@Req() req,
|
||||
@Body() forgotPasswordDto: ForgotPasswordDto,
|
||||
) {
|
||||
return this.authService.forgotPassword(
|
||||
forgotPasswordDto,
|
||||
req.raw.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/* @HttpCode(HttpStatus.OK)
|
||||
@Post('register')
|
||||
async register(@Req() req, @Body() createUserDto: CreateUserDto) {
|
||||
|
||||
16
apps/server/src/core/auth/dto/forgot-password.dto.ts
Normal file
16
apps/server/src/core/auth/dto/forgot-password.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { IsEmail, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class ForgotPasswordDto {
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
token: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
newPassword: string;
|
||||
}
|
||||
@ -11,10 +11,13 @@ import { TokensDto } from '../dto/tokens.dto';
|
||||
import { SignupService } from './signup.service';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||
import { comparePasswordHash, hashPassword } from '../../../common/helpers';
|
||||
import { comparePasswordHash, hashPassword, nanoIdGen } from '../../../common/helpers';
|
||||
import { ChangePasswordDto } from '../dto/change-password.dto';
|
||||
import { MailService } from '../../../integrations/mail/mail.service';
|
||||
import ChangePasswordEmail from '@docmost/transactional/emails/change-password-email';
|
||||
import { ForgotPasswordDto } from '../dto/forgot-password.dto';
|
||||
import ForgotPasswordEmail from '@docmost/transactional/emails/forgot-password-email';
|
||||
import { UserTokensRepo } from '@docmost/db/repos/user-tokens/user-tokens.repo';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -22,6 +25,7 @@ export class AuthService {
|
||||
private signupService: SignupService,
|
||||
private tokenService: TokenService,
|
||||
private userRepo: UserRepo,
|
||||
private userTokensRepo: UserTokensRepo,
|
||||
private mailService: MailService,
|
||||
) {}
|
||||
|
||||
@ -46,6 +50,94 @@ export class AuthService {
|
||||
return { tokens };
|
||||
}
|
||||
|
||||
async forgotPassword(
|
||||
forgotPasswordDto: ForgotPasswordDto,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const user = await this.userRepo.findByEmail(
|
||||
forgotPasswordDto.email,
|
||||
workspaceId,
|
||||
true,
|
||||
);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
forgotPasswordDto.token == null ||
|
||||
forgotPasswordDto.newPassword == null
|
||||
) {
|
||||
// Generate 5-character user token
|
||||
const code = nanoIdGen(5).toUpperCase();
|
||||
const hashedToken = await hashPassword(code);
|
||||
await this.userTokensRepo.insertUserToken({
|
||||
token: hashedToken,
|
||||
user_id: user.id,
|
||||
workspace_id: user.workspaceId,
|
||||
expires_at: new Date(new Date().getTime() + 3_600_000), // should expires in 1 hour
|
||||
type: "forgot-password",
|
||||
});
|
||||
|
||||
const emailTemplate = ForgotPasswordEmail({
|
||||
username: user.name,
|
||||
code: code,
|
||||
});
|
||||
await this.mailService.sendToQueue({
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
template: emailTemplate,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all user tokens that are not expired
|
||||
const userTokens = await this.userTokensRepo.findByUserId(
|
||||
user.id,
|
||||
user.workspaceId,
|
||||
"forgot-password"
|
||||
);
|
||||
// Limit to the last 3 token, so we have a total time window of 15 minutes
|
||||
const validUserTokens = userTokens
|
||||
.filter((token) => token.expires_at > new Date() && token.used_at == null)
|
||||
.slice(0, 3);
|
||||
|
||||
for (const token of validUserTokens) {
|
||||
const validated = await comparePasswordHash(
|
||||
forgotPasswordDto.token,
|
||||
token.token,
|
||||
);
|
||||
if (validated) {
|
||||
await Promise.all([
|
||||
this.userTokensRepo.deleteUserToken(user.id, user.workspaceId, "forgot-password"),
|
||||
this.userTokensRepo.deleteExpiredUserTokens(),
|
||||
]);
|
||||
|
||||
const newPasswordHash = await hashPassword(
|
||||
forgotPasswordDto.newPassword,
|
||||
);
|
||||
await this.userRepo.updateUser(
|
||||
{
|
||||
password: newPasswordHash,
|
||||
},
|
||||
user.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const emailTemplate = ChangePasswordEmail({ username: user.name });
|
||||
await this.mailService.sendToQueue({
|
||||
to: user.email,
|
||||
subject: 'Your password has been changed',
|
||||
template: emailTemplate,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestException('Incorrect code');
|
||||
}
|
||||
|
||||
async register(createUserDto: CreateUserDto, workspaceId: string) {
|
||||
const user = await this.signupService.signup(createUserDto, workspaceId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user