0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

refactor: replace dayjs with date-fns

This commit is contained in:
Gao Sun 2022-11-07 14:33:47 +08:00
parent 877eb892c9
commit 15bb084b6e
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
22 changed files with 193 additions and 133 deletions

View file

@ -47,7 +47,6 @@
"clean-deep": "^3.4.0",
"cross-env": "^7.0.3",
"csstype": "^3.0.11",
"dayjs": "^1.10.5",
"deepmerge": "^4.2.2",
"dnd-core": "^16.0.0",
"eslint": "^8.21.0",
@ -97,5 +96,8 @@
"stylelint": {
"extends": "@silverhand/eslint-config-react/.stylelintrc"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {
"date-fns": "^2.29.3"
}
}

View file

@ -1,18 +1,18 @@
import type { Nullable } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { isValid } from 'date-fns';
type Props = {
children: Nullable<string | number>;
};
const DateTime = ({ children }: Props) => {
const date = dayjs(children);
const date = children && new Date(children);
if (!children || !date.isValid()) {
if (!date || !isValid(date)) {
return <span>-</span>;
}
return <span>{date.toDate().toLocaleDateString()}</span>;
return <span>{date.toLocaleDateString()}</span>;
};
export default DateTime;

View file

@ -1,6 +1,5 @@
import type { LogDto, User } from '@logto/schemas';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import useSWR from 'swr';
@ -85,7 +84,7 @@ const AuditLogDetails = () => {
</div>
<div className={styles.infoItem}>
<div className={styles.label}>{t('log_details.time')}</div>
<div>{dayjs(data.createdAt).toDate().toLocaleString()}</div>
<div>{new Date(data.createdAt).toLocaleString()}</div>
</div>
</div>
<div>

View file

@ -1,4 +1,4 @@
import dayjs from 'dayjs';
import { format } from 'date-fns';
import type { ChangeEventHandler } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -36,7 +36,7 @@ const tickFormatter = new Intl.NumberFormat('en-US', {
});
const Dashboard = () => {
const [date, setDate] = useState<string>(dayjs().format('YYYY-MM-DD'));
const [date, setDate] = useState<string>(format(Date.now(), 'yyyy-MM-dd'));
const { data: totalData, error: totalError } = useSWR<TotalUsersResponse, RequestError>(
'/api/dashboard/users/total'
);

View file

@ -4,7 +4,7 @@ import type { ConnectorResponse, ConnectorMetadata, SignInExperience } from '@lo
import { AppearanceMode } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { format } from 'date-fns';
import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
@ -195,7 +195,7 @@ const Preview = ({ signInExperience, className }: Props) => {
<div className={classNames(styles.device, styles[mode])}>
{platform !== 'desktopWeb' && (
<div className={styles.topBar}>
<div className={styles.time}>{dayjs().format('HH:mm')}</div>
<div className={styles.time}>{format(Date.now(), 'HH:mm')}</div>
<PhoneInfo />
</div>
)}

View file

@ -30,7 +30,7 @@
"@silverhand/essentials": "^1.3.0",
"chalk": "^4",
"clean-deep": "^3.4.0",
"dayjs": "^1.10.5",
"date-fns": "^2.29.3",
"debug": "^4.3.4",
"decamelize": "^5.0.0",
"deepmerge": "^4.2.2",

View file

@ -35,10 +35,9 @@ jest.mock('@logto/shared', () => ({
const now = Date.now();
jest.mock(
'dayjs',
// eslint-disable-next-line unicorn/consistent-function-scoping
jest.fn(() => () => ({
add: jest.fn((delta: number) => new Date(now + delta * 1000)),
'date-fns',
jest.fn(() => ({
add: jest.fn((_: Date, { seconds }: { seconds: number }) => new Date(now + seconds * 1000)),
}))
);

View file

@ -1,7 +1,7 @@
import type { CreateApplication, OidcClientMetadata } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import { adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas/lib/seeds';
import dayjs from 'dayjs';
import { add } from 'date-fns';
import type { AdapterFactory, AllClientMetadata } from 'oidc-provider';
import snakecaseKeys from 'snakecase-keys';
@ -99,7 +99,7 @@ export default function postgresAdapter(modelName: string): ReturnType<AdapterFa
modelName,
id,
payload,
expiresAt: dayjs().add(expiresIn, 'second').valueOf(),
expiresAt: add(Date.now(), { seconds: expiresIn }).valueOf(),
}),
find: async (id) => findPayloadById(modelName, id),
findByUserCode: async (userCode) => findPayloadByPayloadField(modelName, 'userCode', userCode),

View file

@ -7,7 +7,7 @@ import { OidcModelInstances } from '@logto/schemas';
import { convertToIdentifiers, convertToTimestamp } from '@logto/shared';
import type { Nullable } from '@silverhand/essentials';
import { conditional } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { add, isBefore } from 'date-fns';
import type { ValueExpression } from 'slonik';
import { sql } from 'slonik';
@ -30,7 +30,7 @@ const isConsumed = (modelName: string, consumedAt: Nullable<number>): boolean =>
return Boolean(consumedAt);
}
return dayjs(consumedAt).add(refreshTokenReuseInterval, 'seconds').isBefore(dayjs());
return isBefore(add(consumedAt, { seconds: refreshTokenReuseInterval }), Date.now());
};
const withConsumed = <T>(

View file

@ -1,4 +1,8 @@
import dayjs from 'dayjs';
// The FP version works better for `format()`
/* eslint-disable import/no-duplicates */
import { endOfDay, subDays } from 'date-fns';
import { format } from 'date-fns/fp';
/* eslint-enable import/no-duplicates */
import dashboardRoutes from '@/routes/dashboard';
import { createRequester } from '@/utils/test-utils';
@ -8,6 +12,7 @@ const countUsers = jest.fn(async () => ({ count: totalUserCount }));
const getDailyNewUserCountsByTimeInterval = jest.fn(
async (startTimeExclusive: number, endTimeInclusive: number) => mockDailyNewUserCounts
);
const formatToQueryDate = format('yyyy-MM-dd');
jest.mock('@/queries/user', () => ({
countUsers: async () => countUsers(),
@ -83,8 +88,8 @@ describe('dashboardRoutes', () => {
it('should call getDailyNewUserCountsByTimeInterval with the time interval (14 days ago 23:59:59.999, today 23:59:59.999]', async () => {
await logRequest.get('/dashboard/users/new');
expect(getDailyNewUserCountsByTimeInterval).toHaveBeenCalledWith(
dayjs().endOf('day').subtract(14, 'day').valueOf(),
dayjs().endOf('day').valueOf()
subDays(endOfDay(Date.now()), 14).valueOf(),
endOfDay(Date.now()).valueOf()
);
});
@ -105,10 +110,10 @@ describe('dashboardRoutes', () => {
});
describe('GET /dashboard/users/active', () => {
const mockToday = '2022-05-30';
const mockToday = new Date(2022, 4, 30);
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date(mockToday));
jest.useFakeTimers().setSystemTime(mockToday);
});
it('should fail when the parameter `date` does not match the date regex', async () => {
@ -117,44 +122,44 @@ describe('dashboardRoutes', () => {
});
it('should call getDailyActiveUserCountsByTimeInterval with the time interval (2022-05-31, 2022-06-30] when the parameter `date` is 2022-06-30', async () => {
const targetDate = '2022-06-30';
await logRequest.get(`/dashboard/users/active?date=${targetDate}`);
const targetDate = new Date(2022, 5, 30);
await logRequest.get(`/dashboard/users/active?date=${formatToQueryDate(targetDate)}`);
expect(getDailyActiveUserCountsByTimeInterval).toHaveBeenCalledWith(
dayjs('2022-05-31').endOf('day').valueOf(),
dayjs(targetDate).endOf('day').valueOf()
endOfDay(new Date(2022, 4, 31)).valueOf(),
endOfDay(targetDate).valueOf()
);
});
it('should call getDailyActiveUserCountsByTimeInterval with the time interval (30 days ago, tomorrow] when there is no parameter `date`', async () => {
await logRequest.get('/dashboard/users/active');
expect(getDailyActiveUserCountsByTimeInterval).toHaveBeenCalledWith(
dayjs('2022-04-30').endOf('day').valueOf(),
dayjs(mockToday).endOf('day').valueOf()
endOfDay(new Date(2022, 3, 30)).valueOf(),
endOfDay(mockToday).valueOf()
);
});
it('should call countActiveUsersByTimeInterval with correct parameters when the parameter `date` is 2022-06-30', async () => {
const targetDate = '2022-06-30';
await logRequest.get(`/dashboard/users/active?date=${targetDate}`);
const targetDate = new Date(2022, 5, 30);
await logRequest.get(`/dashboard/users/active?date=${formatToQueryDate(targetDate)}`);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
1,
dayjs('2022-06-16').endOf('day').valueOf(),
dayjs('2022-06-23').endOf('day').valueOf()
endOfDay(new Date(2022, 5, 16)).valueOf(),
endOfDay(new Date(2022, 5, 23)).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
2,
dayjs('2022-06-23').endOf('day').valueOf(),
dayjs(targetDate).endOf('day').valueOf()
endOfDay(new Date(2022, 5, 23)).valueOf(),
endOfDay(targetDate).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
3,
dayjs('2022-05-01').endOf('day').valueOf(),
dayjs('2022-05-31').endOf('day').valueOf()
endOfDay(new Date(2022, 4, 1)).valueOf(),
endOfDay(new Date(2022, 4, 31)).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
4,
dayjs('2022-05-31').endOf('day').valueOf(),
dayjs(targetDate).endOf('day').valueOf()
endOfDay(new Date(2022, 4, 31)).valueOf(),
endOfDay(targetDate).valueOf()
);
});
@ -162,23 +167,23 @@ describe('dashboardRoutes', () => {
await logRequest.get('/dashboard/users/active');
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
1,
dayjs('2022-05-16').endOf('day').valueOf(),
dayjs('2022-05-23').endOf('day').valueOf()
endOfDay(new Date(2022, 4, 16)).valueOf(),
endOfDay(new Date(2022, 4, 23)).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
2,
dayjs('2022-05-23').endOf('day').valueOf(),
dayjs(mockToday).endOf('day').valueOf()
endOfDay(new Date(2022, 4, 23)).valueOf(),
endOfDay(mockToday).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
3,
dayjs('2022-03-31').endOf('day').valueOf(),
dayjs('2022-04-30').endOf('day').valueOf()
endOfDay(new Date(2022, 2, 31)).valueOf(),
endOfDay(new Date(2022, 3, 30)).valueOf()
);
expect(countActiveUsersByTimeInterval).toHaveBeenNthCalledWith(
4,
dayjs('2022-04-30').endOf('day').valueOf(),
dayjs(mockToday).endOf('day').valueOf()
endOfDay(new Date(2022, 3, 30)).valueOf(),
endOfDay(mockToday).valueOf()
);
});

View file

@ -1,6 +1,5 @@
import { dateRegex } from '@logto/core-kit';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { endOfDay, format, parse, startOfDay, subDays } from 'date-fns';
import { object, string } from 'zod';
import koaGuard from '@/middleware/koa-guard';
@ -12,11 +11,11 @@ import { countUsers, getDailyNewUserCountsByTimeInterval } from '@/queries/user'
import type { AuthedRouter } from './types';
const getDateString = (day: Dayjs) => day.format('YYYY-MM-DD');
const getDateString = (date: Date | number) => format(date, 'yyyy-MM-dd');
const indices = (length: number) => [...Array.from({ length }).keys()];
const lastTimestampOfDay = (day: Dayjs) => day.endOf('day').valueOf();
const getEndOfDayTimestamp = (date: Date | number) => endOfDay(date).valueOf();
export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
router.get('/dashboard/users/total', async (ctx, next) => {
@ -27,11 +26,11 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
});
router.get('/dashboard/users/new', async (ctx, next) => {
const today = dayjs();
const today = Date.now();
const dailyNewUserCounts = await getDailyNewUserCountsByTimeInterval(
// (14 days ago 23:59:59.999, today 23:59:59.999]
lastTimestampOfDay(today.subtract(14, 'day')),
lastTimestampOfDay(today)
getEndOfDayTimestamp(subDays(today, 14)),
getEndOfDayTimestamp(today)
);
const last14DaysNewUserCounts = new Map(
@ -39,15 +38,15 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
);
const todayNewUserCount = last14DaysNewUserCounts.get(getDateString(today)) ?? 0;
const yesterday = today.subtract(1, 'day');
const yesterday = subDays(today, 1);
const yesterdayNewUserCount = last14DaysNewUserCounts.get(getDateString(yesterday)) ?? 0;
const todayDelta = todayNewUserCount - yesterdayNewUserCount;
const last7DaysNewUserCount = indices(7)
.map((index) => getDateString(today.subtract(index, 'day')))
.map((index) => getDateString(subDays(today, index)))
.reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0);
const newUserCountFrom13DaysAgoTo7DaysAgo = indices(7)
.map((index) => getDateString(today.subtract(7 + index, 'day')))
.map((index) => getDateString(subDays(today, index + 7)))
.reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0);
const last7DaysDelta = last7DaysNewUserCount - newUserCountFrom13DaysAgoTo7DaysAgo;
@ -75,7 +74,7 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
query: { date },
} = ctx.guard;
const targetDay = date ? dayjs(date) : dayjs(); // Defaults to today
const targetDay = date ? parse(date, 'yyyy-MM-dd', startOfDay(Date.now())) : Date.now(); // Defaults to today
const [
// DAU: Daily Active User
last30DauCounts,
@ -88,39 +87,39 @@ export default function dashboardRoutes<T extends AuthedRouter>(router: T) {
] = await Promise.all([
getDailyActiveUserCountsByTimeInterval(
// (30 days ago 23:59:59.999, target day 23:59:59.999]
lastTimestampOfDay(targetDay.subtract(30, 'day')),
lastTimestampOfDay(targetDay)
getEndOfDayTimestamp(subDays(targetDay, 30)),
getEndOfDayTimestamp(targetDay)
),
countActiveUsersByTimeInterval(
// (14 days ago 23:59:59.999, 7 days ago 23:59:59.999]
lastTimestampOfDay(targetDay.subtract(14, 'day')),
lastTimestampOfDay(targetDay.subtract(7, 'day'))
getEndOfDayTimestamp(subDays(targetDay, 14)),
getEndOfDayTimestamp(subDays(targetDay, 7))
),
countActiveUsersByTimeInterval(
// (7 days ago 23:59:59.999, target day 23:59:59.999]
lastTimestampOfDay(targetDay.subtract(7, 'day')),
lastTimestampOfDay(targetDay)
getEndOfDayTimestamp(subDays(targetDay, 7)),
getEndOfDayTimestamp(targetDay)
),
countActiveUsersByTimeInterval(
// (60 days ago 23:59:59.999, 30 days ago 23:59:59.999]
lastTimestampOfDay(targetDay.subtract(60, 'day')),
lastTimestampOfDay(targetDay.subtract(30, 'day'))
getEndOfDayTimestamp(subDays(targetDay, 60)),
getEndOfDayTimestamp(subDays(targetDay, 30))
),
countActiveUsersByTimeInterval(
// (30 days ago 23:59:59.999, target day 23:59:59.999]
lastTimestampOfDay(targetDay.subtract(30, 'day')),
lastTimestampOfDay(targetDay)
getEndOfDayTimestamp(subDays(targetDay, 30)),
getEndOfDayTimestamp(targetDay)
),
]);
const previousDate = getDateString(targetDay.subtract(1, 'day'));
const previousDate = getDateString(subDays(targetDay, 1));
const targetDate = getDateString(targetDay);
const previousDAU = last30DauCounts.find(({ date }) => date === previousDate)?.count ?? 0;
const dau = last30DauCounts.find(({ date }) => date === targetDate)?.count ?? 0;
const dauCurve = indices(30).map((index) => {
const dateString = getDateString(targetDay.subtract(29 - index, 'day'));
const dateString = getDateString(subDays(targetDay, 29 - index));
const count = last30DauCounts.find(({ date }) => date === dateString)?.count ?? 0;
return { date: dateString, count };

View file

@ -1,6 +1,6 @@
import type { User } from '@logto/schemas';
import { PasscodeType } from '@logto/schemas';
import dayjs from 'dayjs';
import { addDays, subDays } from 'date-fns';
import { Provider } from 'oidc-provider';
import { mockPasswordEncrypted, mockUserWithPassword } from '@/__mocks__';
@ -15,6 +15,8 @@ const encryptUserPassword = jest.fn(async (password: string) => ({
}));
const findUserById = jest.fn(async (): Promise<User> => mockUserWithPassword);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ userId: 'id' }));
const getYesterdayDate = () => subDays(Date.now(), 1);
const getTomorrowDate = () => addDays(Date.now(), 1);
jest.mock('@/lib/user', () => ({
...jest.requireActual('@/lib/user'),
@ -84,7 +86,7 @@ describe('session -> forgotPasswordRoutes', () => {
result: {
verification: {
userId: 'id',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowDate().toISOString(),
flow: PasscodeType.ForgotPassword,
},
},
@ -105,7 +107,7 @@ describe('session -> forgotPasswordRoutes', () => {
interactionDetails.mockResolvedValueOnce({
result: {
verification: {
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowDate().toISOString(),
flow: PasscodeType.ForgotPassword,
},
},
@ -121,7 +123,7 @@ describe('session -> forgotPasswordRoutes', () => {
result: {
verification: {
userId: 'id',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowDate().toISOString(),
flow: PasscodeType.SignIn,
},
},
@ -165,7 +167,7 @@ describe('session -> forgotPasswordRoutes', () => {
result: {
verification: {
userId: 'id',
expiresAt: dayjs().subtract(1, 'day').toISOString(),
expiresAt: getYesterdayDate().toISOString(),
flow: PasscodeType.ForgotPassword,
},
},
@ -181,7 +183,7 @@ describe('session -> forgotPasswordRoutes', () => {
result: {
verification: {
userId: 'id',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowDate().toISOString(),
flow: PasscodeType.ForgotPassword,
},
},
@ -198,7 +200,7 @@ describe('session -> forgotPasswordRoutes', () => {
result: {
verification: {
userId: 'id',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowDate().toISOString(),
flow: PasscodeType.ForgotPassword,
},
},

View file

@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import type { User } from '@logto/schemas';
import { PasscodeType } from '@logto/schemas';
import dayjs from 'dayjs';
import { addDays, addSeconds, subDays } from 'date-fns';
import { Provider } from 'oidc-provider';
import { mockUser } from '@/__mocks__';
@ -15,6 +15,7 @@ import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const getTomorrowIsoString = () => addDays(Date.now(), 1).toISOString();
jest.mock('@/lib/user', () => ({
generateUserId: () => 'user1',
@ -200,7 +201,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
flow: PasscodeType.SignIn,
phone: '13000000000',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
},
})
);
@ -224,7 +225,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
flow: PasscodeType.Register,
phone: '13000000000',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
},
})
);
@ -248,7 +249,7 @@ describe('session -> passwordlessRoutes', () => {
expect.objectContaining({
verification: {
userId: 'id',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
flow: PasscodeType.ForgotPassword,
},
})
@ -299,7 +300,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
flow: PasscodeType.SignIn,
email: 'a@a.com',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
},
})
);
@ -322,7 +323,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
flow: PasscodeType.Register,
email: 'a@a.com',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
},
})
);
@ -346,7 +347,7 @@ describe('session -> passwordlessRoutes', () => {
expect.objectContaining({
verification: {
userId: 'id',
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(fakeTime, verificationTimeout).toISOString(),
flow: PasscodeType.ForgotPassword,
},
})
@ -382,7 +383,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000000',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -406,7 +407,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -427,7 +428,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
phone: '13000000000',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -441,7 +442,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000000',
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -469,7 +470,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000000',
flow: PasscodeType.SignIn,
expiresAt: dayjs().subtract(1, 'day').toISOString(),
expiresAt: subDays(Date.now(), 1).toISOString(),
},
},
});
@ -483,7 +484,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'XX@foo',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -497,7 +498,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -517,7 +518,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'a@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -541,7 +542,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -564,7 +565,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
email: 'a@a.com',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -578,7 +579,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'a@a.com',
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -591,7 +592,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -605,7 +606,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -625,7 +626,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000001',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -647,7 +648,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000001',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -668,7 +669,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
phone: '13000000001',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -682,7 +683,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000001',
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -695,7 +696,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -709,7 +710,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
phone: '13000000000',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -729,7 +730,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'b@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -751,7 +752,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'b@a.com',
flow: PasscodeType.SignIn,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -772,7 +773,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
email: 'b@a.com',
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -786,7 +787,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'b@a.com',
flow: PasscodeType.ForgotPassword,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -799,7 +800,7 @@ describe('session -> passwordlessRoutes', () => {
result: {
verification: {
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});
@ -813,7 +814,7 @@ describe('session -> passwordlessRoutes', () => {
verification: {
email: 'a@a.com',
flow: PasscodeType.Register,
expiresAt: dayjs().add(1, 'day').toISOString(),
expiresAt: getTomorrowIsoString(),
},
},
});

View file

@ -1,7 +1,7 @@
import type { LogType, PasscodeType } from '@logto/schemas';
import { logTypeGuard } from '@logto/schemas';
import type { Truthy } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { addSeconds, isAfter, isValid, parseISO } from 'date-fns';
import type { Context } from 'koa';
import type { Provider } from 'oidc-provider';
import type { ZodType } from 'zod';
@ -60,8 +60,9 @@ export const getVerificationStorageFromInteraction = async <T = VerificationStor
};
export const checkValidateExpiration = (expiresAt: string) => {
const parsed = parseISO(expiresAt);
assertThat(
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
isValid(parsed) && isAfter(parsed, Date.now()),
new RequestError({ code: 'session.verification_expired', status: 401 })
);
};
@ -75,7 +76,7 @@ export const assignVerificationResult = async (
) => {
const verification: VerificationStorage = {
...verificationData,
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
expiresAt: addSeconds(Date.now(), verificationTimeout).toISOString(),
};
await provider.interactionResult(ctx.req, ctx.res, {

View file

@ -15,6 +15,7 @@
},
"devDependencies": {
"@jest/types": "^29.1.2",
"@logto/js": "1.0.0-beta.11",
"@logto/node": "1.0.0-beta.12",
"@logto/schemas": "workspace:^",
"@peculiar/webcrypto": "^1.3.3",

View file

@ -10,7 +10,7 @@ import { extractCookie } from '@/utils';
import { MemoryStorage } from './storage';
const defaultConfig = {
export const defaultConfig = {
endpoint: logtoUrl,
appId: demoAppApplicationId,
persistAccessToken: false,
@ -18,8 +18,8 @@ const defaultConfig = {
export default class MockClient {
public interactionCookie?: string;
private navigateUrl?: string;
private navigateUrl?: string;
private readonly storage: MemoryStorage;
private readonly logto: LogtoClient;
@ -88,6 +88,10 @@ export default class MockClient {
return this.logto.getAccessToken(resource);
}
public async getRefreshToken() {
return this.logto.getRefreshToken();
}
public async signOut(postSignOutRedirectUri?: string) {
return this.logto.signOut(postSignOutRedirectUri);
}

View file

@ -0,0 +1,4 @@
declare module 'node-fetch' {
const nodeFetch: typeof fetch;
export = nodeFetch;
}

View file

@ -1,8 +1,13 @@
import path from 'path';
import { fetchTokenByRefreshToken } from '@logto/js';
import { managementResource } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
import fetch from 'node-fetch';
import { signInWithUsernameAndPassword } from '@/api';
import MockClient from '@/client';
import MockClient, { defaultConfig } from '@/client';
import { logtoUrl } from '@/constants';
import { createUserByAdmin } from '@/helpers';
import { generateUsername, generatePassword } from '@/utils';
@ -36,4 +41,41 @@ describe('get access token', () => {
// Request for invalid resource should throw
void expect(client.getAccessToken('api.foo.com')).rejects.toThrow();
});
it('sign-in and get multiple Access Token by the same Refresh Token within refreshTokenReuseInterval', async () => {
const client = new MockClient({ resources: [managementResource.indicator] });
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await signInWithUsernameAndPassword(
username,
password,
client.interactionCookie
);
await client.processSession(redirectTo);
assert(client.isAuthenticated, new Error('Sign in get get access token failed'));
const refreshToken = await client.getRefreshToken();
assert(refreshToken, new Error('No Refresh Token found'));
const getAccessTokenByRefreshToken = async () =>
fetchTokenByRefreshToken(
{
clientId: defaultConfig.appId,
tokenEndpoint: path.join(logtoUrl, '/oidc/token'),
refreshToken,
resource: managementResource.indicator,
},
async <T>(...args: Parameters<typeof fetch>): Promise<T> => {
const response = await fetch(...args);
assert(response.ok, new Error('Request error'));
return response.json();
}
);
// Allow to use the same refresh token to fetch access token within short time period
await Promise.all([getAccessTokenByRefreshToken(), getAccessTokenByRefreshToken()]);
});
});

View file

@ -45,7 +45,6 @@
"dependencies": {
"@logto/schemas": "workspace:^",
"@silverhand/essentials": "^1.3.0",
"dayjs": "^1.10.5",
"find-up": "^5.0.0",
"nanoid": "^3.3.4",
"slonik": "^30.0.0"

View file

@ -1,4 +1,3 @@
import dayjs from 'dayjs';
import { sql } from 'slonik';
import { SqlToken } from 'slonik/dist/src/tokens.js';
@ -124,7 +123,7 @@ describe('convertToTimestamp()', () => {
});
it('converts to sql per time parameter', () => {
const time = dayjs(123_123_123);
const time = new Date(123_123_123);
expect(convertToTimestamp(time)).toEqual({
sql: 'to_timestamp($1)',

View file

@ -1,7 +1,6 @@
import type { SchemaValuePrimitive, SchemaValue } from '@logto/schemas';
import type { Falsy } from '@silverhand/essentials';
import { notFalsy } from '@silverhand/essentials';
import dayjs from 'dayjs';
import type { SqlSqlToken, SqlToken, QueryResult, IdentifierSqlToken } from 'slonik';
import { sql } from 'slonik';
@ -76,7 +75,8 @@ export const convertToIdentifiers = <T extends Table>({ table, fields }: T, with
};
};
export const convertToTimestamp = (time = dayjs()) => sql`to_timestamp(${time.valueOf() / 1000})`;
export const convertToTimestamp = (time = new Date()) =>
sql`to_timestamp(${time.valueOf() / 1000})`;
export const manyRows = async <T>(query: Promise<QueryResult<T>>): Promise<readonly T[]> => {
const { rows } = await query;

23
pnpm-lock.yaml generated
View file

@ -134,7 +134,7 @@ importers:
clean-deep: ^3.4.0
cross-env: ^7.0.3
csstype: ^3.0.11
dayjs: ^1.10.5
date-fns: ^2.29.3
deepmerge: ^4.2.2
dnd-core: ^16.0.0
eslint: ^8.21.0
@ -170,6 +170,8 @@ importers:
swr: ^1.3.0
typescript: ^4.7.4
zod: ^3.19.1
dependencies:
date-fns: 2.29.3
devDependencies:
'@fontsource/roboto-mono': 4.5.7
'@logto/core-kit': 1.0.0-beta.20
@ -201,7 +203,6 @@ importers:
clean-deep: 3.4.0
cross-env: 7.0.3
csstype: 3.0.11
dayjs: 1.10.7
deepmerge: 4.2.2
dnd-core: 16.0.0
eslint: 8.21.0
@ -271,7 +272,7 @@ importers:
chalk: ^4
clean-deep: ^3.4.0
copyfiles: ^2.4.1
dayjs: ^1.10.5
date-fns: ^2.29.3
debug: ^4.3.4
decamelize: ^5.0.0
deepmerge: ^4.2.2
@ -329,7 +330,7 @@ importers:
'@silverhand/essentials': 1.3.0
chalk: 4.1.2
clean-deep: 3.4.0
dayjs: 1.10.7
date-fns: 2.29.3
debug: 4.3.4
decamelize: 5.0.1
deepmerge: 4.2.2
@ -462,6 +463,7 @@ importers:
packages/integration-tests:
specifiers:
'@jest/types': ^29.1.2
'@logto/js': 1.0.0-beta.11
'@logto/node': 1.0.0-beta.12
'@logto/schemas': workspace:^
'@peculiar/webcrypto': ^1.3.3
@ -487,6 +489,7 @@ importers:
typescript: ^4.7.4
devDependencies:
'@jest/types': 29.1.2
'@logto/js': 1.0.0-beta.11
'@logto/node': 1.0.0-beta.12
'@logto/schemas': link:../schemas
'@peculiar/webcrypto': 1.3.3
@ -623,7 +626,6 @@ importers:
'@silverhand/ts-config': 1.2.1
'@types/jest': ^29.1.2
'@types/node': ^16.0.0
dayjs: ^1.10.5
eslint: ^8.21.0
find-up: ^5.0.0
jest: ^29.1.2
@ -635,7 +637,6 @@ importers:
dependencies:
'@logto/schemas': link:../schemas
'@silverhand/essentials': 1.3.0
dayjs: 1.10.7
find-up: 5.0.0
nanoid: 3.3.4
slonik: 30.1.2
@ -3467,7 +3468,7 @@ packages:
'@jest/types': 29.1.2
deepmerge: 4.2.2
identity-obj-proxy: 3.0.0
jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq
jest: 29.1.2_@types+node@16.11.12
jest-matcher-specific-error: 1.0.0
jest-transform-stub: 2.0.0
ts-jest: 29.0.3_37jxomqt5oevoqzq6g3r6n3ili
@ -5648,8 +5649,10 @@ packages:
whatwg-url: 11.0.0
dev: true
/dayjs/1.10.7:
resolution: {integrity: sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==}
/date-fns/2.29.3:
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
engines: {node: '>=0.11'}
dev: false
/debug/2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
@ -13451,7 +13454,7 @@ packages:
'@jest/types': 29.1.2
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 29.1.2_k5ytkvaprncdyzidqqws5bqksq
jest: 29.1.2_@types+node@16.11.12
jest-util: 29.2.1
json5: 2.2.1
lodash.memoize: 4.1.2