Skip to content

Commit 71bc396

Browse files
committed
feat: adapt frontend to oidc flow
1 parent c817e35 commit 71bc396

8 files changed

Lines changed: 139 additions & 55 deletions

File tree

frontend/src/index.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ code {
159159
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
160160
}
161161

162+
pre {
163+
@apply bg-accent border border-border rounded-md p-2;
164+
}
165+
162166
.lead {
163167
@apply text-xl text-muted-foreground;
164168
}

frontend/src/lib/hooks/oidc.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
export type OIDCValues = {
2+
scope: string;
3+
response_type: string;
4+
client_id: string;
5+
redirect_uri: string;
6+
state: string;
7+
};
8+
9+
interface IuseOIDCParams {
10+
values: OIDCValues;
11+
compiled: string;
12+
isOidc: boolean;
13+
missingParams: string[];
14+
}
15+
16+
const optionalParams: string[] = ["state"];
17+
18+
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
19+
let compiled: string = "";
20+
let isOidc = false;
21+
const missingParams: string[] = [];
22+
23+
const values: OIDCValues = {
24+
scope: params.get("scope") ?? "",
25+
response_type: params.get("response_type") ?? "",
26+
client_id: params.get("client_id") ?? "",
27+
redirect_uri: params.get("redirect_uri") ?? "",
28+
state: params.get("state") ?? "",
29+
};
30+
31+
for (const key of Object.keys(values)) {
32+
if (!values[key as keyof OIDCValues]) {
33+
if (!optionalParams.includes(key)) {
34+
missingParams.push(key);
35+
}
36+
}
37+
}
38+
39+
if (missingParams.length === 0) {
40+
isOidc = true;
41+
}
42+
43+
if (isOidc) {
44+
compiled = new URLSearchParams(values).toString();
45+
}
46+
47+
return {
48+
values,
49+
compiled,
50+
isOidc,
51+
missingParams,
52+
};
53+
}

frontend/src/pages/authorize-page.tsx

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,24 @@ import { getOidcClientInfoScehma } from "@/schemas/oidc-schemas";
1313
import { Button } from "@/components/ui/button";
1414
import axios from "axios";
1515
import { toast } from "sonner";
16-
17-
type AuthorizePageProps = {
18-
scope: string;
19-
responseType: string;
20-
clientId: string;
21-
redirectUri: string;
22-
state: string;
23-
};
24-
25-
const optionalAuthorizeProps = ["state"];
16+
import { useOIDCParams } from "@/lib/hooks/oidc";
2617

