mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-17 20:17:02 +01:00
Add logout callback URL and session expiration environment variables (#2023)
Co-authored-by: @Meierschlumpf
This commit is contained in:
@@ -14,6 +14,7 @@ import { signOut, useSession } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import { forwardRef } from 'react';
|
||||
import { useLogoutUrl } from '~/hooks/custom-session-provider';
|
||||
import { useColorScheme } from '~/hooks/use-colorscheme';
|
||||
|
||||
import { useBoardLink } from '../Templates/BoardLayout';
|
||||
@@ -26,6 +27,8 @@ export const AvatarMenu = () => {
|
||||
const Icon = colorScheme === 'dark' ? IconSun : IconMoonStars;
|
||||
const defaultBoardHref = useBoardLink('/board');
|
||||
|
||||
const logoutUrl = useLogoutUrl();
|
||||
|
||||
return (
|
||||
<Menu width={256}>
|
||||
<Menu.Target>
|
||||
@@ -64,11 +67,13 @@ export const AvatarMenu = () => {
|
||||
<Menu.Item
|
||||
icon={<IconLogout size="1rem" />}
|
||||
color="red"
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
signOut({
|
||||
redirect: false,
|
||||
}).then(() => window.location.reload())
|
||||
}
|
||||
}).then(() =>
|
||||
logoutUrl ? window.location.assign(logoutUrl) : window.location.reload()
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('actions.avatar.logout', {
|
||||
username: sessionData.user.name,
|
||||
|
||||
13
src/env.js
13
src/env.js
@@ -16,7 +16,7 @@ const numberSchema = z
|
||||
.string()
|
||||
.regex(/\d*/)
|
||||
.transform((value) => (value === undefined ? undefined : Number(value)))
|
||||
.optional()
|
||||
.optional();
|
||||
|
||||
const portSchema = z
|
||||
.string()
|
||||
@@ -49,6 +49,12 @@ const env = createEnv({
|
||||
DEMO_MODE: z.string().optional(),
|
||||
HOSTNAME: z.string().optional(),
|
||||
|
||||
//regex allows number with extra letter as time multiplier, applied with secondsFromTimeString
|
||||
AUTH_SESSION_EXPIRY_TIME: z
|
||||
.string()
|
||||
.regex(/^\d+[smhd]?$/)
|
||||
.optional(),
|
||||
|
||||
// Authentication
|
||||
AUTH_PROVIDER: z
|
||||
.string()
|
||||
@@ -96,7 +102,7 @@ const env = createEnv({
|
||||
AUTH_OIDC_OWNER_GROUP: z.string().default('admin'),
|
||||
AUTH_OIDC_AUTO_LOGIN: zodParsedBoolean(),
|
||||
AUTH_OIDC_SCOPE_OVERWRITE: z.string().default('openid email profile groups'),
|
||||
AUTH_OIDC_TIMEOUT: numberSchema.default(3500)
|
||||
AUTH_OIDC_TIMEOUT: numberSchema.default('3500'),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -118,6 +124,7 @@ const env = createEnv({
|
||||
.optional()
|
||||
.default('light'),
|
||||
NEXT_PUBLIC_DOCKER_HOST: z.string().optional(),
|
||||
AUTH_LOGOUT_REDIRECT_URL: z.string().optional(),
|
||||
},
|
||||
/**
|
||||
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
|
||||
@@ -157,6 +164,8 @@ const env = createEnv({
|
||||
AUTH_OIDC_AUTO_LOGIN: process.env.AUTH_OIDC_AUTO_LOGIN,
|
||||
AUTH_OIDC_SCOPE_OVERWRITE: process.env.AUTH_OIDC_SCOPE_OVERWRITE,
|
||||
AUTH_OIDC_TIMEOUT: process.env.AUTH_OIDC_TIMEOUT,
|
||||
AUTH_LOGOUT_REDIRECT_URL: process.env.AUTH_LOGOUT_REDIRECT_URL,
|
||||
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
||||
DEMO_MODE: process.env.DEMO_MODE,
|
||||
},
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
|
||||
44
src/hooks/custom-session-provider.tsx
Normal file
44
src/hooks/custom-session-provider.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { Session } from 'next-auth';
|
||||
import { SessionProvider, signIn } from 'next-auth/react';
|
||||
import { createContext, useContext, useEffect } from 'react';
|
||||
|
||||
interface CustomSessionProviderProps {
|
||||
session: Session;
|
||||
children: React.ReactNode;
|
||||
logoutUrl?: string;
|
||||
}
|
||||
|
||||
export const CustomSessionProvider = ({
|
||||
session,
|
||||
children,
|
||||
logoutUrl,
|
||||
}: CustomSessionProviderProps) => {
|
||||
//Automatically redirect to the login page after a session expires or after 24 days
|
||||
useEffect(() => {
|
||||
if (!session) return () => {};
|
||||
//setTimeout doesn't allow for a number higher than 2147483647 (2³¹-1 , or roughly 24 days)
|
||||
const timeout = setTimeout(signIn, Math.min(dayjs(session.expires).diff(), 2147483647));
|
||||
return () => clearTimeout(timeout);
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<SessionProvider session={session} refetchOnWindowFocus={false}>
|
||||
<SessionContext.Provider value={{ logoutUrl }}>{children}</SessionContext.Provider>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
interface SessionContextProps {
|
||||
logoutUrl?: string;
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextProps | null>(null);
|
||||
|
||||
export function useLogoutUrl() {
|
||||
const context = useContext(SessionContext);
|
||||
if (!context) {
|
||||
throw new Error('You cannot use logoutUrl outside of session context.');
|
||||
}
|
||||
return context.logoutUrl;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import utc from 'dayjs/plugin/utc';
|
||||
import 'flag-icons/css/flag-icons.min.css';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
import { Session } from 'next-auth';
|
||||
import { getSession, SessionProvider } from 'next-auth/react';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { AppProps } from 'next/app';
|
||||
import Script from 'next/script';
|
||||
@@ -19,12 +19,16 @@ import 'video.js/dist/video-js.css';
|
||||
import { CommonHead } from '~/components/layout/Meta/CommonHead';
|
||||
import { ConfigProvider } from '~/config/provider';
|
||||
import { env } from '~/env.js';
|
||||
import { CustomSessionProvider } from '~/hooks/custom-session-provider';
|
||||
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
|
||||
import { modals } from '~/modals';
|
||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||
import { ColorTheme } from '~/tools/color';
|
||||
import { getLanguageByCode } from '~/tools/language';
|
||||
import { getServiceSidePackageAttributes, ServerSidePackageAttributesType } from '~/tools/server/getPackageVersion';
|
||||
import {
|
||||
ServerSidePackageAttributesType,
|
||||
getServiceSidePackageAttributes,
|
||||
} from '~/tools/server/getPackageVersion';
|
||||
import { theme } from '~/tools/server/theme/theme';
|
||||
import { ConfigType } from '~/types/config';
|
||||
import { api } from '~/utils/api';
|
||||
@@ -44,6 +48,7 @@ function App(
|
||||
environmentColorScheme: MantineColorScheme;
|
||||
packageAttributes: ServerSidePackageAttributesType;
|
||||
editModeEnabled: boolean;
|
||||
logoutUrl?: string;
|
||||
analyticsEnabled: boolean;
|
||||
config?: ConfigType;
|
||||
primaryColor?: MantineTheme['primaryColor'];
|
||||
@@ -111,7 +116,7 @@ function App(
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
)}
|
||||
<SessionProvider session={pageProps.session}>
|
||||
<CustomSessionProvider session={pageProps.session} logoutUrl={pageProps.logoutUrl}>
|
||||
<ColorSchemeProvider {...pageProps}>
|
||||
{(colorScheme) => (
|
||||
<ColorTheme.Provider value={colorTheme}>
|
||||
@@ -151,7 +156,7 @@ function App(
|
||||
)}
|
||||
</ColorSchemeProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</SessionProvider>
|
||||
</CustomSessionProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -177,6 +182,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
|
||||
pageProps: {
|
||||
...getActiveColorScheme(session, ctx),
|
||||
packageAttributes: getServiceSidePackageAttributes(),
|
||||
logoutUrl: env.AUTH_LOGOUT_REDIRECT_URL,
|
||||
analyticsEnabled,
|
||||
session,
|
||||
locale: ctx.locale ?? 'en',
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function LoginPage({
|
||||
validateInputOnChange: true,
|
||||
validateInputOnBlur: true,
|
||||
validate: i18nZodResolver(signInSchemaWithProvider),
|
||||
initialValues: { name: '', password: '', provider: '' },
|
||||
});
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof signInSchemaWithProvider>) => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { type GetServerSidePropsContext, type NextApiRequest, type NextApiRespon
|
||||
import { type NextAuthOptions, getServerSession } from 'next-auth';
|
||||
import { Adapter } from 'next-auth/adapters';
|
||||
import { decode, encode } from 'next-auth/jwt';
|
||||
import { env } from '~/env';
|
||||
import { secondsFromTimeString } from '~/tools/client/parseDuration';
|
||||
import { adapter, getProviders, onCreateUser } from '~/utils/auth';
|
||||
import { createRedirectUri } from '~/utils/auth/oidc';
|
||||
import EmptyNextAuthProvider from '~/utils/empty-provider';
|
||||
@@ -13,7 +15,8 @@ import { colorSchemeParser } from '~/validations/user';
|
||||
import { db } from './db';
|
||||
import { users } from './db/schema';
|
||||
|
||||
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||
const sessionMaxAgeInSeconds =
|
||||
secondsFromTimeString(env.AUTH_SESSION_EXPIRY_TIME) ?? 30 * 24 * 60 * 60; // 30 days
|
||||
|
||||
/**
|
||||
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
||||
|
||||
@@ -18,3 +18,32 @@ export const parseDuration = (time: number, t: TFunction): string => {
|
||||
|
||||
return eta;
|
||||
};
|
||||
|
||||
export const secondsFromTimeString = (time: string | undefined): number | undefined => {
|
||||
if (!time) {
|
||||
return undefined;
|
||||
}
|
||||
const lastChar = time[time.length - 1];
|
||||
if (!isNaN(+lastChar)) {
|
||||
return Number(time);
|
||||
}
|
||||
|
||||
const numTime = +time.substring(0, time.length - 1);
|
||||
switch (lastChar.toLowerCase()) {
|
||||
case 's': {
|
||||
return numTime;
|
||||
}
|
||||
case 'm': {
|
||||
return numTime * 60;
|
||||
}
|
||||
case 'h': {
|
||||
return numTime * 60 * 60;
|
||||
}
|
||||
case 'd': {
|
||||
return numTime * 24 * 60 * 60;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user