From e849d7803cbd819517273ee00ec3ade92e02d63f Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sun, 15 Feb 2026 21:40:32 -0500 Subject: [PATCH] fix: JWT tokens expire instantly + double /api prefix in analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth service used flat env var names (JWT_SECRET, JWT_ACCESS_EXPIRY_SECONDS) but @nestjs/config nests them under jwt.* — configService.get() returned undefined, so expiresIn was 0 and tokens expired on issue (iat === exp) - JWT strategy had same bug for secretOrKey - AnalyticsView passed /api/analytics/... to useApi which already prepends /api, resulting in /api/api/analytics/... (404) Co-Authored-By: Claude Opus 4.6 --- backend-nest/src/modules/auth/auth.service.ts | 14 +++++++------- backend-nest/src/modules/auth/jwt.strategy.ts | 2 +- frontend/src/views/admin/AnalyticsView.vue | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend-nest/src/modules/auth/auth.service.ts b/backend-nest/src/modules/auth/auth.service.ts index 3155327..84864bb 100644 --- a/backend-nest/src/modules/auth/auth.service.ts +++ b/backend-nest/src/modules/auth/auth.service.ts @@ -146,7 +146,7 @@ export class AuthService { async refresh(refreshToken: string) { try { const payload = await this.jwtService.verifyAsync(refreshToken, { - secret: this.configService.get('JWT_SECRET'), + secret: this.configService.get('jwt.secret'), }); const user = await this.userRepository.findOne({ @@ -166,8 +166,8 @@ export class AuthService { is_super_admin: user.is_super_admin, }, { - secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get('JWT_ACCESS_EXPIRY_SECONDS', 3600), + secret: this.configService.get('jwt.secret'), + expiresIn: this.configService.get('jwt.accessExpirySeconds') || 900, }, ); @@ -322,13 +322,13 @@ export class AuthService { }; const accessToken = await this.jwtService.signAsync(payload, { - secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get('JWT_ACCESS_EXPIRY_SECONDS', 3600), + secret: this.configService.get('jwt.secret'), + expiresIn: this.configService.get('jwt.accessExpirySeconds') || 900, }); const refreshToken = await this.jwtService.signAsync(payload, { - secret: this.configService.get('JWT_SECRET'), - expiresIn: this.configService.get('JWT_REFRESH_EXPIRY_SECONDS', 604800), // 7 days default + secret: this.configService.get('jwt.secret'), + expiresIn: this.configService.get('jwt.refreshExpirySeconds') || 604800, }); return { diff --git a/backend-nest/src/modules/auth/jwt.strategy.ts b/backend-nest/src/modules/auth/jwt.strategy.ts index 5976f33..6ed968c 100644 --- a/backend-nest/src/modules/auth/jwt.strategy.ts +++ b/backend-nest/src/modules/auth/jwt.strategy.ts @@ -34,7 +34,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), + secretOrKey: configService.get('jwt.secret'), }); } diff --git a/frontend/src/views/admin/AnalyticsView.vue b/frontend/src/views/admin/AnalyticsView.vue index 6ea97dc..de7bbe0 100644 --- a/frontend/src/views/admin/AnalyticsView.vue +++ b/frontend/src/views/admin/AnalyticsView.vue @@ -33,8 +33,8 @@ const loadAnalytics = async () => { const hours = rangeToHours(timeRange.value) const [summaryRes, timeseriesRes] = await Promise.all([ - api.get(`/api/analytics/summary?range=${hours}`), - api.get(`/api/analytics/timeseries?range=${hours}&granularity=hourly`) + api.get(`/analytics/summary?range=${hours}`), + api.get(`/analytics/timeseries?range=${hours}&granularity=hourly`) ]) summary.value = summaryRes