2718
export const AuthorizePage = () => {
2819
const { isLoggedIn } = useUserContext();
2920
const { search } = useLocation();
3021
const navigate = useNavigate();
3122

3223
const searchParams = new URLSearchParams(search);
33-
34-
// If there is a better way to do this, please do let me know
35-
const props: AuthorizePageProps = {
36-
scope: searchParams.get("scope") || "",
37-
responseType: searchParams.get("response_type") || "",
38-
clientId: searchParams.get("client_id") || "",
39-
redirectUri: searchParams.get("redirect_uri") || "",
40-
state: searchParams.get("state") || "",
41-
};
24+
const {
25+
values: props,
26+
missingParams,
27+
compiled: compiledOIDCParams,
28+
} = useOIDCParams(searchParams);
4229

4330
const getClientInfo = useQuery({
44-
queryKey: ["client", props.clientId],
31+
queryKey: ["client", props.client_id],
4532
queryFn: async () => {
46-
const res = await fetch(`/api/oidc/clients/${props.clientId}`);
33+
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
4734
const data = await getOidcClientInfoScehma.parseAsync(await res.json());
4835
return data;
4936
},
@@ -53,13 +40,13 @@ export const AuthorizePage = () => {
5340
mutationFn: () => {
5441
return axios.post("/api/oidc/authorize", {
5542
scope: props.scope,
56-
response_type: props.responseType,
57-
client_id: props.clientId,
58-
redirect_uri: props.redirectUri,
43+
response_type: props.response_type,
44+
client_id: props.client_id,
45+
redirect_uri: props.redirect_uri,
5946
state: props.state,
6047
});
6148
},
62-
mutationKey: ["authorize", props.clientId],
49+
mutationKey: ["authorize", props.client_id],
6350
onSuccess: (data) => {
6451
toast.info("Authorized", {
6552
description: "You will be soon redirected to your application",
@@ -74,19 +61,17 @@ export const AuthorizePage = () => {
7461
});
7562

7663
if (!isLoggedIn) {
77-
// TODO: Pass the params to the login page, so user can login -> authorize
78-
return <Navigate to="/login" replace />;
64+
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
7965
}
8066

81-
Object.keys(props).forEach((key) => {
82-
if (
83-
!props[key as keyof AuthorizePageProps] &&
84-
!optionalAuthorizeProps.includes(key)
85-
) {
86-
// TODO: Add reason for error
87-
return <Navigate to="/error" replace />;
88-
}
89-
});
67+
if (missingParams.length > 0) {
68+
return (
69+
<Navigate
70+
to={`/error?error=${encodeURIComponent(`Missing parameters: ${missingParams.join(", ")}`)}`}
71+
replace
72+
/>
73+
);
74+
}
9075

9176
if (getClientInfo.isLoading) {
9277
return (
@@ -102,8 +87,12 @@ export const AuthorizePage = () => {
10287
}
10388

10489
if (getClientInfo.isError) {
105-
// TODO: Add reason for error
106-
return <Navigate to="/error" replace />;
90+
return (
91+
<Navigate
92+
to={`/error?error=${encodeURIComponent(`Failed to load client information`)}`}
93+
replace
94+
/>
95+
);
10796
}
10897

10998
return (

frontend/src/pages/continue-page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const ContinuePage = () => {
8080
clearTimeout(auto);
8181
clearTimeout(reveal);
8282
};
83-
}, []);
83+
});
8484

8585
if (!isLoggedIn) {
8686
return (

frontend/src/pages/error-page.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@ import {
55
CardTitle,
66
} from "@/components/ui/card";
77
import { useTranslation } from "react-i18next";
8+
import { useLocation } from "react-router";
89

910
export const ErrorPage = () => {
1011
const { t } = useTranslation();
12+
const { search } = useLocation();
13+
const searchParams = new URLSearchParams(search);
14+
const error = searchParams.get("error") ?? "";
1115

1216
return (
1317
<Card className="min-w-xs sm:min-w-sm">
1418
<CardHeader>
1519
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
16-
<CardDescription>{t("errorSubtitle")}</CardDescription>
20+
<CardDescription className="flex flex-col gap-1.5">
21+
{error ? (
22+
<>
23+
<p>The following error occured while processing your request:</p>
24+
<pre>{error}</pre>
25+
</>
26+
) : (
27+
<>
28+
<p>{t("errorSubtitle")}</p>
29+
</>
30+
)}
31+
</CardDescription>
1732
</CardHeader>
1833
</Card>
1934
);

frontend/src/pages/login-page.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { OAuthButton } from "@/components/ui/oauth-button";
1818
import { SeperatorWithChildren } from "@/components/ui/separator";
1919
import { useAppContext } from "@/context/app-context";
2020
import { useUserContext } from "@/context/user-context";
21+
import { useOIDCParams } from "@/lib/hooks/oidc";
2122
import { LoginSchema } from "@/schemas/login-schema";
2223
import { useMutation } from "@tanstack/react-query";
2324
import axios, { AxiosError } from "axios";
@@ -47,7 +48,11 @@ export const LoginPage = () => {
4748
const redirectButtonTimer = useRef<number | null>(null);
4849

4950
const searchParams = new URLSearchParams(search);
50-
const redirectUri = searchParams.get("redirect_uri");
51+
const {
52+
values: props,
53+
isOidc,
54+
compiled: compiledOIDCParams,
55+
} = useOIDCParams(searchParams);
5156

5257
const oauthProviders = providers.filter(
5358
(provider) => provider.id !== "local" && provider.id !== "ldap",
@@ -60,7 +65,7 @@ export const LoginPage = () => {
6065
const oauthMutation = useMutation({
6166
mutationFn: (provider: string) =>
6267
axios.get(
63-
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
68+
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
6469
),
6570
mutationKey: ["oauth"],
6671
onSuccess: (data) => {
@@ -85,9 +90,7 @@ export const LoginPage = () => {
8590
mutationKey: ["login"],
8691
onSuccess: (data) => {
8792
if (data.data.totpPending) {
88-
window.location.replace(
89-
`/totp?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
90-
);
93+
window.location.replace(`/totp?${compiledOIDCParams}`);
9194
return;
9295
}
9396

@@ -96,8 +99,12 @@ export const LoginPage = () => {
9699
});
97100

98101
redirectTimer.current = window.setTimeout(() => {
102+
if (isOidc) {
103+
window.location.replace(`/authorize?${compiledOIDCParams}`);
104+
return;
105+
}
99106
window.location.replace(
100-
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
107+
`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
101108
);
102109
}, 500);
103110
},
@@ -115,7 +122,7 @@ export const LoginPage = () => {
115122
if (
116123
providers.find((provider) => provider.id === oauthAutoRedirect) &&
117124
!isLoggedIn &&
118-
redirectUri
125+
props.redirect_uri !== ""
119126
) {
120127
// Not sure of a better way to do this
121128
// eslint-disable-next-line react-hooks/set-state-in-effect
@@ -125,7 +132,13 @@ export const LoginPage = () => {
125132
setShowRedirectButton(true);
126133
}, 5000);
127134
}
128-
}, []);
135+
}, [
136+
providers,
137+
isLoggedIn,
138+
props.redirect_uri,
139+
oauthAutoRedirect,
140+
oauthMutation,
141+
]);
129142

130143
useEffect(
131144
() => () => {
@@ -136,10 +149,10 @@ export const LoginPage = () => {
136149
[],
137150
);
138151

139-
if (isLoggedIn && redirectUri) {
152+
if (isLoggedIn && props.redirect_uri !== "") {
140153
return (
141154
<Navigate
142-
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
155+
to={`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`}
143156
replace
144157
/>
145158
);

frontend/src/pages/logout-page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const LogoutPage = () => {
5555
<CardHeader>
5656
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
5757
<CardDescription>
58-
{provider !== "username" ? (
58+
{provider !== "local" && provider !== "ldap" ? (
5959
<Trans
6060
i18nKey="logoutOauthSubtitle"
6161
t={t}

frontend/src/pages/totp-page.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useEffect, useId, useRef } from "react";
1616
import { useTranslation } from "react-i18next";
1717
import { Navigate, useLocation } from "react-router";
1818
import { toast } from "sonner";
19+
import { useOIDCParams } from "@/lib/hooks/oidc";
1920

2021
export const TotpPage = () => {
2122
const { totpPending } = useUserContext();
@@ -26,7 +27,11 @@ export const TotpPage = () => {
2627
const redirectTimer = useRef<number | null>(null);
2728

2829
const searchParams = new URLSearchParams(search);
29-
const redirectUri = searchParams.get("redirect_uri");
30+
const {
31+
values: props,
32+
isOidc,
33+
compiled: compiledOIDCParams,
34+
} = useOIDCParams(searchParams);
3035

3136
const totpMutation = useMutation({
3237
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
@@ -37,9 +42,14 @@ export const TotpPage = () => {
3742
});
3843

3944
redirectTimer.current = window.setTimeout(() => {
40-
window.location.replace(
41-
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
42-
);
45+
if (isOidc) {
46+
window.location.replace(`/authorize?${compiledOIDCParams}`);
47+
return;
48+
} else {
49+
window.location.replace(
50+
`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
51+
);
52+
}
4353
}, 500);
4454
},
4555
onError: () => {

0 commit comments

Comments
 (0)