From c30c50ba4f78d1679661bbc8705342f6df5bef6e Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:53:09 +0200 Subject: [PATCH 1/4] show error on login --- .../pages/auth/LoginMain/LoginMainPage.tsx | 18 ++++++- web/src/routes/auth/callback.tsx | 49 ++++++++++++------- web/src/routes/auth/login.tsx | 6 +++ 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/web/src/pages/auth/LoginMain/LoginMainPage.tsx b/web/src/pages/auth/LoginMain/LoginMainPage.tsx index c5edcbff8f..f6d279b92f 100644 --- a/web/src/pages/auth/LoginMain/LoginMainPage.tsx +++ b/web/src/pages/auth/LoginMain/LoginMainPage.tsx @@ -8,17 +8,23 @@ import { useAppForm } from '../../../shared/form'; import './style.scss'; import { revalidateLogic } from '@tanstack/react-form'; import { useMutation, useQuery } from '@tanstack/react-query'; +import { useNavigate, useSearch } from '@tanstack/react-router'; import type { AxiosError } from 'axios'; import { useEffect, useMemo, useRef, useState } from 'react'; import api from '../../../shared/api/api'; -import type { OpenIdAuthInfo } from '../../../shared/api/types'; +import { getApiErrorMessage } from '../../../shared/api/apiErrorMessages'; +import { type OpenIdAuthInfo, WebErrorCode } from '../../../shared/api/types'; import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; import { InfoBanner } from '../../../shared/defguard-ui/components/InfoBanner/InfoBanner'; import { OIDCButton } from '../../../shared/defguard-ui/components/SSOButton/OIDCButton'; +import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { createZodIssue } from '../../../shared/defguard-ui/utils/zod'; import { useAuth } from '../../../shared/hooks/useAuth'; +const isWebErrorCode = (value: string): value is WebErrorCode => + (Object.values(WebErrorCode) as string[]).includes(value); + const formSchema = z.object({ username: z.string(m.form_error_required()).trim().min(1, m.form_error_required()), password: z.string(m.form_error_required()).trim().min(1, m.form_error_required()), @@ -34,6 +40,16 @@ const defaults: FormFields = { export const LoginMainPage = () => { const [tooManyAttempts, setTooManyAttempts] = useState(false); const attemptsTimeoutRef = useRef(null); + const { authError } = useSearch({ from: '/auth/login' }); + const navigate = useNavigate({ from: '/auth/login' }); + + useEffect(() => { + if (!isPresent(authError)) return; + Snackbar.error( + isWebErrorCode(authError) ? getApiErrorMessage(authError) : authError, + ); + void navigate({ search: {}, replace: true }); + }, [authError, navigate]); const { data: openIdAuthInfo } = useQuery({ queryFn: api.openid.authInfo, diff --git a/web/src/routes/auth/callback.tsx b/web/src/routes/auth/callback.tsx index e0defaf434..64d34f10c4 100644 --- a/web/src/routes/auth/callback.tsx +++ b/web/src/routes/auth/callback.tsx @@ -3,23 +3,43 @@ import type { AxiosError } from 'axios'; import z from 'zod'; import { LoginLoadingPage } from '../../pages/auth/LoginLoading/LoginLoadingPage'; import api from '../../shared/api/api'; -import { getApiErrorMessage } from '../../shared/api/apiErrorMessages'; import { type ApiError, WebErrorCode } from '../../shared/api/types'; -import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar'; import { useAuth } from '../../shared/hooks/useAuth'; -const searchSchema = z.object({ - code: z.string(), - state: z.string(), -}); +const getAuthErrorFromException = (e: unknown): WebErrorCode | undefined => { + const code = (e as AxiosError).response?.data?.code; + if (code === WebErrorCode.UserGroupsNotSynced || code === WebErrorCode.LicenseLimitReached) { + return code; + } + return undefined; +}; + +const searchSchema = z.union([ + z.object({ + code: z.string(), + state: z.string(), + }), + z.object({ + error: z.string(), + error_description: z.string().optional(), + state: z.string().optional(), + }), +]); // This is used when someone wants to login through a provider export const Route = createFileRoute('/auth/callback')({ validateSearch: searchSchema, loaderDeps: ({ search }) => ({ search }), loader: async ({ deps, context }) => { + const search = deps.search; + if ('error' in search) { + throw redirect({ + to: '/auth/login', + replace: true, + search: { authError: search.error_description ?? search.error }, + }); + } try { - const search = deps.search; const response = await api.openid.callback(search); setTimeout(() => { void context.queryClient.invalidateQueries({ @@ -31,16 +51,11 @@ export const Route = createFileRoute('/auth/callback')({ useAuth.getState().authSubject.next(response.data); }, 1000); } catch (e) { - const code = (e as AxiosError).response?.data?.code; - if ( - code === WebErrorCode.UserGroupsNotSynced || - code === WebErrorCode.LicenseLimitReached - ) { - setTimeout(() => { - Snackbar.error(getApiErrorMessage(code)); - }, 1000); - } - throw redirect({ to: '/auth/login', replace: true }); + throw redirect({ + to: '/auth/login', + replace: true, + search: { authError: getAuthErrorFromException(e) }, + }); } }, component: LoginLoadingPage, diff --git a/web/src/routes/auth/login.tsx b/web/src/routes/auth/login.tsx index 54d103c9d7..bcb6c14184 100644 --- a/web/src/routes/auth/login.tsx +++ b/web/src/routes/auth/login.tsx @@ -1,6 +1,12 @@ import { createFileRoute } from '@tanstack/react-router'; +import z from 'zod'; import { LoginMainPage } from '../../pages/auth/LoginMain/LoginMainPage'; +const searchSchema = z.object({ + authError: z.string().optional(), +}); + export const Route = createFileRoute('/auth/login')({ + validateSearch: searchSchema, component: LoginMainPage, }); From 09b00973e6bfd20f662dd18d43378da2b0172e6f Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:03:32 +0200 Subject: [PATCH 2/4] linter --- web/src/pages/auth/LoginMain/LoginMainPage.tsx | 4 +--- web/src/routes/auth/callback.tsx | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/web/src/pages/auth/LoginMain/LoginMainPage.tsx b/web/src/pages/auth/LoginMain/LoginMainPage.tsx index f6d279b92f..856d243d4d 100644 --- a/web/src/pages/auth/LoginMain/LoginMainPage.tsx +++ b/web/src/pages/auth/LoginMain/LoginMainPage.tsx @@ -45,9 +45,7 @@ export const LoginMainPage = () => { useEffect(() => { if (!isPresent(authError)) return; - Snackbar.error( - isWebErrorCode(authError) ? getApiErrorMessage(authError) : authError, - ); + Snackbar.error(isWebErrorCode(authError) ? getApiErrorMessage(authError) : authError); void navigate({ search: {}, replace: true }); }, [authError, navigate]); diff --git a/web/src/routes/auth/callback.tsx b/web/src/routes/auth/callback.tsx index 64d34f10c4..19ab090bcf 100644 --- a/web/src/routes/auth/callback.tsx +++ b/web/src/routes/auth/callback.tsx @@ -8,7 +8,10 @@ import { useAuth } from '../../shared/hooks/useAuth'; const getAuthErrorFromException = (e: unknown): WebErrorCode | undefined => { const code = (e as AxiosError).response?.data?.code; - if (code === WebErrorCode.UserGroupsNotSynced || code === WebErrorCode.LicenseLimitReached) { + if ( + code === WebErrorCode.UserGroupsNotSynced || + code === WebErrorCode.LicenseLimitReached + ) { return code; } return undefined; From c33b374d19da0e989b32c0d3c2d8da3b30a2d001 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:06:55 +0200 Subject: [PATCH 3/4] change method of dispalying the error --- web/src/pages/auth/LoginMain/LoginMainPage.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/pages/auth/LoginMain/LoginMainPage.tsx b/web/src/pages/auth/LoginMain/LoginMainPage.tsx index 856d243d4d..fe615e67e9 100644 --- a/web/src/pages/auth/LoginMain/LoginMainPage.tsx +++ b/web/src/pages/auth/LoginMain/LoginMainPage.tsx @@ -17,7 +17,6 @@ import { type OpenIdAuthInfo, WebErrorCode } from '../../../shared/api/types'; import { Divider } from '../../../shared/defguard-ui/components/Divider/Divider'; import { InfoBanner } from '../../../shared/defguard-ui/components/InfoBanner/InfoBanner'; import { OIDCButton } from '../../../shared/defguard-ui/components/SSOButton/OIDCButton'; -import { Snackbar } from '../../../shared/defguard-ui/providers/snackbar/snackbar'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; import { createZodIssue } from '../../../shared/defguard-ui/utils/zod'; import { useAuth } from '../../../shared/hooks/useAuth'; @@ -42,10 +41,13 @@ export const LoginMainPage = () => { const attemptsTimeoutRef = useRef(null); const { authError } = useSearch({ from: '/auth/login' }); const navigate = useNavigate({ from: '/auth/login' }); + const [authErrorMessage, setAuthErrorMessage] = useState(null); useEffect(() => { if (!isPresent(authError)) return; - Snackbar.error(isWebErrorCode(authError) ? getApiErrorMessage(authError) : authError); + setAuthErrorMessage( + isWebErrorCode(authError) ? getApiErrorMessage(authError) : authError, + ); void navigate({ search: {}, replace: true }); }, [authError, navigate]); @@ -113,6 +115,12 @@ export const LoginMainPage = () => {

{m.login_main_title()}

{m.login_main_subtitle()}

+ {isPresent(authErrorMessage) && ( + <> + + + + )} {tooManyAttempts && ( <> Date: Fri, 24 Jul 2026 12:33:42 +0200 Subject: [PATCH 4/4] add comment --- web/src/pages/auth/LoginMain/LoginMainPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/pages/auth/LoginMain/LoginMainPage.tsx b/web/src/pages/auth/LoginMain/LoginMainPage.tsx index fe615e67e9..f10402f5e5 100644 --- a/web/src/pages/auth/LoginMain/LoginMainPage.tsx +++ b/web/src/pages/auth/LoginMain/LoginMainPage.tsx @@ -43,6 +43,8 @@ export const LoginMainPage = () => { const navigate = useNavigate({ from: '/auth/login' }); const [authErrorMessage, setAuthErrorMessage] = useState(null); + // authError arrives via URL search param (redirect from external IdP after OpenID/Entra failure); + // consume it into local state then strip from URL so refresh/back-nav doesn't re-show stale error. useEffect(() => { if (!isPresent(authError)) return; setAuthErrorMessage(