diff --git a/packages/backend/server/src/__tests__/auth/guard.spec.ts b/packages/backend/server/src/__tests__/auth/guard.spec.ts index 6a0ebd0500..2260793d5b 100644 --- a/packages/backend/server/src/__tests__/auth/guard.spec.ts +++ b/packages/backend/server/src/__tests__/auth/guard.spec.ts @@ -4,6 +4,7 @@ import ava, { TestFn } from 'ava'; import Sinon from 'sinon'; import request from 'supertest'; +import { Due } from '../../base'; import { AuthModule, CurrentUser, Public, Session } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; import { Models } from '../../models'; @@ -125,7 +126,7 @@ test('should be able to refresh session if needed', async t => { sessionId, }, data: { - expiresAt: new Date(Date.now() + 1000 * 60 * 60 /* expires in 1 hour */), + expiresAt: Due.after('1h'), }, }); diff --git a/packages/backend/server/src/__tests__/doc/history.spec.ts b/packages/backend/server/src/__tests__/doc/history.spec.ts index 8600863edd..4f33d542d5 100644 --- a/packages/backend/server/src/__tests__/doc/history.spec.ts +++ b/packages/backend/server/src/__tests__/doc/history.spec.ts @@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client'; import test from 'ava'; import * as Sinon from 'sinon'; +import { Due } from '../../base'; import { DocStorageModule, PgWorkspaceDocStorageAdapter } from '../../core/doc'; import { DocStorageOptions } from '../../core/doc/options'; import { DocRecord } from '../../core/doc/storage'; @@ -122,7 +123,7 @@ test('should create history if time diff is larger than interval config and stat // @ts-expect-error private method Sinon.stub(adapter, 'lastDocHistory').resolves({ - timestamp: new Date(timestamp.getTime() - 1000 * 60 * 20), + timestamp: Due.before('20m', timestamp), state: Buffer.from([0, 1]), }); diff --git a/packages/backend/server/src/__tests__/models/session.spec.ts b/packages/backend/server/src/__tests__/models/session.spec.ts index 66630b2f6a..ee8257573b 100644 --- a/packages/backend/server/src/__tests__/models/session.spec.ts +++ b/packages/backend/server/src/__tests__/models/session.spec.ts @@ -5,6 +5,7 @@ import { Config } from '../../base/config'; import { SessionModel } from '../../models/session'; import { UserModel } from '../../models/user'; import { createTestingModule, type TestingModule } from '../utils'; +import { Due } from '../../base/utils'; interface Context { config: Config; @@ -137,9 +138,7 @@ test('should not refresh userSession when expires time not hit ttr', async t => let newExpiresAt = await t.context.session.refreshUserSessionIfNeeded(userSession); t.is(newExpiresAt, undefined); - userSession.expiresAt = new Date( - userSession.expiresAt!.getTime() - t.context.config.auth.session.ttr * 1000 - ); + userSession.expiresAt = Due.before(t.context.config.auth.session.ttr); newExpiresAt = await t.context.session.refreshUserSessionIfNeeded(userSession); t.is(newExpiresAt, undefined); @@ -154,9 +153,9 @@ test('should not refresh userSession when expires time hit ttr', async t => { user.id, session.id ); - const ttr = t.context.config.auth.session.ttr * 2; userSession.expiresAt = new Date( - userSession.expiresAt!.getTime() - ttr * 1000 + userSession.expiresAt!.getTime() - + Due.ms(t.context.config.auth.session.ttr) * 2 ); const newExpiresAt = await t.context.session.refreshUserSessionIfNeeded(userSession); diff --git a/packages/backend/server/src/__tests__/payment/service.spec.ts b/packages/backend/server/src/__tests__/payment/service.spec.ts index 4c0da9c178..5620b1f029 100644 --- a/packages/backend/server/src/__tests__/payment/service.spec.ts +++ b/packages/backend/server/src/__tests__/payment/service.spec.ts @@ -6,7 +6,7 @@ import Sinon from 'sinon'; import Stripe from 'stripe'; import { AppModule } from '../../app.module'; -import { EventBus } from '../../base'; +import { Due, EventBus } from '../../base'; import { ConfigFactory, ConfigModule } from '../../base/config'; import { CurrentUser } from '../../core/auth'; import { AuthService } from '../../core/auth/service'; @@ -129,8 +129,8 @@ const sub: Stripe.Subscription = { object: 'subscription', cancel_at_period_end: false, canceled_at: null, - current_period_end: unixNow() + 60 * 60 * 24 * 30, - current_period_start: unixNow() - 60 * 60 * 24 * 1, + current_period_end: Due.after('30d').getTime() / 1000, + current_period_start: Due.before('1d').getTime() / 1000, // @ts-expect-error stub customer: { id: 'cus_1', @@ -914,7 +914,7 @@ const subscriptionSchedule: Stripe.SubscriptionSchedule = { }, ], start_date: unixNow(), - end_date: unixNow() + 30 * 24 * 60 * 60, + end_date: Due.after('30d').getTime() / 1000, }, { items: [ @@ -924,7 +924,7 @@ const subscriptionSchedule: Stripe.SubscriptionSchedule = { quantity: 1, }, ], - start_date: unixNow() + 30 * 24 * 60 * 60, + start_date: Due.after('30d').getTime() / 1000, }, ], }; @@ -1550,10 +1550,7 @@ test('should be able to subscribe onetime payment subscription', async t => { t.is(subInDB?.recurring, SubscriptionRecurring.Monthly); t.is(subInDB?.status, SubscriptionStatus.Active); t.is(subInDB?.stripeSubscriptionId, null); - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toDateString() - ); + t.is(subInDB?.end?.toDateString(), Due.after('30d').toDateString()); }); test('should be able to accumulate onetime payment subscription period', async t => { @@ -1574,7 +1571,7 @@ test('should be able to accumulate onetime payment subscription period', async t }); // add 365 days - t.is(subInDB!.end!.getTime(), end.getTime() + 365 * 24 * 60 * 60 * 1000); + t.is(subInDB!.end!.getTime(), Due.after('1y', end).getTime()); }); test('should be able to recalculate onetime payment subscription period after expiration', async t => { @@ -1599,10 +1596,7 @@ test('should be able to recalculate onetime payment subscription period after ex }); // add 365 days from now - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString() - ); + t.is(subInDB?.end?.toDateString(), Due.after('1y').toDateString()); }); test('should not accumulate onetime payment subscription period for redeemed invoices', async t => { @@ -1617,10 +1611,7 @@ test('should not accumulate onetime payment subscription period for redeemed inv where: { targetId: u1.id }, }); - t.is( - subInDB?.end?.toDateString(), - new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString() - ); + t.is(subInDB?.end?.toDateString(), Due.after('1y').toDateString()); }); // TEAM diff --git a/packages/backend/server/src/base/cache/provider.ts b/packages/backend/server/src/base/cache/provider.ts index 397ab66ac3..91813f9980 100644 --- a/packages/backend/server/src/base/cache/provider.ts +++ b/packages/backend/server/src/base/cache/provider.ts @@ -1,10 +1,11 @@ import Redis from 'ioredis'; +import { type Duration, Due } from '../utils'; export interface CacheSetOptions { /** * in milliseconds */ - ttl?: number; + ttl?: Duration; } export class CacheProvider { @@ -30,7 +31,7 @@ export class CacheProvider { ): Promise { if (opts.ttl) { return this.redis - .set(key, JSON.stringify(value), 'PX', opts.ttl) + .set(key, JSON.stringify(value), 'PX', Due.ms(opts.ttl)) .then(() => true) .catch(() => false); } @@ -56,7 +57,7 @@ export class CacheProvider { ): Promise { if (opts.ttl) { return this.redis - .set(key, JSON.stringify(value), 'PX', opts.ttl, 'NX') + .set(key, JSON.stringify(value), 'PX', Due.ms(opts.ttl), 'NX') .then(v => !!v) .catch(() => false); } diff --git a/packages/backend/server/src/base/config/__tests__/config.spec.ts b/packages/backend/server/src/base/config/__tests__/config.spec.ts index 55185b4592..ed5eef725d 100644 --- a/packages/backend/server/src/base/config/__tests__/config.spec.ts +++ b/packages/backend/server/src/base/config/__tests__/config.spec.ts @@ -103,8 +103,8 @@ test('should override correctly', t => { // keyed config // 'session.ttl', 'session.ttr' session: { - ttl: 2000, - ttr: 1000, + ttl: '1M', + ttr: '1d', }, }, storages: { @@ -131,7 +131,7 @@ test('should override correctly', t => { }, allowSignup: true, session: { - ttl: 3000, + ttl: '2M', }, }, storages: { @@ -159,8 +159,8 @@ test('should override correctly', t => { // right merged to left t.deepEqual(config.auth.session, { - ttl: 3000, - ttr: 1000, + ttl: '2M', + ttr: '1d', }); // right covered left diff --git a/packages/backend/server/src/base/job/queue/index.ts b/packages/backend/server/src/base/job/queue/index.ts index f6f954e4ad..a67274d620 100644 --- a/packages/backend/server/src/base/job/queue/index.ts +++ b/packages/backend/server/src/base/job/queue/index.ts @@ -6,6 +6,7 @@ import { type QueueOptions } from 'bullmq'; import { Config } from '../../config'; import { QueueRedis } from '../../redis'; +import { Due } from '../../utils'; import { Queue, QUEUES } from './def'; import { JobExecutor } from './executor'; import { JobQueue } from './queue'; @@ -40,7 +41,7 @@ export class JobModule { ...QUEUES.map(name => { if (name === Queue.NIGHTLY_JOB) { // avoid nightly jobs been run multiple times - return { name, removeOnComplete: { age: 1000 * 60 * 60 } }; + return { name, removeOnComplete: { age: Due.ms('1m') } }; } return { name }; }) diff --git a/packages/backend/server/src/base/storage/providers/s3.ts b/packages/backend/server/src/base/storage/providers/s3.ts index 188a1c18de..21ae75f01d 100644 --- a/packages/backend/server/src/base/storage/providers/s3.ts +++ b/packages/backend/server/src/base/storage/providers/s3.ts @@ -22,7 +22,7 @@ import { PutObjectMetadata, StorageProvider, } from './provider'; -import { autoMetadata, SIGNED_URL_EXPIRED, toBuffer } from './utils'; +import { autoMetadata, SIGNED_URL_EXPIRED_SEC, toBuffer } from './utils'; export interface S3StorageConfig extends S3ClientConfig { usePresignedURL?: { @@ -138,7 +138,7 @@ export class S3StorageProvider implements StorageProvider { Bucket: this.bucket, Key: key, }), - { expiresIn: SIGNED_URL_EXPIRED } + { expiresIn: SIGNED_URL_EXPIRED_SEC } ); return { diff --git a/packages/backend/server/src/base/storage/providers/utils.ts b/packages/backend/server/src/base/storage/providers/utils.ts index 4f926e8201..7ab7045dee 100644 --- a/packages/backend/server/src/base/storage/providers/utils.ts +++ b/packages/backend/server/src/base/storage/providers/utils.ts @@ -4,6 +4,7 @@ import { crc32 } from '@node-rs/crc32'; import { getStreamAsBuffer } from 'get-stream'; import { getMime } from '../../../native'; +import { Due } from '../../utils'; import { BlobInputType, PutObjectMetadata } from './provider'; export async function toBuffer(input: BlobInputType): Promise { @@ -43,4 +44,4 @@ export function autoMetadata( return metadata; } -export const SIGNED_URL_EXPIRED = 60 * 60; // 1 hour +export const SIGNED_URL_EXPIRED_SEC = Due.s('1h'); diff --git a/packages/backend/server/src/base/utils/duration.ts b/packages/backend/server/src/base/utils/duration.ts index e6fb5abf40..7f6c815d3e 100644 --- a/packages/backend/server/src/base/utils/duration.ts +++ b/packages/backend/server/src/base/utils/duration.ts @@ -53,26 +53,28 @@ function parse(str: string): DurationInput { return input; } +export type Duration = string | DurationInput; + export const Due = { - ms: (dueStr: string | DurationInput) => { - const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr; + ms: (duration: Duration) => { + const input = typeof duration === 'string' ? parse(duration) : duration; return Object.entries(input).reduce((duration, [unit, val]) => { return duration + UnitToSecMap[unit as DurationUnit] * (val || 0) * 1000; }, 0); }, - s: (dueStr: string | DurationInput) => { - const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr; + s: (duration: Duration) => { + const input = typeof duration === 'string' ? parse(duration) : duration; return Object.entries(input).reduce((duration, [unit, val]) => { return duration + UnitToSecMap[unit as DurationUnit] * (val || 0); }, 0); }, parse, - after: (dueStr: string | number | DurationInput, date?: Date) => { - const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr); + after: (duration: Duration, date?: Date) => { + const timestamp = Due.ms(duration); return new Date((date?.getTime() ?? Date.now()) + timestamp); }, - before: (dueStr: string | number | DurationInput, date?: Date) => { - const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr); + before: (duration: Duration, date?: Date) => { + const timestamp = Due.ms(duration); return new Date((date?.getTime() ?? Date.now()) - timestamp); }, }; diff --git a/packages/backend/server/src/base/utils/unit.ts b/packages/backend/server/src/base/utils/unit.ts index f0798822b6..9603b2ad9e 100644 --- a/packages/backend/server/src/base/utils/unit.ts +++ b/packages/backend/server/src/base/utils/unit.ts @@ -1,4 +1,3 @@ export const OneKB = 1024; export const OneMB = OneKB * OneKB; export const OneGB = OneKB * OneMB; -export const OneDay = 1000 * 60 * 60 * 24; diff --git a/packages/backend/server/src/core/auth/config.ts b/packages/backend/server/src/core/auth/config.ts index 05e5695c6f..73bf3bfbf4 100644 --- a/packages/backend/server/src/core/auth/config.ts +++ b/packages/backend/server/src/core/auth/config.ts @@ -4,8 +4,8 @@ import { defineModuleConfig } from '../../base'; export interface AuthConfig { session: { - ttl: number; - ttr: number; + ttl: string; + ttr: string; }; allowSignup: boolean; requireEmailDomainVerification: boolean; @@ -60,10 +60,10 @@ defineModuleConfig('auth', { }, 'session.ttl': { desc: 'Application auth expiration time in seconds.', - default: 60 * 60 * 24 * 15, // 15 days + default: '15d', }, 'session.ttr': { desc: 'Application auth time to refresh in seconds.', - default: 60 * 60 * 24 * 7, // 7 days + default: '7d', }, }); diff --git a/packages/backend/server/src/core/auth/controller.ts b/packages/backend/server/src/core/auth/controller.ts index 809d0b3dc2..ab32ae6f27 100644 --- a/packages/backend/server/src/core/auth/controller.ts +++ b/packages/backend/server/src/core/auth/controller.ts @@ -199,21 +199,17 @@ export class AuthController { throw new WrongSignInCredentials({ email }); } - const ttlInSec = 30 * 60; + const ttl = '30m'; const token = await this.models.verificationToken.create( TokenType.SignIn, email, - ttlInSec + ttl ); const otp = this.crypto.otp(); // TODO(@forehalo): this is a temporary solution, we should not rely on cache to store the otp const cacheKey = OTP_CACHE_KEY(otp); - await this.cache.set( - cacheKey, - { token, clientNonce }, - { ttl: ttlInSec * 1000 } - ); + await this.cache.set(cacheKey, { token, clientNonce }, { ttl }); const magicLink = this.url.link(callbackUrl, { token: otp, diff --git a/packages/backend/server/src/core/auth/service.ts b/packages/backend/server/src/core/auth/service.ts index c50273aa94..74b72e7ca9 100644 --- a/packages/backend/server/src/core/auth/service.ts +++ b/packages/backend/server/src/core/auth/service.ts @@ -2,7 +2,7 @@ import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; import type { CookieOptions, Request, Response } from 'express'; import { assign, pick } from 'lodash-es'; -import { Config, SignUpForbidden } from '../../base'; +import { Config, type Duration, SignUpForbidden } from '../../base'; import { Models, type User, type UserSession } from '../../models'; import { FeatureService } from '../features'; import { Mailer } from '../mail/mailer'; @@ -128,7 +128,7 @@ export class AuthService implements OnApplicationBootstrap { return await this.models.session.findUserSessionsBySessionId(sessionId); } - async createUserSession(userId: string, sessionId?: string, ttl?: number) { + async createUserSession(userId: string, sessionId?: string, ttl?: Duration) { return await this.models.session.createOrRefreshUserSession( userId, sessionId, @@ -157,7 +157,7 @@ export class AuthService implements OnApplicationBootstrap { async refreshUserSessionIfNeeded( res: Response, userSession: UserSession, - ttr?: number + ttr?: Duration ): Promise { const newExpiresAt = await this.models.session.refreshUserSessionIfNeeded( userSession, diff --git a/packages/backend/server/src/core/doc/adapters/workspace.ts b/packages/backend/server/src/core/doc/adapters/workspace.ts index 77530ff68e..fd6c712552 100644 --- a/packages/backend/server/src/core/doc/adapters/workspace.ts +++ b/packages/backend/server/src/core/doc/adapters/workspace.ts @@ -4,6 +4,7 @@ import { chunk } from 'lodash-es'; import { DocHistoryNotFound, DocNotFound, + Due, EventBus, FailedToSaveUpdates, FailedToUpsertSnapshot, @@ -251,7 +252,8 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter { force || // last history created before interval in configs lastHistoryTimestamp < - snapshot.timestamp - this.options.historyMinInterval(snapshot.spaceId) + snapshot.timestamp - + Due.ms(this.options.historyMinInterval(snapshot.spaceId)) ) { shouldCreateHistory = true; } diff --git a/packages/backend/server/src/core/doc/config.ts b/packages/backend/server/src/core/doc/config.ts index ca61f34d00..40e841a6f5 100644 --- a/packages/backend/server/src/core/doc/config.ts +++ b/packages/backend/server/src/core/doc/config.ts @@ -4,7 +4,7 @@ declare global { interface AppConfigSchema { doc: { history: { - interval: number; + interval: string; }; experimental: { yocto: boolean; @@ -20,6 +20,6 @@ defineModuleConfig('doc', { }, 'history.interval': { desc: 'The minimum time interval in milliseconds of creating a new history snapshot when doc get updated.', - default: 1000 * 60 * 10 /* 10 mins */, + default: '10m', }, }); diff --git a/packages/backend/server/src/core/doc/reader.ts b/packages/backend/server/src/core/doc/reader.ts index 2c6da66edf..a377ffb1e9 100644 --- a/packages/backend/server/src/core/doc/reader.ts +++ b/packages/backend/server/src/core/doc/reader.ts @@ -25,8 +25,6 @@ import { import { PgWorkspaceDocStorageAdapter } from './adapters/workspace'; import { type DocDiff, type DocRecord } from './storage'; -const DOC_CONTENT_CACHE_7_DAYS = 7 * 24 * 60 * 60 * 1000; - export interface WorkspaceDocInfo { id: string; name: string; @@ -90,7 +88,7 @@ export abstract class DocReader { const content = await this.getDocContentWithoutCache(workspaceId, docId); if (content) { await this.cache.set(cacheKey, content, { - ttl: DOC_CONTENT_CACHE_7_DAYS, + ttl: '7d', }); } return content; diff --git a/packages/backend/server/src/core/notification/__tests__/service.spec.ts b/packages/backend/server/src/core/notification/__tests__/service.spec.ts index ff0849ee21..a6025d5bfd 100644 --- a/packages/backend/server/src/core/notification/__tests__/service.spec.ts +++ b/packages/backend/server/src/core/notification/__tests__/service.spec.ts @@ -8,7 +8,7 @@ import { createTestingModule, type TestingModule, } from '../../../__tests__/utils'; -import { NotificationNotFound } from '../../../base'; +import { Due, NotificationNotFound } from '../../../base'; import { DocMode, MentionNotificationBody, @@ -381,7 +381,7 @@ test('should clean expired notifications', async t => { // wait for 100 days mock.timers.enable({ apis: ['Date'], - now: Date.now() + 1000 * 60 * 60 * 24 * 100, + now: Due.after('100d'), }); await t.context.models.notification.cleanExpiredNotifications(); count = await notificationService.countByUserId(member.id); @@ -390,7 +390,7 @@ test('should clean expired notifications', async t => { // wait for 1 year mock.timers.enable({ apis: ['Date'], - now: Date.now() + 1000 * 60 * 60 * 24 * 365, + now: Due.after('1y'), }); await t.context.models.notification.cleanExpiredNotifications(); count = await notificationService.countByUserId(member.id); diff --git a/packages/backend/server/src/core/quota/utils.ts b/packages/backend/server/src/core/quota/utils.ts index b4688799d1..b075838daf 100644 --- a/packages/backend/server/src/core/quota/utils.ts +++ b/packages/backend/server/src/core/quota/utils.ts @@ -1,4 +1,4 @@ -import { OneDay, OneKB } from '../../base'; +import { Due, OneKB } from '../../base'; export const ByteUnit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; @@ -14,6 +14,7 @@ export function formatSize(bytes: number, decimals: number = 2): string { ); } +const ONE_DAY_IN_MS = Due.ms('1d'); export function formatDate(ms: number): string { - return `${(ms / OneDay).toFixed(0)} days`; + return `${(ms / ONE_DAY_IN_MS).toFixed(0)} days`; } diff --git a/packages/backend/server/src/core/workspaces/resolvers/doc.ts b/packages/backend/server/src/core/workspaces/resolvers/doc.ts index c1b6e2eb5c..2c0a66f38e 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/doc.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/doc.ts @@ -403,8 +403,7 @@ export class WorkspaceDocResolver { const allowed = await this.cache.setnx( `fixingOwner:${workspaceId}:${docId}`, 1, - // TODO(@forehalo): we definitely need a timer helper - { ttl: 1000 * 60 * 60 * 24 } + { ttl: '1d' } ); // fixed by other instance diff --git a/packages/backend/server/src/core/workspaces/types.ts b/packages/backend/server/src/core/workspaces/types.ts index c09ac4c5f3..7d30846ed5 100644 --- a/packages/backend/server/src/core/workspaces/types.ts +++ b/packages/backend/server/src/core/workspaces/types.ts @@ -174,13 +174,11 @@ export class InviteResult { error?: object; } -const Day = 24 * 60 * 60 * 1000; - export enum WorkspaceInviteLinkExpireTime { - OneDay = Day, - ThreeDays = 3 * Day, - OneWeek = 7 * Day, - OneMonth = 30 * Day, + OneDay = '1d', + ThreeDays = '3d', + OneWeek = '1w', + OneMonth = '1M', } registerEnumType(WorkspaceInviteLinkExpireTime, { diff --git a/packages/backend/server/src/models/__tests__/notification.spec.ts b/packages/backend/server/src/models/__tests__/notification.spec.ts index 47602f2a29..a943db94f8 100644 --- a/packages/backend/server/src/models/__tests__/notification.spec.ts +++ b/packages/backend/server/src/models/__tests__/notification.spec.ts @@ -4,6 +4,7 @@ import { mock } from 'node:test'; import ava, { TestFn } from 'ava'; import { createTestingModule, type TestingModule } from '../../__tests__/utils'; +import { Due } from '../../base'; import { Config } from '../../base/config'; import { DocMode, @@ -259,7 +260,7 @@ test('should clean expired notifications', async t => { // wait for 1 year mock.timers.enable({ apis: ['Date'], - now: Date.now() + 1000 * 60 * 60 * 24 * 365, + now: Due.after('1y'), }); count = await t.context.models.notification.cleanExpiredNotifications(); t.is(count, 1); diff --git a/packages/backend/server/src/models/common/feature.ts b/packages/backend/server/src/models/common/feature.ts index 1fc5117fa9..ba765111ac 100644 --- a/packages/backend/server/src/models/common/feature.ts +++ b/packages/backend/server/src/models/common/feature.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { OneDay, OneGB, OneMB } from '../../base'; +import { Due, OneGB, OneMB } from '../../base'; const UserPlanQuotaConfig = z.object({ // quota name @@ -104,7 +104,7 @@ export const FeatureConfigs: { blobLimit: 10 * OneMB, businessBlobLimit: 100 * OneMB, storageQuota: 10 * OneGB, - historyPeriod: 7 * OneDay, + historyPeriod: Due.ms('7d'), memberLimit: 3, copilotActionLimit: 10, }, @@ -116,7 +116,7 @@ export const FeatureConfigs: { name: 'Pro', blobLimit: 100 * OneMB, storageQuota: 100 * OneGB, - historyPeriod: 30 * OneDay, + historyPeriod: Due.ms('30d'), memberLimit: 10, copilotActionLimit: 10, }, @@ -128,7 +128,7 @@ export const FeatureConfigs: { name: 'Lifetime Pro', blobLimit: 100 * OneMB, storageQuota: 1024 * OneGB, - historyPeriod: 30 * OneDay, + historyPeriod: Due.ms('30d'), memberLimit: 10, copilotActionLimit: 10, }, @@ -141,7 +141,7 @@ export const FeatureConfigs: { blobLimit: 500 * OneMB, storageQuota: 100 * OneGB, seatQuota: 20 * OneGB, - historyPeriod: 30 * OneDay, + historyPeriod: Due.ms('30d'), memberLimit: 1, }, }, diff --git a/packages/backend/server/src/models/notification.ts b/packages/backend/server/src/models/notification.ts index a180c4befa..28d2b1a2ec 100644 --- a/packages/backend/server/src/models/notification.ts +++ b/packages/backend/server/src/models/notification.ts @@ -7,7 +7,7 @@ import { } from '@prisma/client'; import { z } from 'zod'; -import { PaginationInput } from '../base'; +import { Due, PaginationInput } from '../base'; import { BaseModel } from './base'; import { DocMode } from './common'; @@ -15,8 +15,6 @@ export { NotificationLevel, NotificationType }; export type { Notification }; // #region input - -export const ONE_YEAR = 1000 * 60 * 60 * 24 * 365; const IdSchema = z.string().trim().min(1).max(100); export const BaseNotificationCreateSchema = z.object({ @@ -237,7 +235,7 @@ export class NotificationModel extends BaseModel { async cleanExpiredNotifications() { const { count } = await this.db.notification.deleteMany({ // delete notifications that are older than one year - where: { createdAt: { lte: new Date(Date.now() - ONE_YEAR) } }, + where: { createdAt: { lte: Due.before('1y') } }, }); if (count > 0) { this.logger.log(`Deleted ${count} expired notifications`); diff --git a/packages/backend/server/src/models/session.ts b/packages/backend/server/src/models/session.ts index 44940b27cb..e35024b67a 100644 --- a/packages/backend/server/src/models/session.ts +++ b/packages/backend/server/src/models/session.ts @@ -6,7 +6,7 @@ import { type UserSession, } from '@prisma/client'; -import { Config } from '../base'; +import { Config, Due, Duration } from '../base'; import { BaseModel } from './base'; export type { Session, UserSession }; @@ -46,7 +46,7 @@ export class SessionModel extends BaseModel { async createOrRefreshUserSession( userId: string, sessionId?: string, - ttl = this.config.auth.session.ttl + ttl: Duration = this.config.auth.session.ttl ) { // check whether given session is valid if (sessionId) { @@ -66,7 +66,7 @@ export class SessionModel extends BaseModel { sessionId = session.id; } - const expiresAt = new Date(Date.now() + ttl * 1000); + const expiresAt = Due.after(ttl); return await this.db.userSession.upsert({ where: { sessionId_userId: { @@ -87,19 +87,17 @@ export class SessionModel extends BaseModel { async refreshUserSessionIfNeeded( userSession: UserSession, - ttr = this.config.auth.session.ttr + ttr: Duration = this.config.auth.session.ttr ): Promise { if ( userSession.expiresAt && - userSession.expiresAt.getTime() - Date.now() > ttr * 1000 + Due.before(ttr, userSession.expiresAt) > new Date() ) { // no need to refresh return; } - const newExpiresAt = new Date( - Date.now() + this.config.auth.session.ttl * 1000 - ); + const newExpiresAt = Due.after(this.config.auth.session.ttl); await this.db.userSession.update({ where: { id: userSession.id, diff --git a/packages/backend/server/src/models/verification-token.ts b/packages/backend/server/src/models/verification-token.ts index 5da61e5190..65a23762f1 100644 --- a/packages/backend/server/src/models/verification-token.ts +++ b/packages/backend/server/src/models/verification-token.ts @@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { type VerificationToken } from '@prisma/client'; +import { Due, Duration } from '../base'; import { CryptoHelper } from '../base/helpers'; import { BaseModel } from './base'; @@ -25,18 +26,14 @@ export class VerificationTokenModel extends BaseModel { /** * create token by type and credential (optional) with ttl in seconds (default 30 minutes) */ - async create( - type: TokenType, - credential?: string, - ttlInSec: number = 30 * 60 - ) { + async create(type: TokenType, credential?: string, ttl: Duration = '30m') { const plaintextToken = randomUUID(); const { token } = await this.db.verificationToken.create({ data: { type, token: plaintextToken, credential, - expiresAt: new Date(Date.now() + ttlInSec * 1000), + expiresAt: Due.after(ttl), }, }); return this.crypto.encrypt(token); diff --git a/packages/backend/server/src/plugins/captcha/service.ts b/packages/backend/server/src/plugins/captcha/service.ts index c687abdc3c..0d601ef2b7 100644 --- a/packages/backend/server/src/plugins/captcha/service.ts +++ b/packages/backend/server/src/plugins/captcha/service.ts @@ -78,7 +78,7 @@ export class CaptchaService { const challenge = await this.models.verificationToken.create( TokenType.Challenge, resource, - 5 * 60 + '5m' ); return { diff --git a/packages/backend/server/src/plugins/copilot/message.ts b/packages/backend/server/src/plugins/copilot/message.ts index 4dd7e987ac..90f30cee8b 100644 --- a/packages/backend/server/src/plugins/copilot/message.ts +++ b/packages/backend/server/src/plugins/copilot/message.ts @@ -6,7 +6,6 @@ import { SessionCache } from '../../base'; import { SubmittedMessage, SubmittedMessageSchema } from './types'; const CHAT_MESSAGE_KEY = 'chat-message'; -const CHAT_MESSAGE_TTL = 3600 * 1 * 1000; // 1 hours @Injectable() export class ChatMessageCache { @@ -20,7 +19,7 @@ export class ChatMessageCache { const parsedMessage = SubmittedMessageSchema.parse(message); const id = randomUUID(); await this.cache.set(`${CHAT_MESSAGE_KEY}:${id}`, parsedMessage, { - ttl: CHAT_MESSAGE_TTL, + ttl: '1h', }); return id; } diff --git a/packages/backend/server/src/plugins/indexer/__tests__/job.spec.ts b/packages/backend/server/src/plugins/indexer/__tests__/job.spec.ts index 8977879711..2642299d96 100644 --- a/packages/backend/server/src/plugins/indexer/__tests__/job.spec.ts +++ b/packages/backend/server/src/plugins/indexer/__tests__/job.spec.ts @@ -6,7 +6,7 @@ import Sinon from 'sinon'; import { createModule } from '../../../__tests__/create-module'; import { Mockers } from '../../../__tests__/mocks'; -import { JOB_SIGNAL } from '../../../base'; +import { Due, JOB_SIGNAL } from '../../../base'; import { ConfigModule } from '../../../base/config'; import { ServerConfigModule } from '../../../core/config'; import { Models } from '../../../models'; @@ -160,7 +160,7 @@ test('should not index workspace if it is not updated in 180 days', async t => { user, workspaceId: workspace.id, docId: workspace.id, - updatedAt: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000 - 1), + updatedAt: Due.before('181d'), }); const count = module.queue.count('indexer.indexWorkspace'); diff --git a/packages/backend/server/src/plugins/indexer/job.ts b/packages/backend/server/src/plugins/indexer/job.ts index f68b2c590b..7863375da9 100644 --- a/packages/backend/server/src/plugins/indexer/job.ts +++ b/packages/backend/server/src/plugins/indexer/job.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; -import { Config, JOB_SIGNAL, JobQueue, OnJob } from '../../base'; +import { Config, Due, JOB_SIGNAL, JobQueue, OnJob } from '../../base'; import { readAllDocIdsFromWorkspaceSnapshot } from '../../core/utils/blocksuite'; import { Models } from '../../models'; import { IndexerService } from './service'; @@ -182,8 +182,7 @@ export class IndexerJob { // ignore 180 days not updated workspaces if ( !snapshotMeta?.updatedAt || - Date.now() - snapshotMeta.updatedAt.getTime() > - 180 * 24 * 60 * 60 * 1000 + snapshotMeta.updatedAt < Due.before('180d') ) { continue; } diff --git a/packages/backend/server/src/plugins/license/service.ts b/packages/backend/server/src/plugins/license/service.ts index b8a9d2d559..d5525c4e1d 100644 --- a/packages/backend/server/src/plugins/license/service.ts +++ b/packages/backend/server/src/plugins/license/service.ts @@ -7,6 +7,7 @@ import { z } from 'zod'; import { CryptoHelper, + Due, EventBus, InternalServerError, InvalidLicenseToActivate, @@ -337,7 +338,7 @@ export class LicenseService { const licenses = await this.db.installedLicense.findMany({ where: { validatedAt: { - lte: new Date(Date.now() - 1000 * 60 * 60 /* 1h */), + lte: Due.before('1h'), }, }, }); diff --git a/packages/backend/server/src/plugins/oauth/service.ts b/packages/backend/server/src/plugins/oauth/service.ts index b6f06b6053..a943b27ef3 100644 --- a/packages/backend/server/src/plugins/oauth/service.ts +++ b/packages/backend/server/src/plugins/oauth/service.ts @@ -29,7 +29,7 @@ export class OAuthService { async saveOAuthState(state: OAuthState) { const token = randomUUID(); await this.cache.set(`${OAUTH_STATE_KEY}:${token}`, state, { - ttl: 3600 * 3 * 1000 /* 3 hours */, + ttl: '3h', }); return token; diff --git a/packages/backend/server/src/plugins/worker/controller.ts b/packages/backend/server/src/plugins/worker/controller.ts index b2e2bd86ce..1521819d4e 100644 --- a/packages/backend/server/src/plugins/worker/controller.ts +++ b/packages/backend/server/src/plugins/worker/controller.ts @@ -26,9 +26,6 @@ import { } from './utils'; import { decodeWithCharset } from './utils/encoding'; -// cache for 30 minutes -const CACHE_TTL = 1000 * 60 * 30; - @Public() @UseNamedGuard('selfhost') @Controller('/api/worker') @@ -98,7 +95,7 @@ export class WorkerController { if (contentType?.startsWith('image/')) { const buffer = Buffer.from(await response.arrayBuffer()); await this.cache.set(cachedUrl, buffer.toString('base64'), { - ttl: CACHE_TTL, + ttl: '30m', }); const contentDisposition = response.headers.get('Content-Disposition'); return resp @@ -118,7 +115,7 @@ export class WorkerController { if (response.status >= 400 && response.status < 500) { // rejected by server, cache a empty response await this.cache.set(cachedUrl, Buffer.from([]).toString('base64'), { - ttl: CACHE_TTL, + ttl: '30m', }); } this.logger.error('Failed to fetch image', { @@ -302,7 +299,7 @@ export class WorkerController { responseSize: json.length, }); - await this.cache.set(cachedUrl, res, { ttl: CACHE_TTL }); + await this.cache.set(cachedUrl, res, { ttl: '30m' }); return resp .status(200) .header({