auth-guard.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
  2. import { Reflector } from '@nestjs/core';
  3. import { Permission } from '@vendure/common/lib/generated-types';
  4. import { Request, Response } from 'express';
  5. import { GraphQLResolveInfo } from 'graphql';
  6. import { REQUEST_CONTEXT_KEY } from '../../common/constants';
  7. import { ForbiddenError } from '../../common/error/errors';
  8. import { ConfigService } from '../../config/config.service';
  9. import { LogLevel } from '../../config/logger/vendure-logger';
  10. import { CachedSession } from '../../config/session-cache/session-cache-strategy';
  11. import { Customer } from '../../entity/customer/customer.entity';
  12. import { RequestContextService } from '../../service/helpers/request-context/request-context.service';
  13. import { ChannelService } from '../../service/services/channel.service';
  14. import { CustomerService } from '../../service/services/customer.service';
  15. import { SessionService } from '../../service/services/session.service';
  16. import { extractSessionToken } from '../common/extract-session-token';
  17. import { parseContext } from '../common/parse-context';
  18. import { RequestContext } from '../common/request-context';
  19. import { setSessionToken } from '../common/set-session-token';
  20. import { PERMISSIONS_METADATA_KEY } from '../decorators/allow.decorator';
  21. /**
  22. * @description
  23. * A guard which:
  24. *
  25. * 1. checks for the existence of a valid session token in the request and if found,
  26. * attaches the current User entity to the request.
  27. * 2. enforces any permissions required by the target handler (resolver, field resolver or route),
  28. * and throws a ForbiddenError if those permissions are not present.
  29. */
  30. @Injectable()
  31. export class AuthGuard implements CanActivate {
  32. strategy: any;
  33. constructor(
  34. private reflector: Reflector,
  35. private configService: ConfigService,
  36. private requestContextService: RequestContextService,
  37. private sessionService: SessionService,
  38. private customerService: CustomerService,
  39. private channelService: ChannelService,
  40. ) {}
  41. async canActivate(context: ExecutionContext): Promise<boolean> {
  42. const { req, res, info } = parseContext(context);
  43. const isFieldResolver = this.isFieldResolver(info);
  44. const permissions = this.reflector.get<Permission[]>(PERMISSIONS_METADATA_KEY, context.getHandler());
  45. if (isFieldResolver && !permissions) {
  46. return true;
  47. }
  48. const authDisabled = this.configService.authOptions.disableAuth;
  49. const isPublic = !!permissions && permissions.includes(Permission.Public);
  50. const hasOwnerPermission = !!permissions && permissions.includes(Permission.Owner);
  51. let requestContext: RequestContext;
  52. if (isFieldResolver) {
  53. requestContext = (req as any)[REQUEST_CONTEXT_KEY];
  54. } else {
  55. const session = await this.getSession(req, res, hasOwnerPermission);
  56. requestContext = await this.requestContextService.fromRequest(req, info, permissions, session);
  57. const requestContextShouldBeReinitialized = await this.setActiveChannel(requestContext, session);
  58. if (requestContextShouldBeReinitialized) {
  59. requestContext = await this.requestContextService.fromRequest(
  60. req,
  61. info,
  62. permissions,
  63. session,
  64. );
  65. }
  66. (req as any)[REQUEST_CONTEXT_KEY] = requestContext;
  67. }
  68. if (authDisabled || !permissions || isPublic) {
  69. return true;
  70. } else {
  71. const canActivate =
  72. requestContext.userHasPermissions(permissions) || requestContext.authorizedAsOwnerOnly;
  73. if (!canActivate) {
  74. throw new ForbiddenError(LogLevel.Verbose);
  75. } else {
  76. return canActivate;
  77. }
  78. }
  79. }
  80. private async setActiveChannel(
  81. requestContext: RequestContext,
  82. session?: CachedSession,
  83. ): Promise<boolean> {
  84. if (!session) {
  85. return false;
  86. }
  87. // In case the session does not have an activeChannelId or the activeChannelId
  88. // does not correspond to the current channel, the activeChannelId on the session is set
  89. const activeChannelShouldBeSet =
  90. !session.activeChannelId || session.activeChannelId !== requestContext.channelId;
  91. if (activeChannelShouldBeSet) {
  92. await this.sessionService.setActiveChannel(session, requestContext.channel);
  93. if (requestContext.activeUserId) {
  94. const customer = await this.customerService.findOneByUserId(
  95. requestContext,
  96. requestContext.activeUserId,
  97. false,
  98. );
  99. // To avoid assigning the customer to the active channel on every request,
  100. // it is only done on the first request and whenever the channel changes
  101. if (customer) {
  102. try {
  103. await this.channelService.assignToChannels(requestContext, Customer, customer.id, [
  104. requestContext.channelId,
  105. ]);
  106. } catch (e: any) {
  107. const isDuplicateError =
  108. e.code === 'ER_DUP_ENTRY' /* mySQL/MariaDB */ ||
  109. e.code === '23505'; /* postgres */
  110. if (isDuplicateError) {
  111. // For a duplicate error, this means that concurrent requests have resulted in attempting to
  112. // assign the Customer to the channel more than once. In this case we can safely ignore the
  113. // error as the Customer was successfully assigned in the earlier call.
  114. // See https://github.com/vendure-ecommerce/vendure/issues/834
  115. } else {
  116. throw e;
  117. }
  118. }
  119. }
  120. }
  121. return true;
  122. }
  123. return false;
  124. }
  125. private async getSession(
  126. req: Request,
  127. res: Response,
  128. hasOwnerPermission: boolean,
  129. ): Promise<CachedSession | undefined> {
  130. const sessionToken = extractSessionToken(req, this.configService.authOptions.tokenMethod);
  131. let serializedSession: CachedSession | undefined;
  132. if (sessionToken) {
  133. serializedSession = await this.sessionService.getSessionFromToken(sessionToken);
  134. if (serializedSession) {
  135. return serializedSession;
  136. }
  137. // if there is a token but it cannot be validated to a Session,
  138. // then the token is no longer valid and should be unset.
  139. setSessionToken({
  140. req,
  141. res,
  142. authOptions: this.configService.authOptions,
  143. rememberMe: false,
  144. sessionToken: '',
  145. });
  146. }
  147. if (hasOwnerPermission && !serializedSession) {
  148. serializedSession = await this.sessionService.createAnonymousSession();
  149. setSessionToken({
  150. sessionToken: serializedSession.token,
  151. rememberMe: true,
  152. authOptions: this.configService.authOptions,
  153. req,
  154. res,
  155. });
  156. }
  157. return serializedSession;
  158. }
  159. /**
  160. * Returns true is this guard is being called on a FieldResolver, i.e. not a top-level
  161. * Query or Mutation resolver.
  162. */
  163. private isFieldResolver(info?: GraphQLResolveInfo): boolean {
  164. if (!info) {
  165. return false;
  166. }
  167. const parentType = info?.parentType?.name;
  168. return parentType !== 'Query' && parentType !== 'Mutation' && parentType !== 'Subscription';
  169. }
  170. }