diff --git a/apps/client-ts/src/components/Auth/DashboardClient.tsx b/apps/client-ts/src/components/Auth/DashboardClient.tsx index 21cbd8c83..bc73eb8ea 100644 --- a/apps/client-ts/src/components/Auth/DashboardClient.tsx +++ b/apps/client-ts/src/components/Auth/DashboardClient.tsx @@ -38,6 +38,10 @@ import { import { Separator } from "@/components/ui/separator" import { CircleIcon } from "@radix-ui/react-icons" +import { useCreateSamlSso } from "@/hooks/stytch/useCreateSamlSso"; +import { useCreateOidcSso } from "@/hooks/stytch/useCreateOidcSso"; +import { useInvite } from "@/hooks/stytch/useInvite"; +import { useDeleteMember } from "@/hooks/stytch/useDeleteMember"; type Props = { @@ -65,10 +69,13 @@ const MemberRow = ({ member, user }: { member: Member; user: Member; }) => { const router = useRouter(); const pathname = usePathname(); const [isDisabled, setIsDisabled] = useState(false); + + const { mutate, isLoading, error, data } = useDeleteMember(); + const doDelete: MouseEventHandler = (e) => { e.preventDefault(); setIsDisabled(true); - //TODO: await deleteMember(member.member_id); + mutate(member.member_id); // Force a reload to refresh the user list router.replace(pathname); // TODO: Success toast? @@ -121,6 +128,9 @@ const MemberList = ({ setIsDisabled(!isValidEmail(email)); }, [email]); + const { mutate, isLoading, error, data } = useInvite(); + + const onInviteSubmit: FormEventHandler = (e) => { e.preventDefault(); // Disable button right away to prevent sending emails twice @@ -129,7 +139,7 @@ const MemberList = ({ } else { setIsDisabled(true); } - //TODO: await invite(email); + mutate(email); // Force a reload to refresh the user list router.replace(pathname); }; @@ -176,30 +186,34 @@ const IDPList = ({ const router = useRouter(); const searchParams = useSearchParams(); + const { mutate: samlMutate, isLoading: samlLoading, error: samlError, data: samlData } = useCreateSamlSso(); + const { mutate: oidcMutate, isLoading: oidcLoading, error: oidcError, data: oidcData } = useCreateOidcSso(); + + const onSamlCreate: FormEventHandler = (e) => { e.preventDefault(); - /*TODO const res = await createSamlSSOConn(idpNameSAML); - if (res.status !== 200) { + samlMutate(idpNameSAML); + if (samlError) { alert("Error creating connection"); return; } - const conn = await res.json(); - await router.push( + const conn = samlData as any; + router.push( `/${searchParams.get('slug')}/dashboard/saml/${conn.connection_id}` - );*/ + ); }; const onOidcCreate: FormEventHandler = (e) => { e.preventDefault(); - /*const res = await createOidcSSOConn(idpNameOIDC); - if (res.status !== 200) { + oidcMutate(idpNameOIDC); + if (oidcError) { alert("Error creating connection"); return; } - const conn = await res.json(); - await router.push( + const conn = oidcData as any; + router.push( `/${searchParams.get('slug')}/dashboard/oidc/${conn.connection_id}` - );*/ + ); }; return ( diff --git a/apps/client-ts/src/components/Nav/user-nav.tsx b/apps/client-ts/src/components/Nav/user-nav.tsx index f06e240a6..7b906fc40 100644 --- a/apps/client-ts/src/components/Nav/user-nav.tsx +++ b/apps/client-ts/src/components/Nav/user-nav.tsx @@ -20,7 +20,7 @@ import Link from "next/link"; import { useEffect } from "react"; export function UserNav() { - const {data, isLoading} = useProfile(); + /*const {data, isLoading} = useProfile(); if(!data) { console.log("loading profiles"); } @@ -36,7 +36,7 @@ export function UserNav() { id_organization: data[0].id_organization as string, }) } - }, [data, setProfile]); + }, [data, setProfile]);*/ return ( @@ -49,7 +49,7 @@ export function UserNav() { - + {/*

{profile ? profile.first_name : isLoading ? : "No profiles found"} @@ -59,20 +59,19 @@ export function UserNav() { {profile ? profile.email : isLoading ? : "No mail found"}

-
- +
*/} - + Profile - + {/* Billing Settings - + */} diff --git a/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx b/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx new file mode 100644 index 000000000..536f05301 --- /dev/null +++ b/apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx @@ -0,0 +1,29 @@ +import { createOidcSSOConn, createSamlSSOConn } from '@/lib/stytch/api'; +import { useCallback, useState } from 'react'; + +export const useCreateOidcSso = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [data, setData] = useState(null); + + const mutate = useCallback(async (display_name: string) => { + setIsLoading(true); + setError(null); + try { + const response = await createOidcSSOConn(display_name) + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + setData(data); + } catch (error) { + setError(error instanceof Error ? error.message : String(error)); + } finally { + setIsLoading(false); + } + }, []); + + return { mutate, isLoading, error, data }; +}; diff --git a/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx b/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx new file mode 100644 index 000000000..6ef006c05 --- /dev/null +++ b/apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx @@ -0,0 +1,29 @@ +import { createSamlSSOConn } from '@/lib/stytch/api'; +import { useCallback, useState } from 'react'; + +export const useCreateSamlSso = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [data, setData] = useState(null); + + const mutate = useCallback(async (display_name: string) => { + setIsLoading(true); + setError(null); + try { + const response = await createSamlSSOConn(display_name) + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + setData(data); + } catch (error) { + setError(error instanceof Error ? error.message : String(error)); + } finally { + setIsLoading(false); + } + }, []); + + return { mutate, isLoading, error, data }; +}; diff --git a/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx b/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx new file mode 100644 index 000000000..85f45afdd --- /dev/null +++ b/apps/client-ts/src/hooks/stytch/useDeleteMember.tsx @@ -0,0 +1,29 @@ +import { deleteMember, invite } from '@/lib/stytch/api'; +import { useCallback, useState } from 'react'; + +export const useDeleteMember = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [data, setData] = useState(null); + + const mutate = useCallback(async (member_id: string) => { + setIsLoading(true); + setError(null); + try { + const response = await deleteMember(member_id) + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + setData(data); + } catch (error) { + setError(error instanceof Error ? error.message : String(error)); + } finally { + setIsLoading(false); + } + }, []); + + return { mutate, isLoading, error, data }; +}; diff --git a/apps/client-ts/src/hooks/stytch/useInvite.tsx b/apps/client-ts/src/hooks/stytch/useInvite.tsx new file mode 100644 index 000000000..67973b71f --- /dev/null +++ b/apps/client-ts/src/hooks/stytch/useInvite.tsx @@ -0,0 +1,29 @@ +import { invite } from '@/lib/stytch/api'; +import { useCallback, useState } from 'react'; + +export const useInvite = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [data, setData] = useState(null); + + const mutate = useCallback(async (email: string) => { + setIsLoading(true); + setError(null); + try { + const response = await invite(email) + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + setData(data); + } catch (error) { + setError(error instanceof Error ? error.message : String(error)); + } finally { + setIsLoading(false); + } + }, []); + + return { mutate, isLoading, error, data }; +}; diff --git a/apps/client-ts/src/middleware.ts b/apps/client-ts/src/middleware.ts index 8afc61c68..b7088fc75 100644 --- a/apps/client-ts/src/middleware.ts +++ b/apps/client-ts/src/middleware.ts @@ -167,7 +167,7 @@ export async function middleware(request: NextRequest) { const sessionJWT = request.cookies.get("session")?.value; if (!sessionJWT) { - return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + return NextResponse.redirect(new URL("/auth/login", request.url)); } try { @@ -179,10 +179,16 @@ export async function middleware(request: NextRequest) { }); } catch (err) { console.error("Could not find member by session token", err); - return new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }); + return NextResponse.redirect(new URL("/auth/login", request.url)); } console.log(sessionAuthRes); - const response = NextResponse.next(); + + let response; + if(request.nextUrl.pathname == '/profile'){ + response = NextResponse.redirect(new URL(`/auth/${sessionAuthRes.organization.organization_slug}/dashboard`, request.url)); + }else{ + response = NextResponse.next(); + } // Stytch issues a new JWT on every authenticate call - store it on the UA for faster validation next time setSession(response, sessionAuthRes.session_jwt); @@ -190,6 +196,7 @@ export async function middleware(request: NextRequest) { if (!isAdmin) { return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 }); } + response.headers.set('x-member-org', sessionAuthRes.member.organization_id); return response; @@ -201,6 +208,11 @@ export async function middleware(request: NextRequest) { export const config = { matcher: [ '/', + '/profile', + '/api-keys', + '/connections', + '/configuration', + '/events', '/auth/[slug]/dashboard/:path*', '/api/callback', '/api/discovery/:path*',