Browse Source

Merge pull request #9688 from weseek/feat/85969-85962-create-access-token-model

feat: Create new access token model
Yuki Takei 1 year ago
parent
commit
1a483b67b3

+ 1 - 0
apps/app/src/server/crowi/setup-models.ts

@@ -71,5 +71,6 @@ export const setupIndependentModels = async(): Promise<void> => {
     import('../models/user-group'),
     import('../models/user-registration-order'),
     import('../models/user-ui-settings'),
+    import('../models/access-token'),
   ]);
 };

+ 114 - 0
apps/app/src/server/models/access-token.ts

@@ -0,0 +1,114 @@
+import crypto from 'crypto';
+
+import type { Ref, IUserHasId } from '@growi/core/dist/interfaces';
+import type { Document, Model, Types } from 'mongoose';
+import { Schema } from 'mongoose';
+import mongoosePaginate from 'mongoose-paginate-v2';
+import uniqueValidator from 'mongoose-unique-validator';
+
+import loggerFactory from '~/utils/logger';
+
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+const logger = loggerFactory('growi:models:access-token');
+
+const generateTokenHash = (token: string) => crypto.createHash('sha256').update(token).digest('hex');
+
+export type IAccessToken = {
+  user: Ref<IUserHasId>,
+  tokenHash: string,
+  expiredAt: Date,
+  scope?: string[],
+  description?: string,
+}
+
+export interface IAccessTokenDocument extends IAccessToken, Document {
+  isExpired: () => boolean
+}
+
+export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
+  generateToken: (userId: Types.ObjectId, expiredAt: Date, scope: string[], description?: string,) => Promise<string>
+  deleteToken: (token: string) => Promise<void>
+  deleteTokenById: (tokenId: Types.ObjectId) => Promise<void>
+  deleteAllTokensByUserId: (userId: Types.ObjectId) => Promise<void>
+  deleteExpiredToken: () => Promise<void>
+  findUserIdByToken: (token: string) => Promise<Types.ObjectId>
+  findTokenByUserId: (userId: Types.ObjectId) => Promise<IAccessTokenDocument[]>
+  validateTokenScopes: (token: string, requiredScope: string[]) => Promise<boolean>
+}
+
+const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
+  user: {
+    type: Schema.Types.ObjectId, ref: 'User', required: true,
+  },
+  tokenHash: { type: String, required: true, unique: true },
+  expiredAt: { type: Date, required: true, index: true },
+  scope: [{ type: String, default: '' }],
+  description: { type: String, default: '' },
+});
+
+accessTokenSchema.plugin(mongoosePaginate);
+accessTokenSchema.plugin(uniqueValidator);
+
+accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId, expiredAt: Date, scope?: string[], description?: string) {
+
+  const token = crypto.randomBytes(32).toString('hex');
+  const tokenHash = generateTokenHash(token);
+
+  try {
+    const { _id } = await this.create({
+      user: userId, tokenHash, expiredAt, scope, description,
+    });
+
+    logger.debug('Token generated');
+    return {
+      token, _id, expiredAt, scope, description,
+    };
+  }
+  catch (err) {
+    logger.debug('Failed to generate token');
+    throw err;
+  }
+};
+
+accessTokenSchema.statics.deleteToken = async function(token: string) {
+  const tokenHash = generateTokenHash(token);
+  return this.deleteOne({ tokenHash });
+};
+
+accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId) {
+  return this.deleteOne({ _id: tokenId });
+};
+
+accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId) {
+  return this.deleteMany({ user: userId });
+};
+
+accessTokenSchema.statics.deleteExpiredToken = async function() {
+  const now = new Date();
+  return this.deleteMany({ expiredAt: { $lte: now } });
+};
+
+accessTokenSchema.statics.findUserIdByToken = async function(token: string) {
+  const tokenHash = generateTokenHash(token);
+  const now = new Date();
+  return this.findOne({ tokenHash, expiredAt: { $gt: now } }).select('user');
+};
+
+accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId) {
+  const now = new Date();
+  return this.find({ user: userId, expiredAt: { $gt: now } }).select('_id expiredAt scope description');
+};
+
+accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: string[]) {
+  const tokenHash = generateTokenHash(token);
+  const now = new Date();
+  const tokenData = await this.findOne({ tokenHash, expiredAt: { $gt: now }, scope: { $all: requiredScopes } });
+  return tokenData != null;
+};
+
+accessTokenSchema.methods.isExpired = function() {
+  return this.expiredAt < new Date();
+};
+
+export const AccessToken = getOrCreateModel<IAccessTokenDocument, IAccessToken>('AccessToken', accessTokenSchema);

+ 5 - 5
apps/app/src/server/models/user.js

@@ -120,11 +120,6 @@ const factory = (crowi) => {
     }
   }
 
-  function generateRandomEmail() {
-    const randomstr = generateRandomTempPassword();
-    return `change-it-${randomstr}@example.com`;
-  }
-
   function generateRandomTempPassword() {
     const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
     let password = '';
@@ -138,6 +133,11 @@ const factory = (crowi) => {
     return password;
   }
 
+  function generateRandomEmail() {
+    const randomstr = generateRandomTempPassword();
+    return `change-it-${randomstr}@example.com`;
+  }
+
   function generatePassword(password) {
     validateCrowi();
 

+ 1 - 1
apps/app/src/server/service/import/construct-convert-map.integ.ts

@@ -30,6 +30,6 @@ describe('constructConvertMap', () => {
 
     // assert
     expect(result).not.toBeNull();
-    expect(Object.keys(result).length).toEqual(36);
+    expect(Object.keys(result).length).toEqual(37);
   });
 });