request-context.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import { LanguageCode, Permission } from '@vendure/common/lib/generated-types';
  2. import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
  3. import { isObject } from '@vendure/common/lib/shared-utils';
  4. import { Request } from 'express';
  5. import { TFunction } from 'i18next';
  6. import { idsAreEqual } from '../../common/utils';
  7. import { CachedSession } from '../../config/session-cache/session-cache-strategy';
  8. import { Channel } from '../../entity/channel/channel.entity';
  9. import { ApiType } from './get-api-type';
  10. export type SerializedRequestContext = {
  11. _req?: any;
  12. _session: JsonCompatible<Required<CachedSession>>;
  13. _apiType: ApiType;
  14. _channel: JsonCompatible<Channel>;
  15. _languageCode: LanguageCode;
  16. _isAuthorized: boolean;
  17. _authorizedAsOwnerOnly: boolean;
  18. };
  19. /**
  20. * @description
  21. * The RequestContext holds information relevant to the current request, which may be
  22. * required at various points of the stack.
  23. *
  24. * It is a good practice to inject the RequestContext (using the {@link Ctx} decorator) into
  25. * _all_ resolvers & REST handlers, and then pass it through to the service layer.
  26. *
  27. * This allows the service layer to access information about the current user, the active language,
  28. * the active Channel, and so on. In addition, the {@link TransactionalConnection} relies on the
  29. * presence of the RequestContext object in order to correctly handle per-request database transactions.
  30. *
  31. * @example
  32. * ```TypeScript
  33. * \@Query()
  34. * myQuery(\@Ctx() ctx: RequestContext) {
  35. * return this.myService.getData(ctx);
  36. * }
  37. * ```
  38. * @docsCategory request
  39. */
  40. export class RequestContext {
  41. private readonly _languageCode: LanguageCode;
  42. private readonly _channel: Channel;
  43. private readonly _session?: CachedSession;
  44. private readonly _isAuthorized: boolean;
  45. private readonly _authorizedAsOwnerOnly: boolean;
  46. private readonly _translationFn: TFunction;
  47. private readonly _apiType: ApiType;
  48. private readonly _req?: Request;
  49. /**
  50. * @internal
  51. */
  52. constructor(options: {
  53. req?: Request;
  54. apiType: ApiType;
  55. channel: Channel;
  56. session?: CachedSession;
  57. languageCode?: LanguageCode;
  58. isAuthorized: boolean;
  59. authorizedAsOwnerOnly: boolean;
  60. translationFn?: TFunction;
  61. }) {
  62. const { req, apiType, channel, session, languageCode, translationFn } = options;
  63. this._req = req;
  64. this._apiType = apiType;
  65. this._channel = channel;
  66. this._session = session;
  67. this._languageCode = languageCode || (channel && channel.defaultLanguageCode);
  68. this._isAuthorized = options.isAuthorized;
  69. this._authorizedAsOwnerOnly = options.authorizedAsOwnerOnly;
  70. this._translationFn = translationFn || (((key: string) => key) as any);
  71. }
  72. /**
  73. * @description
  74. * Creates an "empty" RequestContext object. This is only intended to be used
  75. * when a service method must be called outside the normal request-response
  76. * cycle, e.g. when programmatically populating data. Usually a better alternative
  77. * is to use the {@link RequestContextService} `create()` method, which allows more control
  78. * over the resulting RequestContext object.
  79. */
  80. static empty(): RequestContext {
  81. return new RequestContext({
  82. apiType: 'admin',
  83. authorizedAsOwnerOnly: false,
  84. channel: new Channel(),
  85. isAuthorized: true,
  86. });
  87. }
  88. /**
  89. * @description
  90. * Creates a new RequestContext object from a serialized object created by the
  91. * `serialize()` method.
  92. */
  93. static deserialize(ctxObject: SerializedRequestContext): RequestContext {
  94. return new RequestContext({
  95. req: ctxObject._req as any,
  96. apiType: ctxObject._apiType,
  97. channel: new Channel(ctxObject._channel),
  98. session: {
  99. ...ctxObject._session,
  100. expires: ctxObject._session?.expires && new Date(ctxObject._session.expires),
  101. },
  102. languageCode: ctxObject._languageCode,
  103. isAuthorized: ctxObject._isAuthorized,
  104. authorizedAsOwnerOnly: ctxObject._authorizedAsOwnerOnly,
  105. });
  106. }
  107. /**
  108. * @description
  109. * Returns `true` if there is an active Session & User associated with this request,
  110. * and that User has the specified permissions on the active Channel.
  111. */
  112. userHasPermissions(permissions: Permission[]): boolean {
  113. const user = this.session?.user;
  114. if (!user || !this.channelId) {
  115. return false;
  116. }
  117. const permissionsOnChannel = user.channelPermissions.find(c => idsAreEqual(c.id, this.channelId));
  118. if (permissionsOnChannel) {
  119. return this.arraysIntersect(permissionsOnChannel.permissions, permissions);
  120. }
  121. return false;
  122. }
  123. /**
  124. * @description
  125. * Serializes the RequestContext object into a JSON-compatible simple object.
  126. * This is useful when you need to send a RequestContext object to another
  127. * process, e.g. to pass it to the Job Queue via the {@link JobQueueService}.
  128. */
  129. serialize(): SerializedRequestContext {
  130. const serializableThis: any = Object.assign({}, this);
  131. if (this._req) {
  132. serializableThis._req = this.shallowCloneRequestObject(this._req);
  133. }
  134. return JSON.parse(JSON.stringify(serializableThis));
  135. }
  136. /**
  137. * @description
  138. * Creates a shallow copy of the RequestContext instance. This means that
  139. * mutations to the copy itself will not affect the original, but deep mutations
  140. * (e.g. copy.channel.code = 'new') *will* also affect the original.
  141. */
  142. copy(): RequestContext {
  143. return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
  144. }
  145. /**
  146. * @description
  147. * The raw Express request object.
  148. */
  149. get req(): Request | undefined {
  150. return this._req;
  151. }
  152. /**
  153. * @description
  154. * Signals which API this request was received by, e.g. `admin` or `shop`.
  155. */
  156. get apiType(): ApiType {
  157. return this._apiType;
  158. }
  159. /**
  160. * @description
  161. * The active {@link Channel} of this request.
  162. */
  163. get channel(): Channel {
  164. return this._channel;
  165. }
  166. get channelId(): ID {
  167. return this._channel.id;
  168. }
  169. get languageCode(): LanguageCode {
  170. return this._languageCode;
  171. }
  172. get session(): CachedSession | undefined {
  173. return this._session;
  174. }
  175. get activeUserId(): ID | undefined {
  176. return this.session?.user?.id;
  177. }
  178. /**
  179. * @description
  180. * True if the current session is authorized to access the current resolver method.
  181. *
  182. * @deprecated Use `userHasPermissions()` method instead.
  183. */
  184. get isAuthorized(): boolean {
  185. return this._isAuthorized;
  186. }
  187. /**
  188. * @description
  189. * True if the current anonymous session is only authorized to operate on entities that
  190. * are owned by the current session.
  191. */
  192. get authorizedAsOwnerOnly(): boolean {
  193. return this._authorizedAsOwnerOnly;
  194. }
  195. /**
  196. * @description
  197. * Translate the given i18n key
  198. */
  199. translate(key: string, variables?: { [k: string]: any }): string {
  200. try {
  201. return this._translationFn(key, variables);
  202. } catch (e) {
  203. return `Translation format error: ${e.message}). Original key: ${key}`;
  204. }
  205. }
  206. /**
  207. * Returns true if any element of arr1 appears in arr2.
  208. */
  209. private arraysIntersect<T>(arr1: T[], arr2: T[]): boolean {
  210. return arr1.reduce((intersects, role) => {
  211. return intersects || arr2.includes(role);
  212. }, false as boolean);
  213. }
  214. /**
  215. * The Express "Request" object is huge and contains many circular
  216. * references. We will preserve just a subset of the whole, by preserving
  217. * only the serializable properties up to 2 levels deep.
  218. * @private
  219. */
  220. private shallowCloneRequestObject(req: Request) {
  221. function copySimpleFieldsToDepth(target: any, maxDepth: number, depth: number = 0) {
  222. const result: any = {};
  223. // tslint:disable-next-line:forin
  224. for (const key in target) {
  225. if (key === 'host' && depth === 0) {
  226. // avoid Express "deprecated: req.host" warning
  227. continue;
  228. }
  229. let val: any;
  230. try {
  231. val = (target as any)[key];
  232. } catch (e) {
  233. val = String(e);
  234. }
  235. if (Array.isArray(val)) {
  236. depth++;
  237. result[key] = val.map(v => {
  238. if (!isObject(v) && typeof val !== 'function') {
  239. return v;
  240. } else {
  241. return copySimpleFieldsToDepth(v, maxDepth, depth);
  242. }
  243. });
  244. depth--;
  245. } else if (!isObject(val) && typeof val !== 'function') {
  246. result[key] = val;
  247. } else if (depth < maxDepth) {
  248. depth++;
  249. result[key] = copySimpleFieldsToDepth(val, maxDepth, depth);
  250. depth--;
  251. }
  252. }
  253. return result;
  254. }
  255. return copySimpleFieldsToDepth(req, 1);
  256. }
  257. }