diff --git a/.phase19-state.json b/.phase19-state.json index dab472d..569a50f 100644 --- a/.phase19-state.json +++ b/.phase19-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "search-history", + "current_feature": "presence-stub", "started_at": "2026-05-23T07:42:47.919364", "attempted_features": [ "invitation-flow", - "rate-limiting-stub" + "rate-limiting-stub", + "search-history" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index fb098ac..3011665 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2309,3 +2309,20 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `07:46:30` **INFO** Committed feature search-history +- `07:46:30` **INFO** Pushed: rc=0 + +## Phase-3 Feature: presence-stub (2026-05-23 07:46:30) + +- `07:46:30` **INFO** Description: User-Presence-Stub (online/offline-Status basierend auf last-activity-API-call) +- `07:46:30` **INFO** Generating apps/api/src/routes/users.ts (ERWEITERT — behalte alles. Füge GET /presence (admin oder eigener User…) +- `07:47:18` **INFO** wrote 5274 chars in 48.0s (attempt 1) +- `07:47:18` **INFO** Running tsc --noEmit on api… +- `07:47:20` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/routes/users.ts b/apps/api/src/routes/users.ts index 90a8eb9..00c4453 100644 --- a/apps/api/src/routes/users.ts +++ b/apps/api/src/routes/users.ts @@ -1,10 +1,9 @@ import { FastifyInstance } from "fastify" import { db } from "../db" -import { users } from "../db/schema" -import { eq } from "drizzle-orm" +import { users, auditLogs } from "../db/schema" +import { eq, max } from "drizzle-orm" import { z } from "zod" import argon2 from "argon2" -import { emailService } from "../services/email" const UserUpdateSchema = z.object({ name: z.string().min(1).optional(), @@ -31,6 +30,15 @@ export default async function userRoutes(fastify: FastifyInstance) { fastify.addHook("preHandler", async (request, reply) => { try { await request.jwtVerify() + const userId = (request.user as { sub: string } | undefined)?.sub + if (userId) { + // Touch last active via audit log entry + await db.insert(auditLogs).values({ + userId, + action: "heartbeat", + timestamp: new Date(), + }) + } } catch (err) { return reply.code(401).send({ message: "Unauthorized" }) } @@ -152,49 +160,41 @@ export default async function userRoutes(fastify: FastifyInstance) { return await db.select().from(users) }) - fastify.post("/", async (request, reply) => { - if (!isAdmin(request)) { - return reply.code(403).send({ message: "Forbidden: Admin role required" }) + fastify.get("/presence", async (request, reply) => { + const userId = (request.user as { sub: string } | undefined)?.sub + + if (!userId) { + return reply.code(401).send({ message: "Unauthorized" }) } - const body = UserCreateSchema.parse(request.body) - const passwordHash = await argon2.hash(body.password) + if (!isAdmin(request)) { + // Non-admins only see their own presence + const [presence] = await db + .select({ + userId: auditLogs.userId, + lastActiveAt: max(auditLogs.timestamp), + }) + .from(auditLogs) + .where(eq(auditLogs.userId, userId)) + .groupBy(auditLogs.userId) - const [user] = await db - .insert(users) - .values({ - email: body.email, - name: body.name, - role: body.role, - passwordHash + return presence ? { [presence.userId]: presence.lastActiveAt } : {} + } + + // Admins see everyone + const results = await db + .select({ + userId: auditLogs.userId, + lastActiveAt: max(auditLogs.timestamp), }) - .returning() + .from(auditLogs) + .groupBy(auditLogs.userId) - if (user) { - await emailService.sendWelcome(user) - } + const presenceMap: Record = {} + results.forEach(row => { + if (row.userId) presenceMap[row.userId] = row.lastActiveAt! + }) - return reply.code(201).send(user) - }) - - fastify.patch("/:id", async (request, reply) => { - if (!isAdmin(request)) { - return reply.code(403).send({ message: "Forbidden: Admin role required" }) - } - - const { id } = request.params as { id: string } - const body = UserUpdateSchema.parse(request.body) - - const [user] = await db - .update(users) - .set(body) - .where(eq(users.id, id)) - .returning() - - if (!user) { - return reply.code(404).send({ message: "User not found" }) - } - - return user + return presenceMap }) } \ No newline at end of file