mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-28 01:10:54 +01:00
feat: add session expiry (#951)
This commit is contained in:
@@ -1,14 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { useEffect } from "react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { SessionProvider } from "@homarr/auth/client";
|
||||
import { SessionProvider, signIn } from "@homarr/auth/client";
|
||||
|
||||
interface AuthProviderProps {
|
||||
session: Session | null;
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children, session }: PropsWithChildren<AuthProviderProps>) => {
|
||||
useLoginRedirectOnSessionExpiry(session);
|
||||
return <SessionProvider session={session}>{children}</SessionProvider>;
|
||||
};
|
||||
|
||||
const useLoginRedirectOnSessionExpiry = (session: Session | null) => {
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
if (!session) return () => {};
|
||||
//setTimeout doesn't allow for a number higher than 2147483647 (2³¹-1 , or roughly 24 days)
|
||||
const timeout = setTimeout(() => void signIn(), Math.min(dayjs(session.expires).diff(), 2147483647));
|
||||
return () => clearTimeout(timeout);
|
||||
}, [session]);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ import { eq, inArray } from "@homarr/db";
|
||||
import { groupMembers, groupPermissions } from "@homarr/db/schema/sqlite";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
|
||||
import { expireDateAfter, generateSessionToken, sessionMaxAgeInSeconds, sessionTokenCookieName } from "./session";
|
||||
import { env } from "./env.mjs";
|
||||
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
|
||||
|
||||
export const getCurrentUserPermissionsAsync = async (db: Database, userId: string) => {
|
||||
const dbGroupMembers = await db.query.groupMembers.findMany({
|
||||
@@ -53,18 +54,18 @@ export const createSignInCallback =
|
||||
}
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
|
||||
const sessionExpires = expireDateAfter(env.AUTH_SESSION_EXPIRY_TIME);
|
||||
|
||||
await adapter.createSession({
|
||||
sessionToken,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
userId: user.id!,
|
||||
expires: sessionExpiry,
|
||||
expires: sessionExpires,
|
||||
});
|
||||
|
||||
cookies().set(sessionTokenCookieName, sessionToken, {
|
||||
path: "/",
|
||||
expires: sessionExpiry,
|
||||
expires: sessionExpires,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
|
||||
@@ -7,12 +7,13 @@ import { db } from "@homarr/db";
|
||||
|
||||
import { adapter } from "./adapter";
|
||||
import { createSessionCallback, createSignInCallback } from "./callbacks";
|
||||
import { env } from "./env.mjs";
|
||||
import { createCredentialsConfiguration } from "./providers/credentials/credentials-provider";
|
||||
import { EmptyNextAuthProvider } from "./providers/empty/empty-provider";
|
||||
import { filterProviders } from "./providers/filter-providers";
|
||||
import { OidcProvider } from "./providers/oidc/oidc-provider";
|
||||
import { createRedirectUri } from "./redirect";
|
||||
import { sessionMaxAgeInSeconds, sessionTokenCookieName } from "./session";
|
||||
import { sessionTokenCookieName } from "./session";
|
||||
|
||||
export const createConfiguration = (isCredentialsRequest: boolean, headers: ReadonlyHeaders | null) =>
|
||||
NextAuth({
|
||||
@@ -43,7 +44,7 @@ export const createConfiguration = (isCredentialsRequest: boolean, headers: Read
|
||||
secret: "secret-is-not-defined-yet", // TODO: This should be added later
|
||||
session: {
|
||||
strategy: "database",
|
||||
maxAge: sessionMaxAgeInSeconds,
|
||||
maxAge: env.AUTH_SESSION_EXPIRY_TIME,
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
|
||||
@@ -23,6 +23,29 @@ const authProvidersSchema = z
|
||||
)
|
||||
.default("credentials");
|
||||
|
||||
const createDurationSchema = (defaultValue) =>
|
||||
z
|
||||
.string()
|
||||
.regex(/^\d+[smhd]?$/)
|
||||
.default(defaultValue)
|
||||
.transform((duration) => {
|
||||
const lastChar = duration[duration.length - 1];
|
||||
if (!isNaN(Number(lastChar))) {
|
||||
return Number(defaultValue);
|
||||
}
|
||||
|
||||
const multipliers = {
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 60 * 60,
|
||||
d: 60 * 60 * 24,
|
||||
};
|
||||
const numberDuration = Number(duration.slice(0, -1));
|
||||
const multiplier = multipliers[lastChar];
|
||||
|
||||
return numberDuration * multiplier;
|
||||
});
|
||||
|
||||
const booleanSchema = z
|
||||
.string()
|
||||
.default("false")
|
||||
@@ -39,6 +62,7 @@ const authProviders = skipValidation ? [] : authProvidersSchema.parse(process.en
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
AUTH_SESSION_EXPIRY_TIME: createDurationSchema("30d"),
|
||||
AUTH_SECRET: process.env.NODE_ENV === "production" ? z.string().min(1) : z.string().min(1).optional(),
|
||||
AUTH_PROVIDERS: authProvidersSchema,
|
||||
...(authProviders.includes("oidc")
|
||||
@@ -70,6 +94,7 @@ export const env = createEnv({
|
||||
},
|
||||
client: {},
|
||||
runtimeEnv: {
|
||||
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_PROVIDERS: process.env.AUTH_PROVIDERS,
|
||||
AUTH_LDAP_BASE: process.env.AUTH_LDAP_BASE,
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { Database } from "@homarr/db";
|
||||
|
||||
import { getCurrentUserPermissionsAsync } from "./callbacks";
|
||||
|
||||
export const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||
export const sessionTokenCookieName = "next-auth.session-token";
|
||||
|
||||
export const expireDateAfter = (seconds: number) => {
|
||||
|
||||
@@ -132,6 +132,13 @@ const createAdapter = () => {
|
||||
type SessionExport = typeof import("../session");
|
||||
const mockSessionToken = "e9ef3010-6981-4a81-b9d6-8495d09cf3b5";
|
||||
const mockSessionExpiry = new Date("2023-07-01");
|
||||
vi.mock("../env.mjs", () => {
|
||||
return {
|
||||
env: {
|
||||
AUTH_SESSION_EXPIRY_TIME: 60 * 60 * 24 * 7,
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("../session", async (importOriginal) => {
|
||||
const mod = await importOriginal<SessionExport>();
|
||||
|
||||
@@ -185,7 +192,7 @@ describe("createSignInCallback", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should call adapter.createSession with correct input", async () => {
|
||||
test("should call adapter.createSession with correct input", async () => {
|
||||
const adapter = createAdapter();
|
||||
const isCredentialsRequest = true;
|
||||
const signInCallback = createSignInCallback(adapter, isCredentialsRequest);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"AUTH_OIDC_AUTO_LOGIN",
|
||||
"AUTH_PROVIDERS",
|
||||
"AUTH_SECRET",
|
||||
"AUTH_SESSION_EXPIRY_TIME",
|
||||
"CI",
|
||||
"DISABLE_REDIS_LOGS",
|
||||
"DB_URL",
|
||||
|
||||
Reference in New Issue
Block a user