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

Merge pull request #9758 from weseek/feat/163166-163325-auto-delete-expired-access-token

feat: Automatic deletion of expired access tokens
Shun Miyazawa 1 год назад
Родитель
Сommit
21b06dca83

+ 1 - 5
apps/app/src/client/components/Me/AccessTokenForm.tsx

@@ -44,6 +44,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
 
 
   const onSubmit = (data: FormInputs) => {
   const onSubmit = (data: FormInputs) => {
     const expiredAtDate = new Date(data.expiredAt);
     const expiredAtDate = new Date(data.expiredAt);
+    expiredAtDate.setHours(23, 59, 59, 999);
     const scopes: Scope[] = data.scopes ? data.scopes : [];
     const scopes: Scope[] = data.scopes ? data.scopes : [];
 
 
     submitHandler({
     submitHandler({
@@ -70,11 +71,6 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
                     min={todayStr}
                     min={todayStr}
                     {...register('expiredAt', {
                     {...register('expiredAt', {
                       required: t('input_validation.message.required', { param: t('page_me_access_token.expiredAt') }),
                       required: t('input_validation.message.required', { param: t('page_me_access_token.expiredAt') }),
-                      validate: (value) => {
-                        const date = new Date(value);
-                        const now = new Date();
-                        return date > now || 'Expiration date must be in the future';
-                      },
                     })}
                     })}
                   />
                   />
                 </div>
                 </div>

+ 2 - 0
apps/app/src/server/crowi/index.js

@@ -18,6 +18,7 @@ import instanciatePageBulkExportJobCleanUpCronService, {
 import instanciatePageBulkExportJobCronService from '~/features/page-bulk-export/server/service/page-bulk-export-job-cron';
 import instanciatePageBulkExportJobCronService from '~/features/page-bulk-export/server/service/page-bulk-export-job-cron';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import questionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import questionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
+import { startCron as startAccessTokenCron } from '~/server/service/access-token';
 import { getGrowiVersion } from '~/utils/growi-version';
 import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
@@ -366,6 +367,7 @@ Crowi.prototype.setupCron = function() {
   pageBulkExportJobCleanUpCronService.startCron();
   pageBulkExportJobCleanUpCronService.startCron();
 
 
   startOpenaiCronIfEnabled();
   startOpenaiCronIfEnabled();
+  startAccessTokenCron();
 };
 };
 
 
 Crowi.prototype.setupQuestionnaireService = function() {
 Crowi.prototype.setupQuestionnaireService = function() {

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

@@ -99,7 +99,7 @@ accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types
 
 
 accessTokenSchema.statics.deleteExpiredToken = async function() {
 accessTokenSchema.statics.deleteExpiredToken = async function() {
   const now = new Date();
   const now = new Date();
-  await this.deleteMany({ expiredAt: { $lte: now } });
+  await this.deleteMany({ expiredAt: { $lt: now } });
 };
 };
 
 
 accessTokenSchema.statics.findUserIdByToken = async function(token: string, requiredScopes: Scope[]) {
 accessTokenSchema.statics.findUserIdByToken = async function(token: string, requiredScopes: Scope[]) {
@@ -109,12 +109,12 @@ accessTokenSchema.statics.findUserIdByToken = async function(token: string, requ
     return;
     return;
   }
   }
   const extractedScopes = extractScopes(requiredScopes);
   const extractedScopes = extractScopes(requiredScopes);
-  return this.findOne({ tokenHash, expiredAt: { $gt: now }, scopes: { $all: extractedScopes } }).select('user');
+  return this.findOne({ tokenHash, expiredAt: { $gte: now }, scopes: { $all: extractedScopes } }).select('user');
 };
 };
 
 
 accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId | string) {
 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 scopes description');
+  return this.find({ user: userId, expiredAt: { $gte: now } }).select('_id expiredAt scopes description');
 };
 };
 
 
 accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: Scope[]) {
 accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: Scope[]) {

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

@@ -44,7 +44,7 @@ const validator = [
       }
       }
 
 
       // Check if date is in the future
       // Check if date is in the future
-      if (expiredAt <= now) {
+      if (expiredAt < now) {
         throw new Error('Expiration date must be in the future');
         throw new Error('Expiration date must be in the future');
       }
       }
 
 

+ 56 - 0
apps/app/src/server/service/access-token/access-token-deletion-cron.ts

@@ -0,0 +1,56 @@
+import nodeCron from 'node-cron';
+
+import { AccessToken } from '~/server/models/access-token';
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:service:access-token-deletion-cron');
+
+export class AccessTokenDeletionCronService {
+
+  cronJob: nodeCron.ScheduledTask;
+
+  // Default execution at midnight
+  accessTokenDeletionCronExpression = '0 15 * * *';
+
+  startCron(): void {
+    const cronExp = configManager.getConfig('accessToken:deletionCronExpression');
+    if (cronExp != null) {
+      this.accessTokenDeletionCronExpression = cronExp;
+    }
+
+    this.cronJob?.stop();
+    this.cronJob = this.generateCronJob();
+    this.cronJob.start();
+
+    logger.info('Access token deletion cron started');
+  }
+
+  private async executeJob(): Promise<void> {
+    try {
+      await AccessToken.deleteExpiredToken();
+      logger.info('Expired access tokens have been deleted');
+    }
+    catch (e) {
+      logger.error('Failed to delete expired access tokens:', e);
+    }
+  }
+
+  private generateCronJob() {
+    return nodeCron.schedule(this.accessTokenDeletionCronExpression, async() => {
+      try {
+        await this.executeJob();
+      }
+      catch (e) {
+        logger.error('Error occurred during access token deletion cron job:', e);
+      }
+    });
+  }
+
+}
+
+export const startCron = (): void => {
+  logger.info('Starting cron service for access token deletion');
+  const accessTokenDeletionCronService = new AccessTokenDeletionCronService();
+  accessTokenDeletionCronService.startCron();
+};

+ 1 - 0
apps/app/src/server/service/access-token/index.ts

@@ -0,0 +1 @@
+export { startCron } from './access-token-deletion-cron';

+ 8 - 0
apps/app/src/server/service/config-manager/config-definition.ts

@@ -329,6 +329,8 @@ export const CONFIG_KEYS = [
   'app:isBulkExportPagesEnabled',
   'app:isBulkExportPagesEnabled',
   'env:useOnlyEnvVars:app:isBulkExportPagesEnabled',
   'env:useOnlyEnvVars:app:isBulkExportPagesEnabled',
 
 
+  // Access Token Settings
+  'accessToken:deletionCronExpression',
 ] as const;
 ] as const;
 
 
 
 
@@ -1332,6 +1334,12 @@ Guideline as a RAG:
     envVarName: 'BULK_EXPORT_PAGES_ENABLED_USES_ONLY_ENV_VARS',
     envVarName: 'BULK_EXPORT_PAGES_ENABLED_USES_ONLY_ENV_VARS',
     defaultValue: false,
     defaultValue: false,
   }),
   }),
+
+  // Access Token Settings
+  'accessToken:deletionCronExpression': defineConfig<string>({
+    envVarName: 'ACCESS_TOKEN_DELETION_CRON_EXPRESSION',
+    defaultValue: '0 15 * * *',
+  }),
 } as const;
 } as const;
 
 
 export type ConfigValues = {
 export type ConfigValues = {