feat: Implement stayLoggedIn functionality for JWT authentication
This commit is contained in:
+4
-4
@@ -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,
|
||||
|
||||
|
||||
Generated
-8525
File diff suppressed because it is too large
Load Diff
+22
-7
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -15,5 +15,5 @@
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "tests"]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user