Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule';
import { TerminusModule } from '@nestjs/terminus';
import { BullModule } from '@nestjs/bull';
import { APP_INTERCEPTOR } from '@nestjs/core';

// Core & Database
import { PrismaModule } from './database/prisma/prisma.module';
Expand All @@ -14,7 +15,7 @@ import valuationConfig from './config/valuation.config';

// Logging
import { LoggingModule } from './common/logging/logging.module';
import { LoggingMiddleware } from './common/logging/logging.middleware';
import { LoggingInterceptor } from './common/logging/logging.interceptor';

// Redis
import { RedisModule } from './common/services/redis.module';
Expand Down Expand Up @@ -84,13 +85,16 @@ import { AuthRateLimitMiddleware } from './auth/middleware/auth.middleware';
ValuationModule,
DocumentsModule,
],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
// Correlation ID & structured logging for all routes
.apply(LoggingMiddleware)
.forRoutes('*')
// Auth rate limiting
.apply(AuthRateLimitMiddleware)
.forRoutes('/auth*');
Expand Down
123 changes: 83 additions & 40 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,34 @@
import * as bcrypt from 'bcrypt';
import { RedisService } from '../common/services/redis.service';
import { v4 as uuidv4 } from 'uuid';
import { StructuredLoggerService } from '../common/logging/logger.service';

@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService,
private configService: ConfigService,
private redisService: RedisService,
) {}
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
private readonly redisService: RedisService,
private readonly logger: StructuredLoggerService,
) {
this.logger.setContext('AuthService');
}

async register(createUserDto: CreateUserDto) {
const user = await this.userService.create(createUserDto);
await this.sendVerificationEmail(user.id, user.email);
return {
message: 'User registered successfully. Please check your email for verification.',
};
try {
const user = await this.userService.create(createUserDto);
await this.sendVerificationEmail(user.id, user.email);
this.logger.logAuth('User registration successful', { userId: user.id });
return {
message: 'User registered successfully. Please check your email for verification.',
};
} catch (error) {
this.logger.error('User registration failed', error.stack, {
email: createUserDto.email,
});
throw error;
}

Check warning on line 40 in src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / Test and Quality Checks

Unexpected any. Specify a different type
}

async login(credentials: {
Expand All @@ -36,38 +48,49 @@
}) {
let user: any;

if (credentials.email && credentials.password) {
user = await this.validateUserByEmail(
credentials.email,
credentials.password,
);
} else if (credentials.walletAddress) {
user = await this.validateUserByWallet(
credentials.walletAddress,
credentials.signature,
);
} else {
throw new BadRequestException(
'Email/password or wallet address/signature required',
);
}
try {
if (credentials.email && credentials.password) {
user = await this.validateUserByEmail(
credentials.email,
credentials.password,
);
} else if (credentials.walletAddress) {
user = await this.validateUserByWallet(
credentials.walletAddress,
credentials.signature,
);
} else {
throw new BadRequestException(
'Email/password or wallet address/signature required',
);
}

Check warning on line 66 in src/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / Test and Quality Checks

Unexpected any. Specify a different type

if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
if (!user) {
this.logger.warn('Invalid login attempt', { email: credentials.email });
throw new UnauthorizedException('Invalid credentials');
}

return this.generateTokens(user);
this.logger.logAuth('User login successful', { userId: user.id });
return this.generateTokens(user);
} catch (error) {
this.logger.error('User login failed', error.stack, {
email: credentials.email,
});
throw error;
}
}

async validateUserByEmail(email: string, password: string): Promise<any> {
const user = await this.userService.findByEmail(email);

if (!user || !user.password) {
this.logger.warn('Email validation failed: User not found', { email });
throw new UnauthorizedException('Invalid credentials');
}

const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
this.logger.warn('Email validation failed: Invalid password', { email });
throw new UnauthorizedException('Invalid credentials');
}

Expand All @@ -89,6 +112,7 @@
firstName: 'Web3',
lastName: 'User',
});
this.logger.logAuth('New Web3 user created', { walletAddress });
}

const { password: _, ...result } = user as any;
Expand All @@ -103,42 +127,54 @@

const user = await this.userService.findById(payload.sub);
if (!user) {
this.logger.warn('Refresh token validation failed: User not found', {
userId: payload.sub,
});
throw new UnauthorizedException('User not found');
}

const storedToken = await this.redisService.get(
`refresh_token:${payload.sub}`,
);
if (storedToken !== refreshToken) {
this.logger.warn('Refresh token validation failed: Invalid token', {
userId: payload.sub,
});
throw new UnauthorizedException('Invalid refresh token');
}

