Просмотр исходного кода

implement access token management APIs: add handlers for generating, deleting, and retrieving access tokens

reiji-h 1 год назад
Родитель
Сommit
f6bcff277d

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

@@ -37,13 +37,13 @@ export interface IAccessTokenDocument extends IAccessToken, Document {
 }
 }
 
 
 export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
 export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
-  generateToken: (userId: Types.ObjectId, expiredAt: Date, scope: string[], description?: string,) => Promise<GenerateTokenResult>
+  generateToken: (userId: Types.ObjectId | string, expiredAt: Date, scope?: string[], description?: string,) => Promise<GenerateTokenResult>
   deleteToken: (token: string) => Promise<void>
   deleteToken: (token: string) => Promise<void>
-  deleteTokenById: (tokenId: Types.ObjectId) => Promise<void>
-  deleteAllTokensByUserId: (userId: Types.ObjectId) => Promise<void>
+  deleteTokenById: (tokenId: Types.ObjectId | string) => Promise<void>
+  deleteAllTokensByUserId: (userId: Types.ObjectId | string) => Promise<void>
   deleteExpiredToken: () => Promise<void>
   deleteExpiredToken: () => Promise<void>
   findUserIdByToken: (token: string) => Promise<HydratedDocument<IAccessTokenDocument> | null>
   findUserIdByToken: (token: string) => Promise<HydratedDocument<IAccessTokenDocument> | null>
-  findTokenByUserId: (userId: Types.ObjectId) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>
+  findTokenByUserId: (userId: Types.ObjectId | string) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>
   validateTokenScopes: (token: string, requiredScope: string[]) => Promise<boolean>
   validateTokenScopes: (token: string, requiredScope: string[]) => Promise<boolean>
 }
 }
 
 
@@ -60,7 +60,7 @@ const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
 accessTokenSchema.plugin(mongoosePaginate);
 accessTokenSchema.plugin(mongoosePaginate);
 accessTokenSchema.plugin(uniqueValidator);
 accessTokenSchema.plugin(uniqueValidator);
 
 
-accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId, expiredAt: Date, scope?: string[], description?: string) {
+accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId | string, expiredAt: Date, scope?: string[], description?: string) {
 
 
   const token = crypto.randomBytes(32).toString('hex');
   const token = crypto.randomBytes(32).toString('hex');
   const tokenHash = generateTokenHash(token);
   const tokenHash = generateTokenHash(token);
@@ -86,11 +86,11 @@ accessTokenSchema.statics.deleteToken = async function(token: string) {
   await this.deleteOne({ tokenHash });
   await this.deleteOne({ tokenHash });
 };
 };
 
 
-accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId) {
+accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId | string) {
   await this.deleteOne({ _id: tokenId });
   await this.deleteOne({ _id: tokenId });
 };
 };
 
 
-accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId) {
+accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId | string) {
   await this.deleteMany({ user: userId });
   await this.deleteMany({ user: userId });
 };
 };
 
 
@@ -105,7 +105,7 @@ accessTokenSchema.statics.findUserIdByToken = async function(token: string) {
   return this.findOne({ tokenHash, expiredAt: { $gt: now } }).select('user');
   return this.findOne({ tokenHash, expiredAt: { $gt: now } }).select('user');
 };
 };
 
 
-accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId) {
+accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId | string) {
   const now = new Date();
   const now = new Date();
   return this.find({ user: userId, expiredAt: { $gt: now } }).select('_id expiredAt scope description');
   return this.find({ user: userId, expiredAt: { $gt: now } }).select('_id expiredAt scope description');
 };
 };

+ 49 - 0
apps/app/src/server/routes/apiv3/personal-setting/delete-access-token.ts

@@ -0,0 +1,49 @@
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+
+import { SupportedAction } from '~/interfaces/activity';
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { AccessToken } from '~/server/models/access-token';
+import loggerFactory from '~/utils/logger';
+
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:personal-setting:generate-access-tokens');
+
+type ReqBody = {
+  tokenId: string,
+}
+
+type DeleteAccessTokenRequest = Request<undefined, ApiV3Response, ReqBody>;
+
+type DeleteAccessTokenHandlersFactory = (crowi: Crowi) => RequestHandler[];
+
+export const deleteAccessTokenHandlersFactory: DeleteAccessTokenHandlersFactory = (crowi) => {
+
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const addActivity = generateAddActivityMiddleware();
+  const activityEvent = crowi.event('activity');
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    addActivity,
+    async(req: DeleteAccessTokenRequest, res: ApiV3Response) => {
+      const { body } = req;
+      const { tokenId } = body;
+
+      try {
+        await AccessToken.deleteTokenById(tokenId);
+
+        const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_DELETE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+
+        return res.apiv3({});
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3(err.toString(), 'delete-access-token-failed'));
+      }
+    }];
+};

