Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@
"start": "ts-node ./src/index.ts",
"test": "npm test"
},
"version": "1.0.1"
"version": "1.0.2"
}
8 changes: 7 additions & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "<anonymous>");
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<string[]>("api-keys")[0]
Authorization: "apikey=" + Container.get<string[]>("api-keys")[0]
},
});
const system = await DistributedActorSystem.create({
Expand Down
48 changes: 26 additions & 22 deletions packages/backend/src/middlewares/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -63,7 +62,7 @@ export const authTempAccount = async (ctx: koa.Context): Promise<void> => {
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);
Expand All @@ -81,8 +80,8 @@ export const authTempAccount = async (ctx: koa.Context): Promise<void> => {
};
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");
Expand All @@ -91,18 +90,18 @@ export const authTempAccount = async (ctx: koa.Context): Promise<void> => {
} 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));
Expand Down Expand Up @@ -299,7 +298,7 @@ export const authLogout = async (ctx: koa.Context): Promise<void> => {
},
() => 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");
};
Expand All @@ -313,7 +312,7 @@ export const authRefresh = async (ctx: koa.Context): Promise<void> => {
async idToken => {
try {
const { sub } = jwt.decode(idToken) as jwt.JwtPayload;

// Refresh the token
const client = await getOidc();
const system = Container.get<ActorSystem>("actor-system");
Expand All @@ -333,7 +332,7 @@ export const authRefresh = async (ctx: koa.Context): Promise<void> => {
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=/`);
Expand All @@ -350,7 +349,7 @@ export const authRefresh = async (ctx: koa.Context): Promise<void> => {
ctx.body = "O.K.";
return;
}

try {
const newTokenSet = await client.refresh(session.refreshToken);
const decoded = jwt.decode(newTokenSet.id_token ?? "") as jwt.JwtPayload;
Expand All @@ -369,9 +368,14 @@ export const authRefresh = async (ctx: koa.Context): Promise<void> => {
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));
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@recapp/frontend",
"private": true,
"version": "1.6.3",
"version": "1.6.4",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
101 changes: 60 additions & 41 deletions packages/frontend/src/actors/TokenActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,64 @@ import Axios from "axios";
import { cookie } from "../utils";

export class TokenActor extends Actor<unknown, Unit> {
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<void> {
this.updateToken();
}

public override async beforeShutdown(): Promise<void> {
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<Unit> {
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<void> {
this.updateToken();
}

public override async beforeShutdown(): Promise<void> {
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<Unit> {
return unit();
}
}
Loading