|
|
@@ -1,10 +1,11 @@
|
|
|
-import { Injectable } from '@nestjs/common';
|
|
|
+import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
|
|
import { ID } from '@vendure/common/lib/shared-types';
|
|
|
import crypto from 'crypto';
|
|
|
import ms from 'ms';
|
|
|
-import { EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
|
+import { Brackets, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
|
|
|
|
import { RequestContext } from '../../api/common/request-context';
|
|
|
+import { Logger } from '../../config';
|
|
|
import { ConfigService } from '../../config/config.service';
|
|
|
import { CachedSession, SessionCacheStrategy } from '../../config/session-cache/session-cache-strategy';
|
|
|
import { TransactionalConnection } from '../../connection/transactional-connection';
|
|
|
@@ -15,10 +16,12 @@ import { AnonymousSession } from '../../entity/session/anonymous-session.entity'
|
|
|
import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
|
|
|
import { Session } from '../../entity/session/session.entity';
|
|
|
import { User } from '../../entity/user/user.entity';
|
|
|
+import { JobQueue } from '../../job-queue/job-queue';
|
|
|
+import { JobQueueService } from '../../job-queue/job-queue.service';
|
|
|
+import { RequestContextService } from '../helpers/request-context/request-context.service';
|
|
|
import { getUserChannelsPermissions } from '../helpers/utils/get-user-channels-permissions';
|
|
|
|
|
|
import { OrderService } from './order.service';
|
|
|
-
|
|
|
/**
|
|
|
* @description
|
|
|
* Contains methods relating to {@link Session} entities.
|
|
|
@@ -26,8 +29,9 @@ import { OrderService } from './order.service';
|
|
|
* @docsCategory services
|
|
|
*/
|
|
|
@Injectable()
|
|
|
-export class SessionService implements EntitySubscriberInterface {
|
|
|
+export class SessionService implements EntitySubscriberInterface, OnApplicationBootstrap {
|
|
|
private sessionCacheStrategy: SessionCacheStrategy;
|
|
|
+ private cleanSessionsJobQueue: JobQueue<{ batchSize: number }>;
|
|
|
private readonly sessionDurationInMs: number;
|
|
|
private readonly sessionCacheTimeoutMs = 50;
|
|
|
|
|
|
@@ -35,6 +39,8 @@ export class SessionService implements EntitySubscriberInterface {
|
|
|
private connection: TransactionalConnection,
|
|
|
private configService: ConfigService,
|
|
|
private orderService: OrderService,
|
|
|
+ private jobQueueService: JobQueueService,
|
|
|
+ private requestContextService: RequestContextService,
|
|
|
) {
|
|
|
this.sessionCacheStrategy = this.configService.authOptions.sessionCacheStrategy;
|
|
|
|
|
|
@@ -47,6 +53,22 @@ export class SessionService implements EntitySubscriberInterface {
|
|
|
this.connection.rawConnection.subscribers.push(this);
|
|
|
}
|
|
|
|
|
|
+ async onApplicationBootstrap() {
|
|
|
+ this.cleanSessionsJobQueue = await this.jobQueueService.createQueue({
|
|
|
+ name: 'clean-sessions',
|
|
|
+ process: async job => {
|
|
|
+ const ctx = await this.requestContextService.create({
|
|
|
+ apiType: 'admin',
|
|
|
+ });
|
|
|
+ const result = await this.cleanExpiredSessions(ctx, job.data.batchSize);
|
|
|
+ return {
|
|
|
+ batchSize: job.data.batchSize,
|
|
|
+ sessionsRemoved: result,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/** @internal */
|
|
|
async afterInsert(event: InsertEvent<any>): Promise<any> {
|
|
|
await this.clearSessionCacheOnDataChange(event);
|
|
|
@@ -298,6 +320,42 @@ export class SessionService implements EntitySubscriberInterface {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @description
|
|
|
+ * Triggers the clean sessions job.
|
|
|
+ */
|
|
|
+ async triggerCleanSessionsJob(batchSize: number) {
|
|
|
+ await this.cleanSessionsJobQueue.add({ batchSize });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @description
|
|
|
+ * Cleans expired sessions from the database & the session cache.
|
|
|
+ */
|
|
|
+ async cleanExpiredSessions(ctx: RequestContext, batchSize: number) {
|
|
|
+ const sessions = await this.connection
|
|
|
+ .getRepository(ctx, Session)
|
|
|
+ .createQueryBuilder('session')
|
|
|
+ .where('session.expires < :now', { now: new Date() })
|
|
|
+ .orWhere(
|
|
|
+ new Brackets(qb1 => {
|
|
|
+ qb1.where('session.userId IS NULL')
|
|
|
+ .andWhere('session.activeOrderId IS NULL')
|
|
|
+ .andWhere('session.updatedAt < :updatedAt', {
|
|
|
+ updatedAt: new Date(Date.now() - ms('7d')),
|
|
|
+ });
|
|
|
+ }),
|
|
|
+ )
|
|
|
+ .take(batchSize)
|
|
|
+ .getMany();
|
|
|
+ Logger.verbose(`Cleaning ${sessions.length} expired sessions`);
|
|
|
+ await this.connection.getRepository(ctx, Session).remove(sessions);
|
|
|
+ for (const session of sessions) {
|
|
|
+ await this.withTimeout(this.sessionCacheStrategy.delete(session.token));
|
|
|
+ }
|
|
|
+ Logger.verbose(`Cleaned ${sessions.length} expired sessions`);
|
|
|
+ return sessions.length;
|
|
|
+ }
|
|
|
/**
|
|
|
* If we are over half way to the current session's expiry date, then we update it.
|
|
|
*
|