bugfix: .env.example files updated. some minor changes in jwt strategy to avoid failing when .env files does not contain needed keys.
This commit is contained in:
@@ -1,4 +1,36 @@
|
||||
// backend/src/config/configuration.ts
|
||||
|
||||
/** Matches values accepted by jsonwebtoken `expiresIn` (via ms), e.g. 7d, 15m, or plain seconds. */
|
||||
const JWT_TIMESPAN_PATTERN =
|
||||
/^\d+(\.\d+)?(ms|s|m|h|d|w|y)?$/i;
|
||||
|
||||
function assertJwtSecret(value: string, envKey: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`❌ Environment variable ${envKey} is required but not set`);
|
||||
}
|
||||
if (trimmed.length < 16) {
|
||||
throw new Error(`❌ ${envKey} must be at least 16 characters`);
|
||||
}
|
||||
if (/CHANGE_ME/i.test(trimmed)) {
|
||||
throw new Error(`❌ ${envKey} must be changed from the placeholder value`);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function assertJwtTimespan(value: string, envKey: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`❌ Environment variable ${envKey} is required but not set`);
|
||||
}
|
||||
if (!/^\d+$/.test(trimmed) && !JWT_TIMESPAN_PATTERN.test(trimmed)) {
|
||||
throw new Error(
|
||||
`❌ ${envKey}="${value}" is invalid. Use a duration like 7d, 15m, 30d, or a number of seconds.`,
|
||||
);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
port: number;
|
||||
database: {
|
||||
@@ -7,6 +39,8 @@ export interface Config {
|
||||
jwt: {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
refreshSecret: string;
|
||||
refreshExpiresIn: string;
|
||||
};
|
||||
throttle: {
|
||||
ttl: number;
|
||||
@@ -24,9 +58,10 @@ export default (): Config => {
|
||||
return value;
|
||||
};
|
||||
|
||||
// Helper for optional env vars with defaults
|
||||
// Helper for optional env vars with defaults (whitespace-only counts as unset)
|
||||
const getEnvVarWithDefault = (key: string, defaultValue: string): string => {
|
||||
return process.env[key] || defaultValue;
|
||||
const value = process.env[key]?.trim();
|
||||
return value ? value : defaultValue;
|
||||
};
|
||||
|
||||
const getEnvVarAsNumber = (key: string, defaultValue: number): number => {
|
||||
@@ -36,14 +71,30 @@ export default (): Config => {
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
};
|
||||
|
||||
const jwtSecret = assertJwtSecret(getEnvVar('JWT_SECRET'), 'JWT_SECRET');
|
||||
const jwtExpiresIn = assertJwtTimespan(
|
||||
getEnvVarWithDefault('JWT_EXPIRES_IN', '7d'),
|
||||
'JWT_EXPIRES_IN',
|
||||
);
|
||||
const jwtRefreshSecret = assertJwtSecret(
|
||||
getEnvVarWithDefault('JWT_REFRESH_SECRET', jwtSecret),
|
||||
'JWT_REFRESH_SECRET',
|
||||
);
|
||||
const jwtRefreshExpiresIn = assertJwtTimespan(
|
||||
getEnvVarWithDefault('JWT_REFRESH_EXPIRES_IN', '30d'),
|
||||
'JWT_REFRESH_EXPIRES_IN',
|
||||
);
|
||||
|
||||
return {
|
||||
port: getEnvVarAsNumber('PORT', 3000),
|
||||
database: {
|
||||
url: getEnvVar('DATABASE_URL'),
|
||||
},
|
||||
jwt: {
|
||||
secret: getEnvVar('JWT_SECRET'),
|
||||
expiresIn: getEnvVarWithDefault('JWT_EXPIRES_IN', '7d'),
|
||||
secret: jwtSecret,
|
||||
expiresIn: jwtExpiresIn,
|
||||
refreshSecret: jwtRefreshSecret,
|
||||
refreshExpiresIn: jwtRefreshExpiresIn,
|
||||
},
|
||||
throttle: {
|
||||
ttl: getEnvVarAsNumber('THROTTLE_TTL', 60),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
InternalServerErrorException
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import type { JwtSignOptions } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
@@ -53,6 +54,20 @@ export class AuthService {
|
||||
private configService: ConfigService,
|
||||
) { }
|
||||
|
||||
private accessJwtSignOptions(): JwtSignOptions {
|
||||
return {
|
||||
secret: this.configService.get<string>('jwt.secret')!,
|
||||
expiresIn: this.configService.get<string>('jwt.expiresIn')!,
|
||||
} as JwtSignOptions;
|
||||
}
|
||||
|
||||
private refreshJwtSignOptions(): JwtSignOptions {
|
||||
return {
|
||||
secret: this.configService.get<string>('jwt.refreshSecret')!,
|
||||
expiresIn: this.configService.get<string>('jwt.refreshExpiresIn')!,
|
||||
} as JwtSignOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user credentials (used by LocalStrategy)
|
||||
* @param email - User's email
|
||||
@@ -128,14 +143,8 @@ export class AuthService {
|
||||
};
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(accessPayload, {
|
||||
secret: this.configService.get('JWT_SECRET'),
|
||||
expiresIn: this.configService.get('JWT_EXPIRES_IN'),
|
||||
}),
|
||||
this.jwtService.signAsync(refreshPayload, {
|
||||
secret: this.configService.get('JWT_REFRESH_SECRET'),
|
||||
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN'),
|
||||
}),
|
||||
this.jwtService.signAsync(accessPayload, this.accessJwtSignOptions()),
|
||||
this.jwtService.signAsync(refreshPayload, this.refreshJwtSignOptions()),
|
||||
]);
|
||||
|
||||
// Store session in database
|
||||
@@ -398,7 +407,7 @@ export class AuthService {
|
||||
try {
|
||||
// Verify the refresh token
|
||||
const payload = await this.jwtService.verifyAsync(refreshToken, {
|
||||
secret: this.configService.get('jwt.refreshSecret'),
|
||||
secret: this.configService.get<string>('jwt.refreshSecret'),
|
||||
});
|
||||
|
||||
// Ensure this is a refresh token
|
||||
@@ -446,10 +455,10 @@ export class AuthService {
|
||||
type: 'access',
|
||||
};
|
||||
|
||||
const newAccessToken = await this.jwtService.signAsync(newAccessPayload, {
|
||||
secret: this.configService.get('jwt.secret'),
|
||||
expiresIn: this.configService.get('jwt.expiresIn'),
|
||||
});
|
||||
const newAccessToken = await this.jwtService.signAsync(
|
||||
newAccessPayload,
|
||||
this.accessJwtSignOptions(),
|
||||
);
|
||||
|
||||
// Update session with new access token
|
||||
await this.prisma.session.update({
|
||||
@@ -732,10 +741,7 @@ export class AuthService {
|
||||
};
|
||||
|
||||
// 3. Generate new token
|
||||
const accessToken = await this.jwtService.signAsync(payload, {
|
||||
secret: this.configService.get('JWT_SECRET'),
|
||||
expiresIn: this.configService.get('JWT_EXPIRES_IN'),
|
||||
});
|
||||
const accessToken = await this.jwtService.signAsync(payload, this.accessJwtSignOptions());
|
||||
|
||||
// 4. Format permissions
|
||||
const permissions = this.getMembershipPermissions(membership);
|
||||
|
||||
@@ -17,7 +17,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
return req?.cookies?.accessToken; // ✅ READ FROM COOKIE
|
||||
},
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: configService.get<string>('jwt.secret'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user