utils.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { ExecutionContext, HttpException } from '@nestjs/common';
  2. import { Reflector } from '@nestjs/core';
  3. import { GqlExecutionContext } from '@nestjs/graphql';
  4. import { Prisma } from '@prisma/client';
  5. import * as A from 'fp-ts/Array';
  6. import * as E from 'fp-ts/Either';
  7. import { pipe } from 'fp-ts/lib/function';
  8. import * as O from 'fp-ts/Option';
  9. import * as T from 'fp-ts/Task';
  10. import * as TE from 'fp-ts/TaskEither';
  11. import { AuthProvider } from './auth/helper';
  12. import {
  13. ENV_EMPTY_AUTH_PROVIDERS,
  14. ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
  15. ENV_NOT_SUPPORT_AUTH_PROVIDERS,
  16. JSON_INVALID,
  17. } from './errors';
  18. import { TeamMemberRole } from './team/team.model';
  19. import { RESTError } from './types/RESTError';
  20. /**
  21. * A workaround to throw an exception in an expression.
  22. * JS throw keyword creates a statement not an expression.
  23. * This function allows throw to be used as an expression
  24. * @param errMessage Message present in the error message
  25. */
  26. export function throwErr(errMessage: string): never {
  27. throw new Error(errMessage);
  28. }
  29. /**
  30. * This function allows throw to be used as an expression
  31. * @param errMessage Message present in the error message
  32. */
  33. export function throwHTTPErr(errorData: RESTError): never {
  34. const { message, statusCode } = errorData;
  35. throw new HttpException(message, statusCode);
  36. }
  37. /**
  38. * Prints the given value to log and returns the same value.
  39. * Used for debugging functional pipelines.
  40. * @param val The value to print
  41. * @returns `val` itself
  42. */
  43. export const trace = <T>(val: T) => {
  44. console.log(val);
  45. return val;
  46. };
  47. /**
  48. * Similar to `trace` but allows for labels and also an
  49. * optional transform function.
  50. * @param name The label to given to the trace log (log outputs like this "<name>: <value>")
  51. * @param transform An optional function to transform the log output value (useful for checking specific aspects or transforms (duh))
  52. * @returns A function which takes a value, and is traced.
  53. */
  54. export const namedTrace =
  55. <T>(name: string, transform?: (val: T) => unknown) =>
  56. (val: T) => {
  57. console.log(`${name}:`, transform ? transform(val) : val);
  58. return val;
  59. };
  60. /**
  61. * Returns the list of required roles annotated on a GQL Operation
  62. * @param reflector NestJS Reflector instance
  63. * @param context NestJS Execution Context
  64. * @returns An Option which contains the defined roles
  65. */
  66. export const getAnnotatedRequiredRoles = (
  67. reflector: Reflector,
  68. context: ExecutionContext,
  69. ) =>
  70. pipe(
  71. reflector.get<TeamMemberRole[]>('requiresTeamRole', context.getHandler()),
  72. O.fromNullable,
  73. );
  74. /**
  75. * Gets the user from the NestJS GQL Execution Context.
  76. * Usually used within guards.
  77. * @param ctx The Execution Context to use to get it
  78. * @returns An Option of the user
  79. */
  80. export const getUserFromGQLContext = (ctx: ExecutionContext) =>
  81. pipe(
  82. ctx,
  83. GqlExecutionContext.create,
  84. (ctx) => ctx.getContext().req,
  85. ({ user }) => user,
  86. O.fromNullable,
  87. );
  88. /**
  89. * Gets a GQL Argument in the defined operation.
  90. * Usually used in guards.
  91. * @param argName The name of the argument to get
  92. * @param ctx The NestJS Execution Context to use to get it.
  93. * @returns The argument value typed as `unknown`
  94. */
  95. export const getGqlArg = <ArgName extends string>(
  96. argName: ArgName,
  97. ctx: ExecutionContext,
  98. ) =>
  99. pipe(
  100. ctx,
  101. GqlExecutionContext.create,
  102. (ctx) => ctx.getArgs<object>(),
  103. // We are not sure if this thing will even exist
  104. // We pass that worry to the caller
  105. (args) => args[argName as any] as unknown,
  106. );
  107. /**
  108. * Sequences an array of TaskEither values while maintaining an array of all the error values
  109. * @param arr Array of TaskEithers
  110. * @returns A TaskEither saying all the errors possible on the left or all the success values on the right
  111. */
  112. export const taskEitherValidateArraySeq = <A, B>(
  113. arr: TE.TaskEither<A, B>[],
  114. ): TE.TaskEither<A[], B[]> =>
  115. pipe(
  116. arr,
  117. A.map(TE.mapLeft(A.of)),
  118. A.sequence(
  119. TE.getApplicativeTaskValidation(T.ApplicativeSeq, A.getMonoid<A>()),
  120. ),
  121. );
  122. /**
  123. * Checks to see if the email is valid or not
  124. * @param email The email
  125. * @see https://emailregex.com/ for information on email regex
  126. * @returns A Boolean depending on the format of the email
  127. */
  128. export const validateEmail = (email: string) => {
  129. return new RegExp(
  130. /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
  131. ).test(email);
  132. };
  133. // Regular expressions for supported address object formats by nodemailer
  134. // check out for more info https://nodemailer.com/message/addresses
  135. const emailRegex1 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  136. const emailRegex2 =
  137. /^[\w\s]* <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
  138. const emailRegex3 =
  139. /^"[\w\s]+" <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
  140. /**
  141. * Checks to see if the SMTP email is valid or not
  142. * @param email
  143. * @returns A Boolean depending on the format of the email
  144. */
  145. export const validateSMTPEmail = (email: string) => {
  146. // Check if the input matches any of the formats
  147. return (
  148. emailRegex1.test(email) ||
  149. emailRegex2.test(email) ||
  150. emailRegex3.test(email)
  151. );
  152. };
  153. /**
  154. * Checks to see if the URL is valid or not
  155. * @param url The URL to validate
  156. * @returns boolean
  157. */
  158. export const validateSMTPUrl = (url: string) => {
  159. // Possible valid formats
  160. // smtp(s)://mail.example.com
  161. // smtp(s)://user:pass@mail.example.com
  162. // smtp(s)://mail.example.com:587
  163. // smtp(s)://user:pass@mail.example.com:587
  164. if (!url || url.length === 0) return false;
  165. const regex =
  166. /^(smtp|smtps):\/\/(?:([^:]+):([^@]+)@)?((?!\.)[^:]+)(?::(\d+))?$/;
  167. if (regex.test(url)) return true;
  168. return false;
  169. };
  170. /**
  171. * Checks to see if the URL is valid or not
  172. * @param url The URL to validate
  173. * @returns boolean
  174. */
  175. export const validateUrl = (url: string) => {
  176. const urlRegex = /^(http|https):\/\/[^ "]+$/;
  177. return urlRegex.test(url);
  178. };
  179. /**
  180. * String to JSON parser
  181. * @param {str} str The string to parse
  182. * @returns {E.Right<T> | E.Left<"json_invalid">} An Either of the parsed JSON
  183. */
  184. export function stringToJson<T>(
  185. str: string,
  186. ): E.Right<T | any> | E.Left<string> {
  187. try {
  188. return E.right(JSON.parse(str));
  189. } catch (err) {
  190. return E.left(JSON_INVALID);
  191. }
  192. }
  193. /**
  194. *
  195. * @param title string whose length we need to check
  196. * @param length minimum length the title needs to be
  197. * @returns boolean if title is of valid length or not
  198. */
  199. export function isValidLength(title: string, length: number) {
  200. if (title.length < length) {
  201. return false;
  202. }
  203. return true;
  204. }
  205. /**
  206. * This function is called by bootstrap() in main.ts
  207. * It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
  208. * If not, it throws an error.
  209. */
  210. export function checkEnvironmentAuthProvider(
  211. VITE_ALLOWED_AUTH_PROVIDERS: string,
  212. ) {
  213. if (!VITE_ALLOWED_AUTH_PROVIDERS) {
  214. throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
  215. }
  216. if (VITE_ALLOWED_AUTH_PROVIDERS === '') {
  217. throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
  218. }
  219. const givenAuthProviders = VITE_ALLOWED_AUTH_PROVIDERS.split(',').map(
  220. (provider) => provider.toLocaleUpperCase(),
  221. );
  222. const supportedAuthProviders = Object.values(AuthProvider).map(
  223. (provider: string) => provider.toLocaleUpperCase(),
  224. );
  225. for (const givenAuthProvider of givenAuthProviders) {
  226. if (!supportedAuthProviders.includes(givenAuthProvider)) {
  227. throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
  228. }
  229. }
  230. }
  231. /**
  232. * Adds escape backslashes to the input so that it can be used inside
  233. * SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
  234. * function.
  235. *
  236. * Eg. "100%" -> "100\\%"
  237. *
  238. * Source: https://stackoverflow.com/a/32648526
  239. */
  240. export function escapeSqlLikeString(str: string) {
  241. if (typeof str != 'string') return str;
  242. return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
  243. switch (char) {
  244. case '\0':
  245. return '\\0';
  246. case '\x08':
  247. return '\\b';
  248. case '\x09':
  249. return '\\t';
  250. case '\x1a':
  251. return '\\z';
  252. case '\n':
  253. return '\\n';
  254. case '\r':
  255. return '\\r';
  256. case '"':
  257. case "'":
  258. case '\\':
  259. case '%':
  260. return '\\' + char; // prepends a backslash to backslash, percent,
  261. // and double/single quotes
  262. }
  263. });
  264. }
  265. /**
  266. * Calculate the expiration date of the token
  267. *
  268. * @param expiresOn Number of days the token is valid for
  269. * @returns Date object of the expiration date
  270. */
  271. export function calculateExpirationDate(expiresOn: null | number) {
  272. if (expiresOn === null) return null;
  273. return new Date(Date.now() + expiresOn * 24 * 60 * 60 * 1000);
  274. }
  275. /*
  276. * Transforms the collection level properties (authorization & headers) under the `data` field.
  277. * Preserves `null` values and prevents duplicate stringification.
  278. *
  279. * @param {Prisma.JsonValue} collectionData - The team collection data to transform.
  280. * @returns {string | null} The transformed team collection data as a string.
  281. */
  282. export function transformCollectionData(
  283. collectionData: Prisma.JsonValue,
  284. ): string | null {
  285. if (!collectionData) {
  286. return null;
  287. }
  288. return typeof collectionData === 'string'
  289. ? collectionData
  290. : JSON.stringify(collectionData);
  291. }