InstitutionsAPI.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. const OError = require('@overleaf/o-error')
  2. const logger = require('logger-sharelatex')
  3. const metrics = require('@overleaf/metrics')
  4. const settings = require('settings-sharelatex')
  5. const request = require('request')
  6. const { promisifyAll } = require('../../util/promises')
  7. const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
  8. const {
  9. V1ConnectionError,
  10. InvalidInstitutionalEmailError
  11. } = require('../Errors/Errors')
  12. const InstitutionsAPI = {
  13. getInstitutionAffiliations(institutionId, callback) {
  14. makeAffiliationRequest(
  15. {
  16. method: 'GET',
  17. path: `/api/v2/institutions/${institutionId.toString()}/affiliations`,
  18. defaultErrorMessage: "Couldn't get institution affiliations"
  19. },
  20. (error, body) => callback(error, body || [])
  21. )
  22. },
  23. getInstitutionLicences(institutionId, startDate, endDate, lag, callback) {
  24. makeAffiliationRequest(
  25. {
  26. method: 'GET',
  27. path: `/api/v2/institutions/${institutionId.toString()}/institution_licences`,
  28. body: { start_date: startDate, end_date: endDate, lag },
  29. defaultErrorMessage: "Couldn't get institution licences"
  30. },
  31. callback
  32. )
  33. },
  34. getInstitutionNewLicences(institutionId, startDate, endDate, lag, callback) {
  35. makeAffiliationRequest(
  36. {
  37. method: 'GET',
  38. path: `/api/v2/institutions/${institutionId.toString()}/new_institution_licences`,
  39. body: { start_date: startDate, end_date: endDate, lag },
  40. defaultErrorMessage: "Couldn't get institution new licences"
  41. },
  42. callback
  43. )
  44. },
  45. getUserAffiliations(userId, callback) {
  46. makeAffiliationRequest(
  47. {
  48. method: 'GET',
  49. path: `/api/v2/users/${userId.toString()}/affiliations`,
  50. defaultErrorMessage: "Couldn't get user affiliations"
  51. },
  52. (error, body) => callback(error, body || [])
  53. )
  54. },
  55. addAffiliation(userId, email, affiliationOptions, callback) {
  56. if (!callback) {
  57. // affiliationOptions is optional
  58. callback = affiliationOptions
  59. affiliationOptions = {}
  60. }
  61. const {
  62. university,
  63. department,
  64. role,
  65. confirmedAt,
  66. entitlement,
  67. rejectIfBlocklisted
  68. } = affiliationOptions
  69. makeAffiliationRequest(
  70. {
  71. method: 'POST',
  72. path: `/api/v2/users/${userId.toString()}/affiliations`,
  73. body: {
  74. email,
  75. university,
  76. department,
  77. role,
  78. confirmedAt,
  79. entitlement,
  80. rejectIfBlocklisted
  81. },
  82. defaultErrorMessage: "Couldn't create affiliation"
  83. },
  84. function(error, body) {
  85. if (error) {
  86. if (error.info && error.info.statusCode === 422) {
  87. return callback(
  88. new InvalidInstitutionalEmailError(error.message).withCause(error)
  89. )
  90. }
  91. return callback(error)
  92. }
  93. if (!university) {
  94. return callback(null, body)
  95. }
  96. // have notifications delete any ip matcher notifications for this university
  97. NotificationsBuilder.ipMatcherAffiliation(userId).read(
  98. university.id,
  99. function(err) {
  100. if (err) {
  101. // log and ignore error
  102. logger.err(
  103. { err },
  104. 'Something went wrong marking ip notifications read'
  105. )
  106. }
  107. callback(null, body)
  108. }
  109. )
  110. }
  111. )
  112. },
  113. removeAffiliation(userId, email, callback) {
  114. makeAffiliationRequest(
  115. {
  116. method: 'POST',
  117. path: `/api/v2/users/${userId.toString()}/affiliations/remove`,
  118. body: { email },
  119. extraSuccessStatusCodes: [404], // `Not Found` responses are considered successful
  120. defaultErrorMessage: "Couldn't remove affiliation"
  121. },
  122. callback
  123. )
  124. },
  125. endorseAffiliation(userId, email, role, department, callback) {
  126. makeAffiliationRequest(
  127. {
  128. method: 'POST',
  129. path: `/api/v2/users/${userId.toString()}/affiliations/endorse`,
  130. body: { email, role, department },
  131. defaultErrorMessage: "Couldn't endorse affiliation"
  132. },
  133. callback
  134. )
  135. },
  136. deleteAffiliations(userId, callback) {
  137. makeAffiliationRequest(
  138. {
  139. method: 'DELETE',
  140. path: `/api/v2/users/${userId.toString()}/affiliations`,
  141. defaultErrorMessage: "Couldn't delete affiliations"
  142. },
  143. callback
  144. )
  145. },
  146. addEntitlement(userId, email, callback) {
  147. makeAffiliationRequest(
  148. {
  149. method: 'POST',
  150. path: `/api/v2/users/${userId}/affiliations/add_entitlement`,
  151. body: { email },
  152. defaultErrorMessage: "Couldn't add entitlement"
  153. },
  154. callback
  155. )
  156. },
  157. removeEntitlement(userId, email, callback) {
  158. makeAffiliationRequest(
  159. {
  160. method: 'POST',
  161. path: `/api/v2/users/${userId}/affiliations/remove_entitlement`,
  162. body: { email },
  163. defaultErrorMessage: "Couldn't remove entitlement"
  164. },
  165. callback
  166. )
  167. }
  168. }
  169. var makeAffiliationRequest = function(requestOptions, callback) {
  170. if (!settings.apis.v1.url) {
  171. return callback(null)
  172. } // service is not configured
  173. if (!requestOptions.extraSuccessStatusCodes) {
  174. requestOptions.extraSuccessStatusCodes = []
  175. }
  176. request(
  177. {
  178. method: requestOptions.method,
  179. url: `${settings.apis.v1.url}${requestOptions.path}`,
  180. body: requestOptions.body,
  181. auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass },
  182. json: true,
  183. timeout: 20 * 1000
  184. },
  185. function(error, response, body) {
  186. if (error) {
  187. return callback(
  188. new V1ConnectionError('error getting affiliations from v1').withCause(
  189. error
  190. )
  191. )
  192. }
  193. if (response && response.statusCode >= 500) {
  194. return callback(
  195. new V1ConnectionError({
  196. message: 'error getting affiliations from v1',
  197. info: {
  198. status: response.statusCode,
  199. body: body
  200. }
  201. })
  202. )
  203. }
  204. let isSuccess = response.statusCode >= 200 && response.statusCode < 300
  205. if (!isSuccess) {
  206. isSuccess = requestOptions.extraSuccessStatusCodes.includes(
  207. response.statusCode
  208. )
  209. }
  210. if (!isSuccess) {
  211. let errorMessage
  212. if (body && body.errors) {
  213. errorMessage = `${response.statusCode}: ${body.errors}`
  214. } else {
  215. errorMessage = `${requestOptions.defaultErrorMessage}: ${response.statusCode}`
  216. }
  217. logger.warn(
  218. { path: requestOptions.path, body: requestOptions.body },
  219. errorMessage
  220. )
  221. return callback(
  222. new OError(errorMessage, { statusCode: response.statusCode })
  223. )
  224. }
  225. callback(null, body)
  226. }
  227. )
  228. }
  229. ;[
  230. 'getInstitutionAffiliations',
  231. 'getUserAffiliations',
  232. 'addAffiliation',
  233. 'removeAffiliation'
  234. ].map(method =>
  235. metrics.timeAsyncMethod(
  236. InstitutionsAPI,
  237. method,
  238. 'mongo.InstitutionsAPI',
  239. logger
  240. )
  241. )
  242. InstitutionsAPI.promises = promisifyAll(InstitutionsAPI)
  243. module.exports = InstitutionsAPI