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:
2026-05-16 22:40:42 +03:30
parent 105175ff4d
commit 627219df58
11 changed files with 209 additions and 39 deletions

View File

@@ -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),

View File

@@ -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);

View File

@@ -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'),
});
}