external-account.ts 4.9 KB

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