Files
planner.sori.studio/backend/src/routes/admin.js

104 lines
3.1 KiB
JavaScript

import { sql } from 'drizzle-orm'
import { db } from '../db/client.js'
import { findAuthenticatedUser } from '../lib/authSession.js'
import { users } from '../db/schema.js'
async function requireAdminUser(request, reply) {
const user = await findAuthenticatedUser(request)
if (!user) {
reply.code(401).send({
message: '인증이 필요합니다.',
})
return null
}
if (user.role !== 'admin') {
reply.code(403).send({
message: '관리자만 접근할 수 있습니다.',
})
return null
}
return user
}
export async function registerAdminRoutes(app) {
app.get('/api/admin/overview', async (request, reply) => {
const adminUser = await requireAdminUser(request, reply)
if (!adminUser) {
return
}
const [summary] = await db
.select({
totalUsers: sql<number>`count(*)::int`,
totalAdmins: sql<number>`count(*) filter (where ${users.role} = 'admin')::int`,
verifiedUsers: sql<number>`count(*) filter (where ${users.emailVerifiedAt} is not null)::int`,
activeUsers30d: sql<number>`count(*) filter (where ${users.lastLoginAt} >= now() - interval '30 days')::int`,
newUsers7d: sql<number>`count(*) filter (where ${users.createdAt} >= now() - interval '7 days')::int`,
})
.from(users)
const plannerCountResult = await db.execute(
sql`select count(*)::int as count from planner_entries`,
)
const goalCountResult = await db.execute(
sql`select count(*)::int as count from goals`,
)
const userRowsResult = await db.execute(sql`
select
u.id,
u.nickname,
u.email,
u.role,
u.created_at as "createdAt",
u.email_verified_at as "emailVerifiedAt",
u.last_login_at as "lastLoginAt",
count(distinct pe.id)::int as "plannerEntryCount",
count(distinct g.id)::int as "goalCount",
max(pe.entry_date) as "lastEntryDate",
max(pe.updated_at) as "lastEntryUpdatedAt"
from users u
left join planner_entries pe on pe.user_id = u.id
left join goals g on g.user_id = u.id
group by u.id
order by coalesce(u.last_login_at, u.created_at) desc, u.id desc
`)
const recentLoginsResult = await db.execute(sql`
select
id,
nickname,
email,
role,
last_login_at as "lastLoginAt"
from users
where last_login_at is not null
order by last_login_at desc
limit 5
`)
return {
summary: {
totalUsers: summary?.totalUsers ?? 0,
totalAdmins: summary?.totalAdmins ?? 0,
verifiedUsers: summary?.verifiedUsers ?? 0,
activeUsers30d: summary?.activeUsers30d ?? 0,
newUsers7d: summary?.newUsers7d ?? 0,
totalPlannerEntries: plannerCountResult.rows[0]?.count ?? 0,
totalGoals: goalCountResult.rows[0]?.count ?? 0,
},
users: userRowsResult.rows.map((row) => ({
...row,
isActiveRecently: row.lastLoginAt
? new Date(row.lastLoginAt).getTime() >= Date.now() - 30 * 24 * 60 * 60 * 1000
: false,
})),
recentLogins: recentLoginsResult.rows,
}
})
}