generate-access-token.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import type { IUserHasId, Scope } from '@growi/core/dist/interfaces';
  2. import { ErrorV3 } from '@growi/core/dist/models';
  3. import type { Request, RequestHandler } from 'express';
  4. import { body } from 'express-validator';
  5. import { SupportedAction } from '~/interfaces/activity';
  6. import type Crowi from '~/server/crowi';
  7. import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
  8. import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
  9. import { AccessToken } from '~/server/models/access-token';
  10. import { isValidScope } from '~/server/util/scope-utils';
  11. import loggerFactory from '~/utils/logger';
  12. import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
  13. import type { ApiV3Response } from '../interfaces/apiv3-response';
  14. const logger = loggerFactory(
  15. 'growi:routes:apiv3:personal-setting:generate-access-tokens',
  16. );
  17. type ReqBody = {
  18. expiredAt: Date;
  19. description?: string;
  20. scopes?: Scope[];
  21. };
  22. interface GenerateAccessTokenRequest
  23. extends Request<undefined, ApiV3Response, ReqBody> {
  24. user: IUserHasId;
  25. }
  26. type GenerateAccessTokenHandlerFactory = (crowi: Crowi) => RequestHandler[];
  27. const validator = [
  28. body('expiredAt')
  29. .exists()
  30. .withMessage('expiredAt is required')
  31. .custom((value) => {
  32. const expiredAt = new Date(value);
  33. const now = new Date();
  34. // Check if date is valid
  35. if (Number.isNaN(expiredAt.getTime())) {
  36. throw new Error('Invalid date format');
  37. }
  38. // Check if date is in the future
  39. if (expiredAt < now) {
  40. throw new Error('Expiration date must be in the future');
  41. }
  42. return true;
  43. }),
  44. body('description')
  45. .optional()
  46. .isString()
  47. .withMessage('description must be a string')
  48. .isLength({ max: 200 })
  49. .withMessage('description must be less than or equal to 200 characters'),
  50. body('scopes')
  51. .optional()
  52. .isArray()
  53. .withMessage('scope must be an array')
  54. .custom((scopes: Scope[]) => {
  55. scopes.forEach((scope) => {
  56. if (!isValidScope(scope)) {
  57. throw new Error(`Invalid scope: ${scope}}`);
  58. }
  59. });
  60. return true;
  61. })
  62. .withMessage('Invalid scope'),
  63. ];
  64. export const generateAccessTokenHandlerFactory: GenerateAccessTokenHandlerFactory =
  65. (crowi) => {
  66. const loginRequiredStrictly =
  67. require('../../../middlewares/login-required')(crowi);
  68. const activityEvent = crowi.event('activity');
  69. const addActivity = generateAddActivityMiddleware();
  70. return [
  71. loginRequiredStrictly,
  72. excludeReadOnlyUser,
  73. addActivity,
  74. validator,
  75. apiV3FormValidator,
  76. async (req: GenerateAccessTokenRequest, res: ApiV3Response) => {
  77. const { user, body } = req;
  78. const { expiredAt, description, scopes } = body;
  79. try {
  80. const tokenData = await AccessToken.generateToken(
  81. user._id,
  82. expiredAt,
  83. scopes,
  84. description,
  85. );
  86. const parameters = {
  87. action: SupportedAction.ACTION_USER_ACCESS_TOKEN_CREATE,
  88. };
  89. activityEvent.emit('update', res.locals.activity._id, parameters);
  90. return res.apiv3(tokenData);
  91. } catch (err) {
  92. logger.error(err);
  93. return res.apiv3Err(
  94. new ErrorV3(err.toString(), 'generate-access-token-failed'),
  95. );
  96. }
  97. },
  98. ];
  99. };