Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
80a01a1
Hotfix Can only click on stats detaiils when availible (#85)
bitbacchus May 26, 2025
39d3e86
Revert "Hotfix Can only click on stats detaiils when availible (#85)"…
bitbacchus May 26, 2025
b7617a0
Create deploy-test.yml
bitbacchus Jun 4, 2025
6312fa6
Update deploy-test.yml
bitbacchus Jun 4, 2025
90f1e04
Update deploy-test.yml
bitbacchus Jun 4, 2025
d9fc23f
Update deploy-test.yml
bitbacchus Jun 4, 2025
791f363
Delete .github/workflows/deploy-check.yml
bitbacchus Jun 4, 2025
f473b22
Update deployment.sh
bitbacchus Jun 4, 2025
88a71cc
Update deployment.sh
bitbacchus Jun 5, 2025
fe753d5
🔒 Improve token handling in TokenActor: retry on failure and clear pr…
bitbacchus Jun 6, 2025
9af576c
Update deploy-test.yml
bitbacchus Jun 6, 2025
14c1d26
Update deploy-test.yml
bitbacchus Jun 6, 2025
42f9fd4
Update deployment.sh
bitbacchus Jun 6, 2025
86cc4c3
Version 1.6.2
bitbacchus Jun 6, 2025
8e525f8
version 1.6.2
bitbacchus Jun 6, 2025
74cc217
:bug: fix(Root): defer auth cookie check to useEffect after actor ini…
bitbacchus Jun 10, 2025
d6917c2
:bug: Feature/prevent question details without data (#90)
bitbacchus Jun 10, 2025
beaeb52
Feature/docker debian slim wkhtmltopdf install (#91)
bitbacchus Jun 10, 2025
e6234b0
Feature/feature/root add spinner on init (#92)
bitbacchus Jun 11, 2025
027de56
Merge branch 'production' into main
bitbacchus Jun 11, 2025
18c22b5
:bug: fix(authRefresh): break infinite refresh loop by returning exip…
bitbacchus Jun 23, 2025
c97c84b
Merge branch 'production' into main
bitbacchus Jun 23, 2025
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();
}
}