external-account.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // disable no-return-await for model functions
  2. /* eslint-disable no-return-await */
  3. import type {
  4. IExternalAccount,
  5. IUser,
  6. IUserHasId,
  7. } from '@growi/core/dist/interfaces';
  8. import type { Document, HydratedDocument, Model } from 'mongoose';
  9. import mongoose, { Schema } from 'mongoose';
  10. import mongoosePaginate from 'mongoose-paginate-v2';
  11. import uniqueValidator from 'mongoose-unique-validator';
  12. import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
  13. import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
  14. import loggerFactory from '~/utils/logger';
  15. import { getOrCreateModel } from '../util/mongoose-utils';
  16. const logger = loggerFactory('growi:models:external-account');
  17. export interface ExternalAccountDocument
  18. extends IExternalAccount<IExternalAuthProviderType>,
  19. Document {}
  20. export interface ExternalAccountModel extends Model<ExternalAccountDocument> {
  21. [x: string]: any; // for old methods
  22. }
  23. const schema = new Schema<ExternalAccountDocument, ExternalAccountModel>(
  24. {
  25. providerType: { type: String, required: true },
  26. accountId: { type: String, required: true },
  27. user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  28. },
  29. {
  30. timestamps: { createdAt: true, updatedAt: false },
  31. },
  32. );
  33. // compound index
  34. schema.index({ providerType: 1, accountId: 1 }, { unique: true });
  35. // apply plugins
  36. schema.plugin(mongoosePaginate);
  37. schema.plugin(uniqueValidator);
  38. /**
  39. * limit items num for pagination
  40. */
  41. const DEFAULT_LIMIT = 50;
  42. /**
  43. * The Exception class thrown when User.username is duplicated when creating user
  44. *
  45. * @class DuplicatedUsernameException
  46. */
  47. class DuplicatedUsernameException {
  48. name: string;
  49. message: string;
  50. user: IUserHasId;
  51. constructor(message, user) {
  52. this.name = this.constructor.name;
  53. this.message = message;
  54. this.user = user;
  55. }
  56. }
  57. /**
  58. * find an account or register if not found
  59. */
  60. schema.statics.findOrRegister = function (
  61. isSameUsernameTreatedAsIdenticalUser: boolean,
  62. isSameEmailTreatedAsIdenticalUser: boolean,
  63. providerType: string,
  64. accountId: string,
  65. usernameToBeRegistered: string | undefined,
  66. nameToBeRegistered = '',
  67. mailToBeRegistered?: string,
  68. ): Promise<HydratedDocument<IExternalAccount<IExternalAuthProviderType>>> {
  69. return this.findOne({ providerType, accountId }).then((account) => {
  70. // ExternalAccount is found
  71. if (account != null) {
  72. logger.debug(`ExternalAccount '${accountId}' is found `, account);
  73. return account;
  74. }
  75. if (usernameToBeRegistered == null) {
  76. throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
  77. }
  78. const User = mongoose.model<
  79. HydratedDocument<IUser>,
  80. Model<IUser> & { createUser; STATUS_ACTIVE }
  81. >('User');
  82. let promise = User.findOne({ username: usernameToBeRegistered }).exec();
  83. if (
  84. isSameUsernameTreatedAsIdenticalUser &&
  85. isSameEmailTreatedAsIdenticalUser
  86. ) {
  87. promise = promise.then((user) => {
  88. if (user == null) {
  89. return User.findOne({ email: mailToBeRegistered });
  90. }
  91. return user;
  92. });
  93. } else if (isSameEmailTreatedAsIdenticalUser) {
  94. promise = User.findOne({ email: mailToBeRegistered }).exec();
  95. }
  96. return promise
  97. .then((user) => {
  98. // when the User that have the same `username` exists
  99. if (user != null) {
  100. throw new DuplicatedUsernameException(
  101. `User '${usernameToBeRegistered}' already exists`,
  102. user,
  103. );
  104. }
  105. // create a new User with STATUS_ACTIVE
  106. logger.debug(
  107. `ExternalAccount '${accountId}' is not found, it is going to be registered.`,
  108. );
  109. return User.createUser(
  110. nameToBeRegistered,
  111. usernameToBeRegistered,
  112. mailToBeRegistered,
  113. undefined,
  114. undefined,
  115. User.STATUS_ACTIVE,
  116. );
  117. })
  118. .then((newUser) => {
  119. return this.associate(providerType, accountId, newUser);
  120. });
  121. });
  122. };
  123. /**
  124. * Create ExternalAccount document and associate to existing User
  125. */
  126. schema.statics.associate = function (
  127. providerType: string,
  128. accountId: string,
  129. user: IUserHasId,
  130. ) {
  131. return this.create({ providerType, accountId, user: user._id });
  132. };
  133. /**
  134. * find all entities with pagination
  135. *
  136. * @see https://github.com/edwardhotchkiss/mongoose-paginate
  137. *
  138. * @static
  139. * @param {any} opts mongoose-paginate options object
  140. * @returns {Promise<any>} mongoose-paginate result object
  141. * @memberof ExternalAccount
  142. */
  143. schema.statics.findAllWithPagination = function (opts) {
  144. const query = {};
  145. const options = Object.assign({ populate: 'user' }, opts);
  146. if (options.sort == null) {
  147. options.sort = { accountId: 1, createdAt: 1 };
  148. }
  149. if (options.limit == null) {
  150. options.limit = DEFAULT_LIMIT;
  151. }
  152. return this.paginate(query, options);
  153. };
  154. export default getOrCreateModel<ExternalAccountDocument, ExternalAccountModel>(
  155. 'ExternalAccount',
  156. schema,
  157. );