#NestJS how to use Sesion with RedisStore using Fastify?

68 messages · Page 1 of 1 (latest)

polar robin
#

I have installed NestJS v.10.3.9, Fastify v.4.28.0, @fastify/secure-session v7.5.1 and @fastify/redis v6.2.0 because I need pernament session to save tokens (id tokens/refresh tokens) to protect authentication against abuse and actions related to other users accounts.

I tried using the code below, but I kept getting a type error, which prevented me from compiling the Nest application.

#

Error:

src/main.ts:89:23 - error TS2339: Property 'redis' does not exist on type 'NestFastifyApplication<RawServerDefault>'.

89           client: app.redis,
wanton coyote
#

Does @fastify/redis modify the type definition for the fastify server?

polar robin
wanton coyote
#

Okay, it looks like @fastify/redis does modify the type of the underlying fastify server so you can grab the redis instance. That said, app is not the fastify server here, it's the Nest wrapper. So you need to use app.getHttpAdapter().getInstance().redis

polar robin
# wanton coyote Okay, it looks like `@fastify/redis` _does_ modify the type of the underlying fa...

I'm trying to test what you recommended to see if the data is actually being saved to Redis, but I'm getting the following error when trying to write.

Logs:

src/app.controller.ts:20:17 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.

20     session.set('test', req.ip);
                   ~~~~~~

Found 1 error(s).

Code:

import { Controller, Get, Req, Session } from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import * as secureSession from '@fastify/secure-session';

@Controller()
export class AppController {
  constructor() {}

  @Get()
  test(
    @Req() req: FastifyRequest,
    @Session() session: secureSession.Session,
  ): void {
    session.set('test', req.ip);
    console.log(session.get('test'));

    return;
  }
}
wanton coyote
#

Try secureSession.Session<Record<string, unknown>>?

polar robin
wanton coyote
#

I'm gonna guess auto-running typeorm migrations?

polar robin
#

The database logs are longer.

wanton coyote
#

Okay, so is there something about the logs you want me to see?

polar robin
#

But strangely, no errors are showing up, and I don't see any routes in the logs when starting the application.

wanton coyote
#

I'd guess a silent error is crashing the app. Tracking it down might be tough though

polar robin
#
import { ConfigService } from '@nestjs/config';
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { LogLevel } from './enums';

@Injectable()
export class LoggerService extends ConsoleLogger {
  private logLevel: boolean;
  private errorLevel: boolean;
  private warnLevel: boolean;
  private debugLevel: boolean;
  private verboseLevel: boolean;

  constructor(private readonly configService: ConfigService) {
    super();
    const logLevelStrings: string[] =
      this.configService.get<string[]>('app.logLevel');
    const logLevel: LogLevel[] = logLevelStrings.map(
      (level: string) => LogLevel[level.trim() as keyof typeof LogLevel],
    );
    logLevel.forEach((logLevel: LogLevel) => {
      this[`${logLevel}Level`] = true;
    });
  }

  log(message: any, context?: string): void {
    if (this.logLevel) super.log(message, context);
  }

  error(message: any, trace?: string, context?: string): void {
    if (this.errorLevel) super.error(message, trace, context);
  }

  warn(message: any, context?: string): void {
    if (this.warnLevel) super.warn(message, context);
  }

  debug(message: any, context?: string): void {
    if (this.debugLevel) super.debug(message, context);
  }

  verbose(message: any, context?: string): void {
    if (this.verboseLevel) super.verbose(message, context);
  }
}
wanton coyote
#

Why do you say it's with the custom logger?

polar robin
#

In main.ts, I have the LoggerService configured like this: app.useLogger(app.get(LoggerService));. When I commented this out, the application started up without any errors. I haven’t tested the session functionality yet.

polar robin
wanton coyote
#

And the logLevelStrings is coming in correctly?

polar robin
#

However, logLevelsString contains: ['warn', 'error'].
But logLevel constains: ['undefined', 'undefined'].

wanton coyote
#

What is the LogLevel enum?

#

Enums are super weird in Typescript, and often times can (and should) be avoided

polar robin
#

I suspect the issue is with the enum type becouse is pascalcase.

export enum LogLevel {
  Log = 'log',
  Error = 'error',
  Warn = 'warn',
  Debug = 'debug',
  Verbose = 'verbose',
}
wanton coyote
#

So wouldn't you need to do LogLevel['Warn'] for example? And you're currently doing LogLevel['warn']

#

Honestly, I'm trying to figure out why you want the enum in the first place. You could just use the ['warn', 'error'] array directly

polar robin
#

And I'm wondering how I could handle this differently without changing the type names to lowercase and without making the code unnecessarily long.

#

But as you can see, I'm using this to "disable" logging for a specific type.

wanton coyote
#

Walk me through what you want to happen

polar robin
#