+ 47 - 0
apps/app/src/server/routes/apiv3/personal-setting/delete-all-access-tokens.ts

@@ -0,0 +1,47 @@
+import type { IUserHasId } from '@growi/core/dist/interfaces';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+
+import { SupportedAction } from '~/interfaces/activity';
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { AccessToken } from '~/server/models/access-token';
+import loggerFactory from '~/utils/logger';
+
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:personal-setting:generate-access-tokens');
+
+interface DeleteAllAccessTokensRequest extends Request<undefined, ApiV3Response, undefined> {
+  user: IUserHasId,
+}
+
+type DeleteAllAccessTokensHandlersFactory = (crowi: Crowi) => RequestHandler[];
+
+export const deleteAllAccessTokensHandlersFactory: DeleteAllAccessTokensHandlersFactory = (crowi) => {
+
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const addActivity = generateAddActivityMiddleware();
+  const activityEvent = crowi.event('activity');
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    addActivity,
+    async(req: DeleteAllAccessTokensRequest, res: ApiV3Response) => {
+      const { user } = req;
+
+      try {
+        await AccessToken.deleteAllTokensByUserId(user._id);
+
+        const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_DELETE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+
+        return res.apiv3({});
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3(err.toString(), 'delete-all-access-token-failed'));
+      }
+    }];
+};

+ 59 - 0
apps/app/src/server/routes/apiv3/personal-setting/generate-access-token.ts

@@ -0,0 +1,59 @@
+
+import type {
+  IUserHasId,
+} from '@growi/core/dist/interfaces';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+
+import { SupportedAction } from '~/interfaces/activity';
+import type Crowi from '~/server/crowi';
+import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { AccessToken } from '~/server/models/access-token';
+import loggerFactory from '~/utils/logger';
+
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:personal-setting:generate-access-tokens');
+
+type ReqBody = {
+  expiredAt: Date,
+  description?: string,
+  scope?: string[],
+}
+
+interface GenerateAccessTokenRequest extends Request<undefined, ApiV3Response, ReqBody> {
+  user: IUserHasId,
+}
+
+
+type GenerateAccessTokenHandlerFactory = (crowi: Crowi) => RequestHandler[];
+
+export const generateAccessTokenHandlerFactory: GenerateAccessTokenHandlerFactory = (crowi) => {
+
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const activityEvent = crowi.event('activity');
+  const addActivity = generateAddActivityMiddleware();
+
+
+  return [
+    loginRequiredStrictly, addActivity,
+    async(req: GenerateAccessTokenRequest, res: ApiV3Response) => {
+
+      const { user, body } = req;
+      const { expiredAt, description, scope } = body;
+
+      try {
+        const tokenData = await AccessToken.generateToken(user._id, expiredAt, scope, description);
+
+        const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_CREATE };
+        activityEvent.emit('update', res.locals.activity._id, parameters);
+
+        return res.apiv3(tokenData);
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3(err.toString(), 'generate-access-token-failed'));
+      }
+    },
+  ];
+};

+ 42 - 0
apps/app/src/server/routes/apiv3/personal-setting/get-access-tokens.ts

@@ -0,0 +1,42 @@
+import type { IUserHasId } from '@growi/core/dist/interfaces';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+
+import type Crowi from '~/server/crowi';
+import { accessTokenParser } from '~/server/middlewares/access-token-parser';
+import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
+import { AccessToken } from '~/server/models/access-token';
+import loggerFactory from '~/utils/logger';
+
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+const logger = loggerFactory('growi:routes:apiv3:personal-setting:get-access-tokens');
+
+interface GetAccessTokenRequest extends Request<undefined, ApiV3Response, undefined> {
+  user: IUserHasId,
+}
+
+type GetAccessTokenHandlerFactory = (crowi: Crowi) => RequestHandler[];
+
+export const getAccessTokenHandlerFactory: GetAccessTokenHandlerFactory = (crowi) => {
+
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+  const addActivity = generateAddActivityMiddleware();
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    addActivity,
+    async(req: GetAccessTokenRequest, res: ApiV3Response) => {
+      const { user } = req;
+
+      try {
+        const accessTokens = await AccessToken.findTokenByUserId(user._id);
+        return res.apiv3({ accessTokens });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(new ErrorV3(err.toString(), 'colud_not_get_access_token'));
+      }
+    },
+  ];
+};

+ 54 - 72
apps/app/src/server/routes/apiv3/personal-setting.js → apps/app/src/server/routes/apiv3/personal-setting/index.js

@@ -1,19 +1,22 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 
 
