diff --git a/packages/core/src/routes/dashboard.ts b/packages/core/src/routes/dashboard.ts index 4ca59fe0a..b85ea21b9 100644 --- a/packages/core/src/routes/dashboard.ts +++ b/packages/core/src/routes/dashboard.ts @@ -1,6 +1,7 @@ import { dateRegex } from '@logto/core-kit'; +import { getNewUsersResponseGuard, getActiveUsersResponseGuard } from '@logto/schemas'; import { endOfDay, format, subDays } from 'date-fns'; -import { object, string } from 'zod'; +import { number, object, string } from 'zod'; import koaGuard from '#src/middleware/koa-guard.js'; @@ -20,56 +21,74 @@ export default function dashboardRoutes( users: { countUsers, getDailyNewUserCountsByTimeInterval }, } = queries; - router.get('/dashboard/users/total', async (ctx, next) => { - const { count: totalUserCount } = await countUsers(); - ctx.body = { totalUserCount }; + router.get( + '/dashboard/users/total', + koaGuard({ + response: object({ + totalUserCount: number(), + }), + status: [200, 401, 403], + }), + async (ctx, next) => { + const { count: totalUserCount } = await countUsers(); + ctx.body = { totalUserCount }; - return next(); - }); + return next(); + } + ); - router.get('/dashboard/users/new', async (ctx, next) => { - const today = Date.now(); - const dailyNewUserCounts = await getDailyNewUserCountsByTimeInterval( - // (14 days ago 23:59:59.999, today 23:59:59.999] - getEndOfDayTimestamp(subDays(today, 14)), - getEndOfDayTimestamp(today) - ); + router.get( + '/dashboard/users/new', + koaGuard({ + response: getNewUsersResponseGuard, + status: [200, 401, 403], + }), + async (ctx, next) => { + const today = Date.now(); + const dailyNewUserCounts = await getDailyNewUserCountsByTimeInterval( + // (14 days ago 23:59:59.999, today 23:59:59.999] + getEndOfDayTimestamp(subDays(today, 14)), + getEndOfDayTimestamp(today) + ); - const last14DaysNewUserCounts = new Map( - dailyNewUserCounts.map(({ date, count }) => [date, count]) - ); + const last14DaysNewUserCounts = new Map( + dailyNewUserCounts.map(({ date, count }) => [date, count]) + ); - const todayNewUserCount = last14DaysNewUserCounts.get(getDateString(today)) ?? 0; - const yesterday = subDays(today, 1); - const yesterdayNewUserCount = last14DaysNewUserCounts.get(getDateString(yesterday)) ?? 0; - const todayDelta = todayNewUserCount - yesterdayNewUserCount; + const todayNewUserCount = last14DaysNewUserCounts.get(getDateString(today)) ?? 0; + const yesterday = subDays(today, 1); + const yesterdayNewUserCount = last14DaysNewUserCounts.get(getDateString(yesterday)) ?? 0; + const todayDelta = todayNewUserCount - yesterdayNewUserCount; - const last7DaysNewUserCount = indices(7) - .map((index) => getDateString(subDays(today, index))) - .reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0); - const newUserCountFrom13DaysAgoTo7DaysAgo = indices(7) - .map((index) => getDateString(subDays(today, index + 7))) - .reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0); - const last7DaysDelta = last7DaysNewUserCount - newUserCountFrom13DaysAgoTo7DaysAgo; + const last7DaysNewUserCount = indices(7) + .map((index) => getDateString(subDays(today, index))) + .reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0); + const newUserCountFrom13DaysAgoTo7DaysAgo = indices(7) + .map((index) => getDateString(subDays(today, index + 7))) + .reduce((sum, date) => sum + (last14DaysNewUserCounts.get(date) ?? 0), 0); + const last7DaysDelta = last7DaysNewUserCount - newUserCountFrom13DaysAgoTo7DaysAgo; - ctx.body = { - today: { - count: todayNewUserCount, - delta: todayDelta, - }, - last7Days: { - count: last7DaysNewUserCount, - delta: last7DaysDelta, - }, - }; + ctx.body = { + today: { + count: todayNewUserCount, + delta: todayDelta, + }, + last7Days: { + count: last7DaysNewUserCount, + delta: last7DaysDelta, + }, + }; - return next(); - }); + return next(); + } + ); router.get( '/dashboard/users/active', koaGuard({ query: object({ date: string().regex(dateRegex).optional() }), + response: getActiveUsersResponseGuard, + status: [200, 401, 403], }), async (ctx, next) => { const { diff --git a/packages/integration-tests/src/tests/api/dashboard.test.ts b/packages/integration-tests/src/tests/api/dashboard.test.ts index 5a0f9d1ba..0dc660317 100644 --- a/packages/integration-tests/src/tests/api/dashboard.test.ts +++ b/packages/integration-tests/src/tests/api/dashboard.test.ts @@ -1,7 +1,8 @@ import { SignInIdentifier } from '@logto/schemas'; import type { StatisticsData } from '#src/api/index.js'; -import { getTotalUsersCount, getNewUsersData, getActiveUsersData } from '#src/api/index.js'; +import { api, getTotalUsersCount, getNewUsersData, getActiveUsersData } from '#src/api/index.js'; +import { createResponseWithCode } from '#src/helpers/admin-tenant.js'; import { createUserByAdmin } from '#src/helpers/index.js'; import { registerNewUser, signInWithPassword } from '#src/helpers/interactions.js'; import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; @@ -16,6 +17,16 @@ describe('admin console dashboard', () => { }); }); + it('non authorized request should return 401', async () => { + await expect(api.get('dashboard/users/total')).rejects.toMatchObject( + createResponseWithCode(401) + ); + await expect(api.get('dashboard/users/new')).rejects.toMatchObject(createResponseWithCode(401)); + await expect(api.get('dashboard/users/active')).rejects.toMatchObject( + createResponseWithCode(401) + ); + }); + it('should get total user count successfully', async () => { const { totalUserCount: originTotalUserCount } = await getTotalUsersCount(); diff --git a/packages/schemas/src/types/dashboard.ts b/packages/schemas/src/types/dashboard.ts new file mode 100644 index 000000000..f6e649f83 --- /dev/null +++ b/packages/schemas/src/types/dashboard.ts @@ -0,0 +1,23 @@ +import { number, object, array, string } from 'zod'; + +const dashboardUsersDataGuard = object({ + count: number(), + delta: number(), +}); + +export const getNewUsersResponseGuard = object({ + today: dashboardUsersDataGuard, + last7Days: dashboardUsersDataGuard, +}); + +export const getActiveUsersResponseGuard = object({ + dauCurve: array( + object({ + date: string(), + count: number(), + }) + ), + dau: dashboardUsersDataGuard, + wau: dashboardUsersDataGuard, + mau: dashboardUsersDataGuard, +}); diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index b44fd4305..a72b35c3b 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -17,3 +17,4 @@ export * from './hook.js'; export * from './service-log.js'; export * from './theme.js'; export * from './cookie.js'; +export * from './dashboard.js';