request-context.ts 9.6 KB

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