Add login redirection

*  Add login redirection

* 🚑 Fix cross site scripting using server side regex validation

*  Add unit test
This commit is contained in:
Manuel
2023-09-24 16:04:07 +02:00
committed by GitHub
parent 7d7fe6016b
commit 690c627f81
12 changed files with 275 additions and 65 deletions

View File

@@ -12,7 +12,7 @@ import {
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons-react';
import { GetServerSideProps } from 'next';
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
import { signIn } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Head from 'next/head';
@@ -20,14 +20,16 @@ import Image from 'next/image';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { z } from 'zod';
import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle';
import { FloatingBackground } from '~/components/layout/Background/FloatingBackground';
import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { signInSchema } from '~/validations/user';
import { ThemeSchemeToggle } from '~/components/ThemeSchemeToggle/ThemeSchemeToggle';
export default function LoginPage() {
export default function LoginPage({
redirectAfterLogin,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { t } = useTranslation('authentication/login');
const { i18nZodResolver } = useI18nZodResolver();
const router = useRouter();
@@ -54,7 +56,7 @@ export default function LoginPage() {
setIsError(true);
return;
}
router.push('/manage');
router.push(redirectAfterLogin ?? '/manage');
});
};
@@ -68,7 +70,7 @@ export default function LoginPage() {
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
<FloatingBackground />
<ThemeSchemeToggle pos="absolute" top={20} right={20}/>
<ThemeSchemeToggle pos="absolute" top={20} right={20} />
<Stack spacing={40} align="center" w="100%">
<Stack spacing={0} align="center">
<Image src="/imgs/logo/logo.svg" width={80} height={80} alt="" />
@@ -120,6 +122,12 @@ export default function LoginPage() {
<Button mt="xs" variant="light" fullWidth type="submit" loading={isLoading}>
{t('form.buttons.submit')}
</Button>
{redirectAfterLogin && (
<Text color="dimmed" align="center" size="xs">
{t('form.afterLoginRedirection', { url: redirectAfterLogin })}
</Text>
)}
</Stack>
</form>
</Card>
@@ -129,9 +137,16 @@ export default function LoginPage() {
);
}
export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => {
const regexExp = /^\/{1}[A-z\/]*$/;
export const getServerSideProps: GetServerSideProps = async ({ locale, req, res, query }) => {
const session = await getServerAuthSession({ req, res });
const zodResult = await z
.object({ redirectAfterLogin: z.string().regex(regexExp) })
.safeParseAsync(query);
const redirectAfterLogin = zodResult.success ? zodResult.data.redirectAfterLogin : null;
if (session) {
return {
redirect: {
@@ -144,6 +159,7 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res
return {
props: {
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
redirectAfterLogin,
},
};
};

View File

@@ -5,12 +5,13 @@ import { Dashboard } from '~/components/Dashboard/Dashboard';
import { BoardLayout } from '~/components/layout/Templates/BoardLayout';
import { useInitConfig } from '~/config/init';
import { env } from '~/env';
import { getServerAuthSession } from '~/server/auth';
import { configExists } from '~/tools/config/configExists';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { boardNamespaces } from '~/tools/server/translation-namespaces';
import { ConfigType } from '~/types/config';
import { getServerAuthSession } from '~/server/auth';
export default function BoardPage({
config: initialConfig,
@@ -35,13 +36,8 @@ const routeParamsSchema = z.object({
slug: z.string(),
});
export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = async ({
params,
locale,
req,
res,
}) => {
const routeParams = routeParamsSchema.safeParse(params);
export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = async (ctx) => {
const routeParams = routeParamsSchema.safeParse(ctx.params);
if (!routeParams.success) {
return {
notFound: true,
@@ -56,38 +52,32 @@ export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = a
}
const config = await getFrontendConfig(routeParams.data.slug);
const translations = await getServerSideTranslations(boardNamespaces, locale, req, res);
const translations = await getServerSideTranslations(
boardNamespaces,
ctx.locale,
ctx.req,
ctx.res
);
const getSuccessResponse = () => {
return {
props: {
config,
primaryColor: config.settings.customization.colors.primary,
secondaryColor: config.settings.customization.colors.secondary,
primaryShade: config.settings.customization.colors.shade,
dockerEnabled: !!env.DOCKER_HOST && !!env.DOCKER_PORT,
...translations,
},
};
const session = await getServerAuthSession({ req: ctx.req, res: ctx.res });
const result = checkForSessionOrAskForLogin(
ctx,
session,
() => config.settings.access.allowGuests || !session?.user
);
if (result) {
return result;
}
if (!config.settings.access.allowGuests) {
const session = await getServerAuthSession({ req, res });
if (session?.user) {
return getSuccessResponse();
}
return {
notFound: true,
props: {
primaryColor: config.settings.customization.colors.primary,
secondaryColor: config.settings.customization.colors.secondary,
primaryShade: config.settings.customization.colors.shade,
}
};
}
return getSuccessResponse();
return {
props: {
config,
primaryColor: config.settings.customization.colors.primary,
secondaryColor: config.settings.customization.colors.secondary,
primaryShade: config.settings.customization.colors.shade,
dockerEnabled: !!env.DOCKER_HOST && !!env.DOCKER_PORT,
...translations,
},
};
};

View File

@@ -12,7 +12,6 @@ import {
Title,
} from '@mantine/core';
import { useListState } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import {
IconBox,
IconCategory,
@@ -35,6 +34,7 @@ import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth';
import { sleep } from '~/tools/client/time';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api';
@@ -204,10 +204,9 @@ const BoardsPage = () => {
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session?.user) {
return {
notFound: true,
};
const result = checkForSessionOrAskForLogin(ctx, session, () => true);
if (result) {
return result;
}
const translations = await getServerSideTranslations(

View File

@@ -19,6 +19,7 @@ import {
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
@@ -132,10 +133,9 @@ export type CreateAccountSchema = z.infer<typeof createAccountSchema>;
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const session = await getServerAuthSession(ctx);
if (!session?.user.isAdmin) {
return {
notFound: true,
};
const result = checkForSessionOrAskForLogin(ctx, session, () => session?.user.isAdmin == true);
if (result) {
return result;
}
const translations = await getServerSideTranslations(

View File

@@ -0,0 +1,34 @@
import {
GetServerSideProps,
GetServerSidePropsContext,
GetServerSidePropsResult,
PreviewData,
} from 'next';
import { Session } from 'next-auth';
import { ParsedUrlQuery } from 'querystring';
export const checkForSessionOrAskForLogin = (
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
session: Session | null,
accessCallback: () => boolean,
): GetServerSidePropsResult<any> | undefined => {
if (!session?.user) {
console.log('detected logged out user!');
return {
props: {},
redirect: {
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
permanent: false,
},
};
}
if (!accessCallback()) {
return {
props: {},
notFound: true
}
}
return undefined;
};