From f8f8f0962429628ad703a447288cdd46cb83e05c Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:19:57 +0330 Subject: [PATCH] Import source: app/backend/src/routes/analytics Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/backend/src/routes/analytics.ts | 90 +++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 app/backend/src/routes/analytics.ts diff --git a/app/backend/src/routes/analytics.ts b/app/backend/src/routes/analytics.ts new file mode 100644 index 0000000..d854757 --- /dev/null +++ b/app/backend/src/routes/analytics.ts @@ -0,0 +1,90 @@ +import type { FastifyInstance, FastifyRequest } from 'fastify'; +import { prisma } from '../db.js'; +import { requireAuth } from '../plugins/session.js'; + +function articleWhere(userId: string) { + return { + OR: [ + { createdById: userId }, + { author: { userId } }, + ], + }; +} + +function since30() { + const date = new Date(); + date.setDate(date.getDate() - 29); + date.setHours(0, 0, 0, 0); + return date; +} + +export default async function analyticsRoutes(app: FastifyInstance) { + app.get('/api/v1/auth/analytics/overview', { preHandler: requireAuth(['admin', 'editor', 'writer']) }, async (req) => { + const where = articleWhere(req.session!.userId); + const [articles, sums, comments, saved, viewsByDay] = await Promise.all([ + prisma.article.count({ where }), + prisma.article.aggregate({ where, _sum: { views: true, reactions: true } }), + prisma.comment.count({ where: { article: where } }), + prisma.savedArticle.count({ where: { article: where } }), + prisma.articleViewLog.groupBy({ + by: ['date'], + where: { date: { gte: since30() }, article: where }, + _sum: { views: true }, + orderBy: { date: 'asc' }, + }), + ]); + return { + articles, + views: sums._sum.views ?? 0, + reactions: sums._sum.reactions ?? 0, + comments, + saved, + viewsByDay: viewsByDay.map((row) => ({ date: row.date.toISOString().slice(0, 10), views: row._sum.views ?? 0 })), + }; + }); + + app.get('/api/v1/auth/analytics/articles', { preHandler: requireAuth(['admin', 'editor', 'writer']) }, async (req) => { + const rows = await prisma.article.findMany({ + where: articleWhere(req.session!.userId), + select: { id: true, slug: true, title: true, status: true, views: true, reactions: true, updatedAt: true, _count: { select: { comments: true, savedBy: true } } }, + orderBy: [{ views: 'desc' }, { updatedAt: 'desc' }], + take: 100, + }); + return { items: rows.map((row) => ({ + id: row.id, + slug: row.slug, + title: row.title, + status: row.status, + views: row.views, + reactions: row.reactions, + comments: row._count.comments, + saved: row._count.savedBy, + updatedAt: row.updatedAt.toISOString(), + })) }; + }); + + app.get<{ Params: { slug: string } }>('/api/v1/auth/analytics/article/:slug', { preHandler: requireAuth(['admin', 'editor', 'writer']) }, async (req, reply) => { + const article = await prisma.article.findFirst({ + where: { slug: req.params.slug, ...articleWhere(req.session!.userId) }, + select: { id: true, slug: true, title: true, status: true, views: true, reactions: true, _count: { select: { comments: true, savedBy: true } } }, + }); + if (!article) return reply.code(404).send({ error: 'not_found' }); + const viewsByDay = await prisma.articleViewLog.findMany({ + where: { articleId: article.id, date: { gte: since30() } }, + orderBy: { date: 'asc' }, + }); + return { + article: { + id: article.id, + slug: article.slug, + title: article.title, + status: article.status, + views: article.views, + reactions: article.reactions, + comments: article._count.comments, + saved: article._count.savedBy, + }, + viewsByDay: viewsByDay.map((row) => ({ date: row.date.toISOString().slice(0, 10), views: row.views })), + }; + }); +}