And as you can see, if a value is false, error logging will not be performed.

log(message: any, context?: string): void {
    if (this.logLevel) super.log(message, context);
  }

  error(message: any, trace?: string, context?: string): void {
    if (this.errorLevel) super.error(message, trace, context);
  }
wanton coyote
#

Right. And passing the log levels ['warn', 'error'] would turn warn and error logging "on" by setting this.errorLevel = true and this.warnLevel = true, correct?

polar robin
#

yes

wanton coyote
#

So what's the need for the LogLevel enum?

polar robin
#

To be 100% certain about the type and the value it contains. Yes, I know this might seem a bit redundant or unnecessary, especially since in envSchema I have something like:

LOG_LEVEL: Joi.string()
    .pattern(new RegExp(commaDelimitedLogLevel))
    .default(`${LogLevel.Warn},${LogLevel.Error}`),

and in appConfig, I have:

logLevel: process.env.LOG_LEVEL.split(','),
wanton coyote
#

Okay. But doing the LogLevel[level] is what's causing the issue here

#

You could use a string union instead for the type

polar robin
#

Yes, because the log names are in PascalCase rather than lowercase, so it can't find the type.

wanton coyote
#

What do you mean they're in Pascal case?

polar robin
#

And I can’t use toLowerCase() on the type.

#

I mean that they have the first letter capitalized.

wanton coyote
#

"they" who?

polar robin
wanton coyote
#

Right, so why did you set the enum up that way?

polar robin
#

I don't know, but I realize it was a bit foolish. It seems to me that the best idea would be to do something like this.

declare global {
  interface String {
    toPascalCase(): string;
  }
}

String.prototype.toPascalCase = function (): string {
  const str = this as string;
  return str
    .replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, (match, index) =>
      index === 0 ? match.toUpperCase() : match.toUpperCase()
    )
    .replace(/\s+/g, '');
};

export {};
wanton coyote
#

Why are you worried about pascal case in the first place? What problem is that enum solving here?

polar robin
#

That I can add LogLevel[level.trim().toPascalCase() as keyof typeof LogLevel] here, and it should work.

wanton coyote
#

Okay, but why use pascal case keys in the enum? You could use lowercase keys and avoid all of this

polar robin
#

Because both values will have one capital letter.

wanton coyote
#

What both values?

polar robin
#

I just don’t want to change the names in the enum. I know it might sound silly, but I like everything to look perfect. And enums look nice when they start with a capital letter.

wanton coyote
#

Okay. If that's the case, then why not enforce that the strings passed via the LOG_LEVEL env value be the keys to the enum?

polar robin
#

LogLevel and level.trim().toPascalCase()

wanton coyote
#

Seems like you're making a lot more work for yourself, but if that's what you want to do then that's your call I guess

polar robin
#
export const commaDelimitedLogLevel = `^(${LogLevel.Log}|${LogLevel.Error}|${LogLevel.Warn}|${LogLevel.Debug}|${LogLevel.Verbose}){1}(,(${LogLevel.Log}|${LogLevel.Error}|${LogLevel.Warn}|${LogLevel.Debug}|${LogLevel.Verbose})){0,4}$`;
wanton coyote
#

LogLevel.Log is 'log', not 'Log'

#

Also, you could write that as {1,5} instead of two groups, and you'd get the same result, right?

sullen night
#

What's even the issue here? It's only the values of the enum that matter at runtime, not the keys, they can be whatever (just like all variable names)

polar robin
#

I shortened the code and did it a bit differently, and although the code executes completely, something in the Logger is still blocking it because the routes are not loading.

constructor(private readonly configService: ConfigService) {
  super();
  const logLevel: LogLevel[] =
    this.configService.get<LogLevel[]>('app.logLevel');
  logLevel.forEach((logLevel: LogLevel) => {
    this[`${logLevel}Level`] = true;
  });
}
wanton coyote
#

Are you certain they aren't loading?

#

Or are they not being logged because that level is disabled?

polar robin
#

You were right. I thought that the custom LogService only affected the logs that are manually added in the code, not the startup logs from Nest.

#

But now it's giving the following error:

[Nest] 33652  - 26.07.2024, 23:53:42   ERROR [ExceptionsHandler] Login sessions require session support. Did you forget to use `express-session` middleware?
Error: Login sessions require session support. Did you forget to use `express-session` middleware?
#

So, do I need to have the express-session package as well? That doesn't really make sense, does it? Since Fastify isn't based on Express, right?

wanton coyote
#

Are you trying to do sessions through passport?

polar robin
#

So, do I need to use @fastify/passport instead of the regular passport?

wanton coyote
#

Sessions and passport with fastify isn't necessarily supported in Nest. You'd probably be better to impliment that yourself

polar robin
#

So what do you suggest I do? Because I have everything configured in main.ts.