diff --git a/apps/server/src/routes/login.spec.ts b/apps/server/src/routes/login.spec.ts index 6a0af3c52..4695a6f9e 100644 --- a/apps/server/src/routes/login.spec.ts +++ b/apps/server/src/routes/login.spec.ts @@ -82,6 +82,24 @@ describe("Login Route test", () => { expect(expiry).toStrictEqual(new Date(session!.cookie.expires!)); }); + it("doesn't renew the session on subsequent requests", async () => { + const { expiry: originalExpiry } = await getSessionFromCookie(setCookieHeader); + + // Simulate user waiting half the period before the session expires. + vi.setSystemTime(originalExpiry!.getTime() - (originalExpiry!.getTime() - Date.now()) / 2); + + // Make a request to renew the session. + await supertest(app) + .get("/") + .set("Cookie", setCookieHeader) + .expect(200); + + // Check the session is still valid and has not been renewed. + const { session, expiry } = await getSessionFromCookie(setCookieHeader); + expect(session).toBeTruthy(); + expect(expiry!.getTime()).toStrictEqual(originalExpiry!.getTime()); + }); + it("cleans up expired sessions", async () => { let { session, expiry } = await getSessionFromCookie(setCookieHeader); expect(session).toBeTruthy(); @@ -123,6 +141,24 @@ describe("Login Route test", () => { expect(expiry?.getTime()).toBeLessThanOrEqual(expectedExpirationDate.getTime()); }); + it("renews the session on subsequent requests", async () => { + const { expiry: originalExpiry } = await getSessionFromCookie(setCookieHeader); + + // Simulate user waiting half the period before the session expires. + vi.setSystemTime(originalExpiry!.getTime() - (originalExpiry!.getTime() - Date.now()) / 2); + + // Make a request to renew the session. + await supertest(app) + .get("/") + .set("Cookie", setCookieHeader) + .expect(200); + + // Check the session is still valid and has been renewed. + const { session, expiry } = await getSessionFromCookie(setCookieHeader); + expect(session).toBeTruthy(); + expect(expiry!.getTime()).toBeGreaterThan(originalExpiry!.getTime()); + }); + it("cleans up expired sessions", async () => { let { session, expiry } = await getSessionFromCookie(setCookieHeader); expect(session).toBeTruthy(); diff --git a/apps/server/src/routes/session_parser.ts b/apps/server/src/routes/session_parser.ts index 1c058f14d..7cee5c9e4 100644 --- a/apps/server/src/routes/session_parser.ts +++ b/apps/server/src/routes/session_parser.ts @@ -55,6 +55,23 @@ export class SQLiteSessionStore extends Store { } } + touch(sid: string, session: session.SessionData, callback?: (err?: any) => void): void { + // For now it's only for session cookies ("Remember me" unchecked). + if (session.cookie?.expires) { + callback?.(); + return; + } + + try { + const expires = Date.now() + 3600000; // fallback to 1 hour + sql.execute(/*sql*/`UPDATE sessions SET expires = ? WHERE id = ?`, [expires, sid]); + callback?.(); + } catch (e) { + log.error(e); + callback?.(e); + } + } + /** * Given a session ID, returns the expiry date of the session. * @@ -79,6 +96,7 @@ const sessionParser: express.RequestHandler = session({ secret: sessionSecret, resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request. saveUninitialized: false, // true forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. + rolling: true, // forces the session to be saved back to the session store, resetting the expiration date. cookie: { path: "/", httpOnly: true,