-
 import { i18n } from '^/config/next-i18next.config';
 import { i18n } from '^/config/next-i18next.config';
 
 
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
-import { AccessToken } from '~/server/models/access-token';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
-import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-import EditorSettings from '../../models/editor-settings';
-import ExternalAccount from '../../models/external-account';
-import InAppNotificationSettings from '../../models/in-app-notification-settings';
+import { generateAddActivityMiddleware } from '../../../middlewares/add-activity';
+import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import EditorSettings from '../../../models/editor-settings';
+import ExternalAccount from '../../../models/external-account';
+import InAppNotificationSettings from '../../../models/in-app-notification-settings';
+
+import { deleteAccessTokenHandlersFactory } from './delete-access-token';
+import { deleteAllAccessTokensHandlersFactory } from './delete-all-access-tokens';
+import { generateAccessTokenHandlerFactory } from './generate-access-token';
+import { getAccessTokenHandlerFactory } from './get-access-tokens';
 
 
 
 
 const logger = loggerFactory('growi:routes:apiv3:personal-setting');
 const logger = loggerFactory('growi:routes:apiv3:personal-setting');
@@ -69,7 +72,7 @@ const router = express.Router();
  */
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
-  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
 
   const { User } = crowi.models;
   const { User } = crowi.models;
@@ -377,6 +380,45 @@ module.exports = (crowi) => {
 
 
   });
   });
 
 
+
+  /**
+   * @swagger
+   *
+   *    /personal-setting/api-token:
+   *      put:
+   *        tags: [GeneralSetting]
+   *        operationId: putUserApiToken
+   *        summary: /personal-setting/api-token
+   *        description: Update user api token
+   *        responses:
+   *          200:
+   *            description: succeded to update user api token
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    userData:
+   *                      type: object
+   *                      description: user data
+   */
+  router.put('/api-token', loginRequiredStrictly, addActivity, async(req, res) => {
+    const { user } = req;
+
+    try {
+      const userData = await user.updateApiToken();
+
+      const parameters = { action: SupportedAction.ACTION_USER_API_TOKEN_UPDATE };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+
+      return res.apiv3({ userData });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err('update-api-token-failed');
+    }
+
+  });
+
   /**
   /**
    * @swagger
    * @swagger
    *   /personal-setting/access-token:
    *   /personal-setting/access-token:
@@ -396,19 +438,7 @@ module.exports = (crowi) => {
    *                   type: objet
    *                   type: objet
    *                   description: array of access tokens
    *                   description: array of access tokens
    */
    */
-  router.get('/access-token', accessTokenParser, loginRequiredStrictly, addActivity, async(req, res) => {
-    const { user } = req;
-
-    try {
-      const accessTokens = await AccessToken.findTokenByUserId(user._id);
-      return res.apiv3({ accessTokens });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('get-access-token-failed');
-    }
-  });
-
+  router.get('/access-token', getAccessTokenHandlerFactory(crowi));
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -441,24 +471,7 @@ module.exports = (crowi) => {
    *                     type: string[]
    *                     type: string[]
    *                     description: scope of access token
    *                     description: scope of access token
    */
    */
-  router.post('/access-token', loginRequiredStrictly, addActivity, async(req, res) => {
-
-    const { user, body } = req;
-    const { expiredAt, description, scope } = body;
-
-    try {
-      const tokenData = await AccessToken.generateToken(user, expiredAt, scope, description);
-
-      const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_CREATE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.apiv3(tokenData);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('generate-access-token-failed');
-    }
-  });
+  router.post('/access-token', generateAccessTokenHandlerFactory(crowi));
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -473,23 +486,7 @@ module.exports = (crowi) => {
    *         description: succeded to delete access token
    *         description: succeded to delete access token
    *
    *
    */
    */
-  router.delete('/access-token', accessTokenParser, loginRequiredStrictly, addActivity, async(req, res) => {
-    const { body } = req;
-    const { tokenId } = body;
-
-    try {
-      await AccessToken.deleteTokenById(tokenId);
-
-      const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_DELETE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.apiv3({});
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('delete-access-token-failed');
-    }
-  });
+  router.delete('/access-token', deleteAccessTokenHandlersFactory(crowi));
 
 
   /**
   /**
    * @swagger
    * @swagger
@@ -503,22 +500,7 @@ module.exports = (crowi) => {
    *         200:
    *         200:
    *           description: succeded to delete all access tokens
    *           description: succeded to delete all access tokens
    */
    */
-  router.delete('/access-token/all', accessTokenParser, loginRequiredStrictly, addActivity, async(req, res) => {
-    const { user } = req;
-
-    try {
-      await AccessToken.deleteAllTokensByUserId(user._id);
-
-      const parameters = { action: SupportedAction.ACTION_USER_ACCESS_TOKEN_DELETE };
-      activityEvent.emit('update', res.locals.activity._id, parameters);
-
-      return res.apiv3({});
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err('delete-all-access-token-failed');
-    }
-  });
+  router.delete('/access-token/all', deleteAllAccessTokensHandlersFactory(crowi));
 
 
   /**
   /**
    * @swagger
    * @swagger