diff --git a/packages/backend/package.json b/packages/backend/package.json index b1c3403..4da8d67 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -48,5 +48,5 @@ "start": "ts-node ./src/index.ts", "test": "npm test" }, - "version": "1.0.1" + "version": "1.0.2" } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 3df2921..c973061 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -119,13 +119,19 @@ const start = async () => { app.use(router.routes()); app.use(router.allowedMethods()); + console.log("Registered routes:"); + router.stack.forEach((layer) => { + const names = layer.stack.map(fn => fn.name || ""); + console.log(`${layer.methods.join(",")} ${layer.path} → [${names.join(", ")}]`); + }); + const httpServer = app.listen(3123, "0.0.0.0"); const distributor = new WebsocketDistributor(systemName, { server: httpServer, authenticationMiddleware, headers: { - Authorization: "apikey="+Container.get("api-keys")[0] + Authorization: "apikey=" + Container.get("api-keys")[0] }, }); const system = await DistributedActorSystem.create({ diff --git a/packages/backend/src/middlewares/authRoutes.ts b/packages/backend/src/middlewares/authRoutes.ts index 261c0c4..3476afb 100644 --- a/packages/backend/src/middlewares/authRoutes.ts +++ b/packages/backend/src/middlewares/authRoutes.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import jwt from "jsonwebtoken"; import { Issuer, Client, ClientMetadata } from "openid-client"; import Container from "typedi"; @@ -63,7 +62,7 @@ export const authTempAccount = async (ctx: koa.Context): Promise => { const userStore = createActorUri("UserStore"); const fpStore = createActorUri("FingerprintStore"); const uid = v4() as Id; - + try { let fpData: Fingerprint | Error = await system.ask(fpStore, FingerprintStoreMessages.Get(fingerprint as Id)); console.log("New fingerprint", fingerprint, fpData); @@ -81,8 +80,8 @@ export const authTempAccount = async (ctx: koa.Context): Promise => { }; await system.send(fpStore, FingerprintStoreMessages.StoreFingerprint(fp)); fpData = fp; - } - await system.send(fpStore, FingerprintStoreMessages.IncreaseCount({fingerprint: fingerprint as Id, userUid: uid as Id, initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined})); + } + await system.send(fpStore, FingerprintStoreMessages.IncreaseCount({ fingerprint: fingerprint as Id, userUid: uid as Id, initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined })); if (fpData.blocked) { console.debug("Fingerprint was blocked", fingerprint); ctx.redirect((process.env.FRONTEND_URI ?? "http://localhost:5173") + "?error=userdeactivated"); @@ -91,18 +90,18 @@ export const authTempAccount = async (ctx: koa.Context): Promise => { } catch (e) { console.error(e); console.debug("A new fingerprint has been found", fingerprint); - const fp: Fingerprint = { - uid: fingerprint as Id, - created: toTimestamp(), - updated: toTimestamp(), - lastSeen: toTimestamp(), - usageCount: 1, - blocked: false, - userUid: uid, - initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined, - }; - await system.send(fpStore, FingerprintStoreMessages.StoreFingerprint(fp)); - await system.send(fpStore, FingerprintStoreMessages.IncreaseCount({fingerprint: fingerprint as Id, userUid: uid as Id, initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined})); + const fp: Fingerprint = { + uid: fingerprint as Id, + created: toTimestamp(), + updated: toTimestamp(), + lastSeen: toTimestamp(), + usageCount: 1, + blocked: false, + userUid: uid, + initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined, + }; + await system.send(fpStore, FingerprintStoreMessages.StoreFingerprint(fp)); + await system.send(fpStore, FingerprintStoreMessages.IncreaseCount({ fingerprint: fingerprint as Id, userUid: uid as Id, initialQuiz: quiz && quiz !== "false" ? quiz as Id : undefined })); } try { const existingUser: User | Error = await system.ask(userStore, UserStoreMessages.GetByFingerprint(fingerprint)); @@ -299,7 +298,7 @@ export const authLogout = async (ctx: koa.Context): Promise => { }, () => ctx.throw(401, "Unable to sign out.") ); - + ctx.set("Set-Cookie", `bearer=; path=/; max-age=0`); ctx.redirect(process.env.FRONTEND_URI ?? "http://localhost:5173"); }; @@ -313,7 +312,7 @@ export const authRefresh = async (ctx: koa.Context): Promise => { async idToken => { try { const { sub } = jwt.decode(idToken) as jwt.JwtPayload; - + // Refresh the token const client = await getOidc(); const system = Container.get("actor-system"); @@ -333,7 +332,7 @@ export const authRefresh = async (ctx: koa.Context): Promise => { const fpStore = createActorUri("FingerprintStore"); try { let fpData: Fingerprint = await system.ask(fpStore, FingerprintStoreMessages.Get(session.fingerprint as Id)); - await system.ask(fpStore, FingerprintStoreMessages.IncreaseCount({fingerprint: session.fingerprint as Id, userUid: session.uid, initialQuiz: undefined})); + await system.ask(fpStore, FingerprintStoreMessages.IncreaseCount({ fingerprint: session.fingerprint as Id, userUid: session.uid, initialQuiz: undefined })); if (fpData.blocked) { system.send(sessionStore, SessionStoreMessages.RemoveSession(sub as Id)); ctx.set("Set-Cookie", `bearer=; path=/`); @@ -350,7 +349,7 @@ export const authRefresh = async (ctx: koa.Context): Promise => { ctx.body = "O.K."; return; } - + try { const newTokenSet = await client.refresh(session.refreshToken); const decoded = jwt.decode(newTokenSet.id_token ?? "") as jwt.JwtPayload; @@ -369,9 +368,14 @@ export const authRefresh = async (ctx: koa.Context): Promise => { refreshExpires: toTimestamp(refreshExpires), }) ); - console.log(`User ${sub} token was refreshed`); + console.log(`User ${sub} token was refreshed.`); ctx.set("Set-Cookie", `bearer=${newTokenSet.id_token}; path=/; expires=${refreshExpires.toHTTP()}`); - ctx.body = "O.K."; + // ctx.body = "O.K."; + ctx.body = { + expires_at: expires.toISO(), + refresh_expires: refreshExpires.toISO() + }; + } catch (e) { console.error("Failed to renew token", e); system.send(sessionStore, SessionStoreMessages.RemoveSession(sub as Id)); diff --git a/packages/frontend/package.json b/packages/frontend/package.json index ee10955..4a1c11c 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@recapp/frontend", "private": true, - "version": "1.6.3", + "version": "1.6.4", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/frontend/src/actors/TokenActor.ts b/packages/frontend/src/actors/TokenActor.ts index 60ffb7d..1921792 100644 --- a/packages/frontend/src/actors/TokenActor.ts +++ b/packages/frontend/src/actors/TokenActor.ts @@ -6,45 +6,64 @@ import Axios from "axios"; import { cookie } from "../utils"; export class TokenActor extends Actor { - public interval: any; - private expiresAt: Date; - - public constructor(name: string, system: ActorSystem) { - super(name, system); - this.expiresAt = new Date(); // Initialize with a default value - } - - public override async afterStart(): Promise { - this.updateToken(); - } - - public override async beforeShutdown(): Promise { - clearTimeout(this.interval); - } - - private updateToken = () => { - const hasToken = !!cookie("bearer"); - if (hasToken) { - Axios.get(import.meta.env.VITE_BACKEND_URI + "/auth/refresh", { withCredentials: true }) - .then(response => { - this.expiresAt = new Date(response.data.expires_at); - this.scheduleNextUpdate(); - }) - .catch(error => { - console.error("Failed to refresh token:", error); - setTimeout(this.updateToken, 5000); // Retry after 5 seconds - }); - } - }; - - private scheduleNextUpdate = () => { - const buffer = 30000; // 30 seconds before expiry - const delay = this.expiresAt.getTime() - Date.now() - buffer; - clearTimeout(this.interval); // Clear previous timeout - this.interval = setTimeout(this.updateToken, delay); - }; - - public async receive(_from: ActorRef, _message: unknown): Promise { - return unit(); - } + public interval: any; + private expiresAt: Date; + + public constructor(name: string, system: ActorSystem) { + super(name, system); + this.expiresAt = new Date(); // Initialize with the current dte and time + } + + public override async afterStart(): Promise { + this.updateToken(); + } + + public override async beforeShutdown(): Promise { + clearTimeout(this.interval); + } + + private updateToken = () => { + const hasToken = !!cookie("bearer"); + if (hasToken) { + Axios.get(import.meta.env.VITE_BACKEND_URI + "/auth/refresh", { withCredentials: true }) + .then(response => { + console.debug("[TokenActor] /auth/refresh response.data:", response.data); + // assume response.data.expires_at is ISO or epoch-string + this.expiresAt = new Date(response.data.expires_at); + this.scheduleNextUpdate(); + }) + .catch(error => { + console.error("[TokenActor] Failed to refresh token:", error); + setTimeout(this.updateToken, 5000); // Retry after 5 seconds + }); + } + }; + + private scheduleNextUpdate = () => { + const bufferMs = 30000; // 30 seconds before expiry + const now = Date.now(); + const expiryMs = this.expiresAt.getTime(); + + const delay = expiryMs - now - bufferMs; + + // --- DEBUG LOGGING START --- + console.debug("[TokenActor] now =", new Date(now).toISOString()); + console.debug("[TokenActor] expiresAt =", this.expiresAt.toISOString()); + console.debug("[TokenActor] bufferMs =", bufferMs, "ms"); + console.debug("[TokenActor] raw delay =", delay, "ms"); + // --- DEBUG LOGGING END --- + + clearTimeout(this.interval); // Clear previous timeout + + // clamp to at least 1 s to avoid tight loops + const safeDelay = Math.max(delay, 1_000); + if (delay <= 0) { + console.warn("[TokenActor] Computed delay <= 0, forcing retry in 1 s"); + } + this.interval = setTimeout(this.updateToken, safeDelay); + }; + + public async receive(_from: ActorRef, _message: unknown): Promise { + return unit(); + } } \ No newline at end of file