add password reset
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { AuthRegistry } from './auth-registry';
|
||||
import { generateSecureToken } from '../../../lib/crypto';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { isDefined, ResetAuth } from '@my-monorepo/common';
|
||||
|
||||
const RESET_TOKEN_VALIDITY_MINUTES = 15;
|
||||
|
||||
@@ -12,10 +13,19 @@ export class AuthCoreService {
|
||||
async addNewResetToken(
|
||||
userId: string,
|
||||
): Promise<{ token: string; user_id: string }> {
|
||||
return this.authRegistry.addResetToken({
|
||||
const res = await this.authRegistry.addResetToken({
|
||||
token: generateSecureToken(),
|
||||
userId,
|
||||
validUntil: dayjs().add(RESET_TOKEN_VALIDITY_MINUTES, 'minutes'),
|
||||
});
|
||||
if (!isDefined(res)) {
|
||||
throw new Error('cannot add reset token');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async checkAndInvalidateResetToken(resetAuth: ResetAuth): Promise<boolean> {
|
||||
const res = await this.authRegistry.checkAndInvalidateResetToken(resetAuth);
|
||||
return res > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ResetAuth } from '@my-monorepo/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../../../services/core/database/database.service';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { DatabaseService } from '../../../services/core/database/database.service';
|
||||
|
||||
export interface CreateResetToken {
|
||||
userId: string;
|
||||
@@ -17,7 +18,7 @@ export class AuthRegistry {
|
||||
|
||||
async addResetToken(
|
||||
resetAuth: CreateResetToken,
|
||||
): Promise<{ token: string; user_id: string }> {
|
||||
): Promise<{ token: string; user_id: string } | undefined> {
|
||||
return this.database
|
||||
.insertInto('reset_tokens')
|
||||
.values({
|
||||
@@ -26,6 +27,24 @@ export class AuthRegistry {
|
||||
valid_until: resetAuth.validUntil.toDate(),
|
||||
})
|
||||
.returning(['user_id', 'token'])
|
||||
.executeTakeFirstOrThrow();
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async checkAndInvalidateResetToken(resetAuth: ResetAuth): Promise<bigint> {
|
||||
const res = await this.database
|
||||
.deleteFrom('reset_tokens')
|
||||
.where('user_id', '=', resetAuth.userId)
|
||||
.where('token', '=', resetAuth.resetToken)
|
||||
.where('valid_until', '>=', dayjs().toDate())
|
||||
.executeTakeFirst();
|
||||
console.log(
|
||||
this.database
|
||||
.deleteFrom('reset_tokens')
|
||||
.where('user_id', '=', resetAuth.userId)
|
||||
.where('token', '=', resetAuth.resetToken)
|
||||
.where('valid_until', '>=', dayjs().toDate())
|
||||
.compile(),
|
||||
);
|
||||
return res.numDeletedRows;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { UsersCoreService } from '../../users/users-core/users-core.service';
|
||||
import { AuthCoreService } from '../auth-core/auth-core.service';
|
||||
import { ResetAuth } from '@my-monorepo/common';
|
||||
import { hashPassword } from '../../../lib/crypto';
|
||||
|
||||
@Injectable()
|
||||
export class AuthControllerService {
|
||||
constructor(private userCoreService: UsersCoreService) {}
|
||||
constructor(
|
||||
private userCoreService: UsersCoreService,
|
||||
private authCoreService: AuthCoreService,
|
||||
) {}
|
||||
|
||||
async verifyCredentials() {
|
||||
const isValidCredential = await this.userCoreService.existsEmailPassword();
|
||||
@@ -11,4 +21,16 @@ export class AuthControllerService {
|
||||
throw new UnauthorizedException('invalid credentials');
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(resetData: ResetAuth) {
|
||||
const isValidToken =
|
||||
await this.authCoreService.checkAndInvalidateResetToken(resetData);
|
||||
if (!isValidToken) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await this.userCoreService.setHashPassword(
|
||||
resetData.userId,
|
||||
await hashPassword(resetData.newPassword),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
NotImplementedException,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AuthControllerService } from './auth-controller.service';
|
||||
import { ZodValidationPipe } from '../../../lib/nestjs/zod-validation/zod-validation.pipe';
|
||||
import { ResetAuth, resetAuthParser } from '@my-monorepo/common';
|
||||
@@ -21,6 +16,6 @@ export class AuthController {
|
||||
async postReset(
|
||||
@Body(new ZodValidationPipe(resetAuthParser)) resetData: ResetAuth,
|
||||
): Promise<void> {
|
||||
throw new NotImplementedException();
|
||||
await this.authService.resetPassword(resetData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,17 @@ export class UsersCoreService {
|
||||
): Promise<Result<User, UserEmailAlreadyExistError>> {
|
||||
return await this.userRegistry.addUser(newUser);
|
||||
}
|
||||
|
||||
async setHashPassword(userId: string, password: string): Promise<boolean> {
|
||||
const updateRes = await this.userRegistry.updatePasswordHash(
|
||||
userId,
|
||||
password,
|
||||
);
|
||||
return updateRes.numUpdatedRows > 0;
|
||||
}
|
||||
|
||||
async getHashPassword(userId: string) {
|
||||
const res = await this.userRegistry.getPasswordHash(userId);
|
||||
return res?.hash ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,19 @@ export class UsersRegistry {
|
||||
deletedAt: user.deleted_at ?? undefined,
|
||||
});
|
||||
}
|
||||
async updatePasswordHash(userId: string, hashedPassword: string) {
|
||||
return this.database
|
||||
.updateTable('users')
|
||||
.set({ hash: hashedPassword })
|
||||
.where('users.id', '=', userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
async getPasswordHash(userId: string) {
|
||||
return this.database
|
||||
.selectFrom('users')
|
||||
.select('users.hash')
|
||||
.where('users.id', '=', userId)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as argon2 from 'argon2';
|
||||
|
||||
export function generateSecureToken(length = 32) {
|
||||
export function generateSecureToken(length = 32): string {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
export function hashPassword(password: string): Promise<string> {
|
||||
return argon2.hash(password);
|
||||
}
|
||||
|
||||
export function verifyPassword(
|
||||
passwordHash: string,
|
||||
clearTextPassword: string,
|
||||
): Promise<boolean> {
|
||||
return argon2.verify(passwordHash, clearTextPassword);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: reset password
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{BASE_URL}}/auth/reset
|
||||
body: json
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"userId": "9",
|
||||
"resetToken": "baeaff51ad72152db8b55006e95ca605a375f4d5143b764bbe0d24a6dca02baf",
|
||||
"newPassword": "Password1234"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
Reference in New Issue
Block a user