mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-01-27 01:32:32 +00:00
feat: adapt frontend to oidc flow
This commit is contained in:
@@ -159,6 +159,10 @@ code {
|
||||
@apply relative rounded bg-muted px-[0.2rem] py-[0.1rem] font-mono text-sm font-semibold break-all;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-accent border border-border rounded-md p-2;
|
||||
}
|
||||
|
||||
.lead {
|
||||
@apply text-xl text-muted-foreground;
|
||||
}
|
||||
|
||||
53
frontend/src/lib/hooks/oidc.ts
Normal file
53
frontend/src/lib/hooks/oidc.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type OIDCValues = {
|
||||
scope: string;
|
||||
response_type: string;
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
interface IuseOIDCParams {
|
||||
values: OIDCValues;
|
||||
compiled: string;
|
||||
isOidc: boolean;
|
||||
missingParams: string[];
|
||||
}
|
||||
|
||||
const optionalParams: string[] = ["state"];
|
||||
|
||||
export function useOIDCParams(params: URLSearchParams): IuseOIDCParams {
|
||||
let compiled: string = "";
|
||||
let isOidc = false;
|
||||
const missingParams: string[] = [];
|
||||
|
||||
const values: OIDCValues = {
|
||||
scope: params.get("scope") ?? "",
|
||||
response_type: params.get("response_type") ?? "",
|
||||
client_id: params.get("client_id") ?? "",
|
||||
redirect_uri: params.get("redirect_uri") ?? "",
|
||||
state: params.get("state") ?? "",
|
||||
};
|
||||
|
||||
for (const key of Object.keys(values)) {
|
||||
if (!values[key as keyof OIDCValues]) {
|
||||
if (!optionalParams.includes(key)) {
|
||||
missingParams.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingParams.length === 0) {
|
||||
isOidc = true;
|
||||
}
|
||||
|
||||
if (isOidc) {
|
||||
compiled = new URLSearchParams(values).toString();
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
compiled,
|
||||
isOidc,
|
||||
missingParams,
|
||||
};
|
||||
}
|
||||
@@ -13,16 +13,7 @@ import { getOidcClientInfoScehma } from "@/schemas/oidc-schemas";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type AuthorizePageProps = {
|
||||
scope: string;
|
||||
responseType: string;
|
||||
clientId: string;
|
||||
redirectUri: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
const optionalAuthorizeProps = ["state"];
|
||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
|
||||
export const AuthorizePage = () => {
|
||||
const { isLoggedIn } = useUserContext();
|
||||
@@ -30,20 +21,16 @@ export const AuthorizePage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
|
||||
// If there is a better way to do this, please do let me know
|
||||
const props: AuthorizePageProps = {
|
||||
scope: searchParams.get("scope") || "",
|
||||
responseType: searchParams.get("response_type") || "",
|
||||
clientId: searchParams.get("client_id") || "",
|
||||
redirectUri: searchParams.get("redirect_uri") || "",
|
||||
state: searchParams.get("state") || "",
|
||||
};
|
||||
const {
|
||||
values: props,
|
||||
missingParams,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
|
||||
const getClientInfo = useQuery({
|
||||
queryKey: ["client", props.clientId],
|
||||
queryKey: ["client", props.client_id],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/oidc/clients/${props.clientId}`);
|
||||
const res = await fetch(`/api/oidc/clients/${props.client_id}`);
|
||||
const data = await getOidcClientInfoScehma.parseAsync(await res.json());
|
||||
return data;
|
||||
},
|
||||
@@ -53,13 +40,13 @@ export const AuthorizePage = () => {
|
||||
mutationFn: () => {
|
||||
return axios.post("/api/oidc/authorize", {
|
||||
scope: props.scope,
|
||||
response_type: props.responseType,
|
||||
client_id: props.clientId,
|
||||
redirect_uri: props.redirectUri,
|
||||
response_type: props.response_type,
|
||||
client_id: props.client_id,
|
||||
redirect_uri: props.redirect_uri,
|
||||
state: props.state,
|
||||
});
|
||||
},
|
||||
mutationKey: ["authorize", props.clientId],
|
||||
mutationKey: ["authorize", props.client_id],
|
||||
onSuccess: (data) => {
|
||||
toast.info("Authorized", {
|
||||
description: "You will be soon redirected to your application",
|
||||
@@ -74,19 +61,17 @@ export const AuthorizePage = () => {
|
||||
});
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// TODO: Pass the params to the login page, so user can login -> authorize
|
||||
return <Navigate to="/login" replace />;
|
||||
return <Navigate to={`/login?${compiledOIDCParams}`} replace />;
|
||||
}
|
||||
|
||||
Object.keys(props).forEach((key) => {
|
||||
if (
|
||||
!props[key as keyof AuthorizePageProps] &&
|
||||
!optionalAuthorizeProps.includes(key)
|
||||
) {
|
||||
// TODO: Add reason for error
|
||||
return <Navigate to="/error" replace />;
|
||||
}
|
||||
});
|
||||
if (missingParams.length > 0) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/error?error=${encodeURIComponent(`Missing parameters: ${missingParams.join(", ")}`)}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (getClientInfo.isLoading) {
|
||||
return (
|
||||
@@ -102,8 +87,12 @@ export const AuthorizePage = () => {
|
||||
}
|
||||
|
||||
if (getClientInfo.isError) {
|
||||
// TODO: Add reason for error
|
||||
return <Navigate to="/error" replace />;
|
||||
return (
|
||||
<Navigate
|
||||
to={`/error?error=${encodeURIComponent(`Failed to load client information`)}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ContinuePage = () => {
|
||||
clearTimeout(auto);
|
||||
clearTimeout(reveal);
|
||||
};
|
||||
}, []);
|
||||
});
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
|
||||
@@ -5,15 +5,30 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
export const ErrorPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { search } = useLocation();
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const error = searchParams.get("error") ?? "";
|
||||
|
||||
return (
|
||||
<Card className="min-w-xs sm:min-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
|
||||
<CardDescription>{t("errorSubtitle")}</CardDescription>
|
||||
<CardDescription className="flex flex-col gap-1.5">
|
||||
{error ? (
|
||||
<>
|
||||
<p>The following error occured while processing your request:</p>
|
||||
<pre>{error}</pre>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>{t("errorSubtitle")}</p>
|
||||
</>
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { OAuthButton } from "@/components/ui/oauth-button";
|
||||
import { SeperatorWithChildren } from "@/components/ui/separator";
|
||||
import { useAppContext } from "@/context/app-context";
|
||||
import { useUserContext } from "@/context/user-context";
|
||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
import { LoginSchema } from "@/schemas/login-schema";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios, { AxiosError } from "axios";
|
||||
@@ -47,7 +48,11 @@ export const LoginPage = () => {
|
||||
const redirectButtonTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
const {
|
||||
values: props,
|
||||
isOidc,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
|
||||
const oauthProviders = providers.filter(
|
||||
(provider) => provider.id !== "local" && provider.id !== "ldap",
|
||||
@@ -60,7 +65,7 @@ export const LoginPage = () => {
|
||||
const oauthMutation = useMutation({
|
||||
mutationFn: (provider: string) =>
|
||||
axios.get(
|
||||
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
`/api/oauth/url/${provider}?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
|
||||
),
|
||||
mutationKey: ["oauth"],
|
||||
onSuccess: (data) => {
|
||||
@@ -85,9 +90,7 @@ export const LoginPage = () => {
|
||||
mutationKey: ["login"],
|
||||
onSuccess: (data) => {
|
||||
if (data.data.totpPending) {
|
||||
window.location.replace(
|
||||
`/totp?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
);
|
||||
window.location.replace(`/totp?${compiledOIDCParams}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,8 +99,12 @@ export const LoginPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
if (isOidc) {
|
||||
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||
return;
|
||||
}
|
||||
window.location.replace(
|
||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
|
||||
);
|
||||
}, 500);
|
||||
},
|
||||
@@ -115,7 +122,7 @@ export const LoginPage = () => {
|
||||
if (
|
||||
providers.find((provider) => provider.id === oauthAutoRedirect) &&
|
||||
!isLoggedIn &&
|
||||
redirectUri
|
||||
props.redirect_uri !== ""
|
||||
) {
|
||||
// Not sure of a better way to do this
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
@@ -125,7 +132,13 @@ export const LoginPage = () => {
|
||||
setShowRedirectButton(true);
|
||||
}, 5000);
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
providers,
|
||||
isLoggedIn,
|
||||
props.redirect_uri,
|
||||
oauthAutoRedirect,
|
||||
oauthMutation,
|
||||
]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -136,10 +149,10 @@ export const LoginPage = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
if (isLoggedIn && redirectUri) {
|
||||
if (isLoggedIn && props.redirect_uri !== "") {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/continue?redirect_uri=${encodeURIComponent(redirectUri)}`}
|
||||
to={`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ export const LogoutPage = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
|
||||
<CardDescription>
|
||||
{provider !== "username" ? (
|
||||
{provider !== "local" && provider !== "ldap" ? (
|
||||
<Trans
|
||||
i18nKey="logoutOauthSubtitle"
|
||||
t={t}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useEffect, useId, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Navigate, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useOIDCParams } from "@/lib/hooks/oidc";
|
||||
|
||||
export const TotpPage = () => {
|
||||
const { totpPending } = useUserContext();
|
||||
@@ -26,7 +27,11 @@ export const TotpPage = () => {
|
||||
const redirectTimer = useRef<number | null>(null);
|
||||
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const redirectUri = searchParams.get("redirect_uri");
|
||||
const {
|
||||
values: props,
|
||||
isOidc,
|
||||
compiled: compiledOIDCParams,
|
||||
} = useOIDCParams(searchParams);
|
||||
|
||||
const totpMutation = useMutation({
|
||||
mutationFn: (values: TotpSchema) => axios.post("/api/user/totp", values),
|
||||
@@ -37,9 +42,14 @@ export const TotpPage = () => {
|
||||
});
|
||||
|
||||
redirectTimer.current = window.setTimeout(() => {
|
||||
window.location.replace(
|
||||
`/continue?redirect_uri=${encodeURIComponent(redirectUri ?? "")}`,
|
||||
);
|
||||
if (isOidc) {
|
||||
window.location.replace(`/authorize?${compiledOIDCParams}`);
|
||||
return;
|
||||
} else {
|
||||
window.location.replace(
|
||||
`/continue?redirect_uri=${encodeURIComponent(props.redirect_uri)}`,
|
||||
);
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
Reference in New Issue
Block a user