feat: Implement stayLoggedIn functionality for JWT authentication

This commit is contained in:
2025-09-29 09:36:36 +02:00
parent ce3138922d
commit 9a600cac0f
7 changed files with 73 additions and 8542 deletions
+4 -4
View File
@@ -1,10 +1,10 @@
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
const eslint = require("@eslint/js");
const tseslint = require("typescript-eslint");
const eslintConfigPrettier = require("eslint-config-prettier");
export default tseslint.config(
module.exports = tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
-8525
View File
File diff suppressed because it is too large Load Diff
+22 -7
View File
@@ -8,6 +8,9 @@ import { validateBody, v } from "./middleware/validate";
import { ok, badRequest, unauthorized, created, conflict, notFound } from "./utils/responses";
import { UserService } from "../services/db/UserService";
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const MONTH_IN_MS = 30 * 24 * 60 * 60 * 1000;
export class RestAuth {
private readonly userService: UserService;
private readonly jwtAuthenticator: JwtAuthenticator;
@@ -72,9 +75,14 @@ export class RestAuth {
validateBody({
username: { required: true, validator: v.isString({ nonEmpty: true }) },
password: { required: true, validator: v.isString({ nonEmpty: true }) },
stayLoggedIn: { required: false, validator: v.isBoolean() },
}),
asyncHandler(async (req, res) => {
const { username, password } = req.body as { username: string; password: string };
const { username, password, stayLoggedIn } = req.body as {
username: string;
password: string;
stayLoggedIn: boolean;
};
const user = await this.userService.getUserAuthByName(username);
if (!user) {
@@ -86,17 +94,24 @@ export class RestAuth {
return unauthorized(res, "Invalid password", { field: "password", code: "INVALID_PASSWORD" });
}
const jwtToken = this.jwtAuthenticator.generateToken({
username: user.name,
id: user.id,
uuid: user.uuid,
});
const tokenAgeMs = stayLoggedIn
? MONTH_IN_MS
: DAY_IN_MS;
const jwtToken = this.jwtAuthenticator.generateToken(
{
username: user.name,
id: user.id,
uuid: user.uuid,
},
tokenAgeMs
);
res.cookie("auth-token", jwtToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 24 * 60 * 60 * 1000,
maxAge: tokenAgeMs,
});
return ok(res, { token: jwtToken });
+6 -2
View File
@@ -19,7 +19,11 @@ export class JwtAuthenticator {
return null;
}
public generateToken(payload: DecodedToken): string {
return jwt.sign(payload, this.secret);
public generateToken(payload: DecodedToken, expiresInMs?: number): string {
const options: jwt.SignOptions = {};
if (expiresInMs !== undefined) {
options.expiresIn = Math.floor(expiresInMs / 1000);
}
return jwt.sign(payload, this.secret, options);
}
}
+29 -2
View File
@@ -4,7 +4,6 @@ import express from "express";
import {RestAuth} from "../../src/rest/auth";
import {JwtAuthenticator} from "../../src/utils/jwtAuthenticator";
import {PasswordUtils} from "../../src/utils/passwordUtils";
// @ts-ignore
import {createMockJwtAuthenticator, createMockUserService, createPublicTestApp} from "../helpers/testSetup";
import crypto from "crypto";
@@ -172,7 +171,7 @@ describe("RestAuth", () => {
username: "testuser",
id: "user-id-123",
uuid: "uuid-123",
});
}, 24 * 60 * 60 * 1000);
const cookieHeader = response.headers['set-cookie'];
expect(cookieHeader).toBeDefined();
@@ -187,6 +186,34 @@ describe("RestAuth", () => {
expect(authTokenCookie).toContain("SameSite=Lax");
});
it("should login with longer token validity when stayLoggedIn is true", async () => {
const mockUser = {name: "testuser", password: "hashed", uuid: "uuid-123", id: "user-id-123"};
const mockToken = "jwt-token-123";
const loginDataWithStayLoggedIn = { ...validLoginData, stayLoggedIn: true };
mockUserService.getUserAuthByName.mockResolvedValue(mockUser);
mockPasswordUtils.comparePassword.mockResolvedValue(true);
mockJwtAuthenticator.generateToken.mockReturnValue(mockToken);
const response = await request(app).post("/auth/login").send(loginDataWithStayLoggedIn).expect(200);
expect(response.body.ok).toBe(true);
expect(response.body.data.token).toBe(mockToken);
expect(mockJwtAuthenticator.generateToken).toHaveBeenCalledWith({
username: "testuser",
id: "user-id-123",
uuid: "uuid-123",
}, 30 * 24 * 60 * 60 * 1000);
const cookieHeader = response.headers['set-cookie'];
expect(cookieHeader).toBeDefined();
const cookies = Array.isArray(cookieHeader) ? cookieHeader : [cookieHeader!];
const authTokenCookie = cookies.find((cookie: string) => cookie.startsWith("auth-token="));
expect(authTokenCookie).toBeDefined();
expect(authTokenCookie).toContain(`auth-token=${mockToken}`);
});
describe("POST /logout", () => {
it("should clear the auth-token cookie and return a success message", async () => {
const response = await request(app).post("/auth/logout").send().expect(200);
+11 -1
View File
@@ -64,7 +64,17 @@ describe("JwtAuthenticator", () => {
const payload = { username: "bob" } as any;
const token = auth.generateToken(payload);
expect(jwt.sign).toHaveBeenCalledWith(payload, secret);
expect(jwt.sign).toHaveBeenCalledWith(payload, secret, {});
expect(token).toBe("signed.jwt");
});
it("generateToken signs payload with expiry when expiresInMs is provided", () => {
(jwt.sign as any).mockReturnValue("signed.jwt");
const payload = { username: "bob" } as any;
const expiresInMs = 24 * 60 * 60 * 1000;
const token = auth.generateToken(payload, expiresInMs);
expect(jwt.sign).toHaveBeenCalledWith(payload, secret, { expiresIn: 86400 }); // 86400 Sekunden = 1 Tag
expect(token).toBe("signed.jwt");
});
});
+1 -1
View File
@@ -15,5 +15,5 @@
]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "tests"]
"exclude": ["node_modules"]
}