this.logger.logAuth('Token refreshed successfully', { userId: user.id });
return this.generateTokens(user);
} catch {
} catch (error) {
this.logger.error('Token refresh failed', error.stack);
throw new UnauthorizedException('Invalid refresh token');
}
}

async logout(userId: string) {
await this.redisService.del(`refresh_token:${userId}`);
this.logger.logAuth('User logged out successfully', { userId });
return { message: 'Logged out successfully' };
}

async forgotPassword(email: string) {
const user = await this.userService.findByEmail(email);
if (!user) {
this.logger.log('Forgot password request for non-existent user', { email });
return { message: 'If email exists, a reset link has been sent' };
}

const resetToken = uuidv4();
const resetTokenExpiry = Date.now() + 3600000;
const resetTokenExpiry = Date.now() + 3600000; // 1 hour

// Save reset token and expiry in Redis
await this.redisService.set(
`password_reset:${resetToken}`,
JSON.stringify({ userId: user.id, expiry: resetTokenExpiry }),
);

await this.sendPasswordResetEmail(user.email, resetToken);
this.logger.log('Password reset email sent', { email });
return { message: 'If email exists, a reset link has been sent' };
}

Expand All @@ -148,19 +184,22 @@
);

if (!resetData) {
this.logger.warn('Invalid or expired password reset token received');
throw new BadRequestException('Invalid or expired reset token');
}

const { userId, expiry } = JSON.parse(resetData);

if (Date.now() > expiry) {
await this.redisService.del(`password_reset:${resetToken}`);
this.logger.warn('Expired password reset token used', { userId });
throw new BadRequestException('Reset token has expired');
}

await this.userService.updatePassword(userId, newPassword);
await this.redisService.del(`password_reset:${resetToken}`);

this.logger.log('Password reset successfully', { userId });
return { message: 'Password reset successfully' };
}

Expand All @@ -170,13 +209,15 @@
);

if (!verificationData) {
this.logger.warn('Invalid or expired email verification token');
throw new BadRequestException('Invalid or expired verification token');
}

const { userId } = JSON.parse(verificationData);
await this.userService.verifyUser(userId);
await this.redisService.del(`email_verification:${token}`);

this.logger.log('Email verified successfully', { userId });
return { message: 'Email verified successfully' };
}

Expand All @@ -185,22 +226,18 @@

const accessToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
expiresIn: this.configService.get<string>(
'JWT_EXPIRES_IN',
'15m',
) as any,
expiresIn: this.configService.get<string>('JWT_EXPIRES_IN', '15m') as any,
});

const refreshToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: this.configService.get<string>(
'JWT_REFRESH_EXPIRES_IN',
'7d',
) as any,
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRES_IN', '7d') as any,
});

this.redisService.set(`refresh_token:${user.id}`, refreshToken);

this.logger.debug('Generated new tokens for user', { userId: user.id });

return {
access_token: accessToken,
refresh_token: refreshToken,
Expand All @@ -215,16 +252,22 @@

private async sendVerificationEmail(userId: string, email: string) {
const verificationToken = uuidv4();

// Save token in Redis
const expiry = Date.now() + 3600000; // 1 hour
await this.redisService.set(
`email_verification:${verificationToken}`,
JSON.stringify({ userId }),
JSON.stringify({ userId, expiry }),
);

this.logger.log(`Verification email sent to ${email}`, { userId });
console.log(
`Verification email sent to ${email} with token: ${verificationToken}`,
);
}

private async sendPasswordResetEmail(email: string, resetToken: string) {
this.logger.log(`Password reset email sent to ${email}`);
console.log(
`Password reset email sent to ${email} with token: ${resetToken}`,
);
Expand Down
33 changes: 33 additions & 0 deletions src/common/logging/correlation-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createNamespace, Namespace } from 'cls-hooked';

/**
* Manages correlation IDs for request tracking using async_hooks (via cls-hooked)
*/
export const CORRELATION_ID_KEY = 'correlationId';

// Create a namespace for correlation IDs
const ns: Namespace = createNamespace('propchain-request');

/**
* Get the correlation ID for the current request context
*/
export const getCorrelationId = (): string | undefined => {
return ns.get(CORRELATION_ID_KEY);
};

/**
* Run a function within a request context and set the correlation ID
*/
export const withCorrelationId = (fn: () => void, correlationId: string): void => {
ns.run(() => {
ns.set(CORRELATION_ID_KEY, correlationId);
fn();
});
};

/**
* Get the underlying namespace
*/
export const getNamespace = (): Namespace => {
return ns;
};
Loading
Loading