Skip to content

Commit

Permalink
✨ Fix stytch protected routes
Browse files Browse the repository at this point in the history
  • Loading branch information
naelob committed Mar 14, 2024
1 parent edbf7b5 commit 21efc2c
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 23 deletions.
38 changes: 26 additions & 12 deletions apps/client-ts/src/components/Auth/DashboardClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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);
};
Expand Down Expand Up @@ -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 (
Expand Down
15 changes: 7 additions & 8 deletions apps/client-ts/src/components/Nav/user-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -36,7 +36,7 @@ export function UserNav() {
id_organization: data[0].id_organization as string,
})
}
}, [data, setProfile]);
}, [data, setProfile]);*/

return (
<DropdownMenu>
Expand All @@ -49,7 +49,7 @@ export function UserNav() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 ml-10" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
{/*<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{profile ? profile.first_name : isLoading ? <Skeleton className="w-[100px] h-[20px] rounded-md" /> : "No profiles found"}
Expand All @@ -59,20 +59,19 @@ export function UserNav() {
{profile ? profile.email : isLoading ? <Skeleton className="w-[100px] h-[20px] rounded-md" /> : "No mail found"}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
</DropdownMenuLabel>*/}
<DropdownMenuGroup>
<Link href={"/api/logout"}>
<Link href={"/profile"}>
<DropdownMenuItem>
Profile
</DropdownMenuItem>
</Link>
<DropdownMenuItem>
{/*<DropdownMenuItem>
Billing
</DropdownMenuItem>
<DropdownMenuItem>
Settings
</DropdownMenuItem>
</DropdownMenuItem>*/}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Expand Down
29 changes: 29 additions & 0 deletions apps/client-ts/src/hooks/stytch/useCreateOidcSso.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 };
};
29 changes: 29 additions & 0 deletions apps/client-ts/src/hooks/stytch/useCreateSamlSso.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 };
};
29 changes: 29 additions & 0 deletions apps/client-ts/src/hooks/stytch/useDeleteMember.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 };
};
29 changes: 29 additions & 0 deletions apps/client-ts/src/hooks/stytch/useInvite.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 };
};
18 changes: 15 additions & 3 deletions apps/client-ts/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -179,17 +179,24 @@ 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);

const isAdmin = sessionAuthRes.member.trusted_metadata!.admin as boolean;
if (!isAdmin) {
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
}

response.headers.set('x-member-org', sessionAuthRes.member.organization_id);

return response;
Expand All @@ -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*',
Expand Down

0 comments on commit 21efc2c

Please sign in to comment.