Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auth rework #1581

Merged
merged 2 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ bun.lockb
/playwright/.cache/
/playwright/.auth/
/playwright/results

certificates
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"prepare": "husky",
"dev": "next dev --turbo",
"dev": "next dev --turbo --experimental-https",
"build": "next build",
"start": "next start",
"type-check": "tsc --pretty --noEmit --incremental false",
Expand Down Expand Up @@ -41,7 +41,6 @@
"event-source-plus": "^0.1.8",
"foxact": "^0.2.43",
"jotai": "^2.10.3",
"jwt-decode": "^4.0.0",
"mini-svg-data-uri": "^1.4.4",
"next": "15.0.3",
"next-themes": "^0.4.4",
Expand Down
52 changes: 0 additions & 52 deletions src/actions/auth.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/app/(dashboard)/[guildUrlName]/components/JoinButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/Tooltip";
import { GUILD_AUTH_COOKIE_NAME } from "@/config/constants";
import { env } from "@/lib/env";
import { fetchGuildLeave } from "@/lib/fetchers";
import { getCookieClientSide } from "@/lib/getCookieClientSide";
import { guildOptions, userOptions } from "@/lib/options";
import type { Schemas } from "@guildxyz/types";
import { CheckCircle, SignOut } from "@phosphor-icons/react/dist/ssr";
Expand All @@ -34,8 +32,6 @@ export const JoinButton = () => {

const joinMutation = useMutation({
mutationFn: async () => {
//TODO: Handle error here, throw error in funciton if needed
const token = getCookieClientSide(GUILD_AUTH_COOKIE_NAME)!;
const url = new URL(
`api/guild/${guild.data.id}/join`,
env.NEXT_PUBLIC_API,
Expand All @@ -45,7 +41,6 @@ export const JoinButton = () => {
method: "post",
maxRetryCount: 0,
headers: {
"x-auth-token": token,
"content-type": "application/json",
},
});
Expand Down
5 changes: 0 additions & 5 deletions src/app/(dashboard)/[guildUrlName]/components/JoinGuild.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use client";

import { Button } from "@/components/ui/Button";
import { GUILD_AUTH_COOKIE_NAME } from "@/config/constants";
import { env } from "@/lib/env";
import { getCookieClientSide } from "@/lib/getCookieClientSide";
import { guildOptions, userOptions } from "@/lib/options";
import type { Schemas } from "@guildxyz/types";
import { CheckCircle } from "@phosphor-icons/react/dist/ssr";
Expand All @@ -23,8 +21,6 @@ export const JoinGuild = () => {

const { mutate, isPending } = useMutation({
mutationFn: async () => {
//TODO: Handle error here, throw error in funciton if needed
const token = getCookieClientSide(GUILD_AUTH_COOKIE_NAME)!;
const url = new URL(
`api/guild/${guild.data.id}/join`,
env.NEXT_PUBLIC_API,
Expand All @@ -34,7 +30,6 @@ export const JoinGuild = () => {
method: "post",
maxRetryCount: 0,
headers: {
"x-auth-token": token,
"content-type": "application/json",
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import { useConfetti } from "@/components/ConfettiProvider";
import { Button } from "@/components/ui/Button";
import { GUILD_AUTH_COOKIE_NAME } from "@/config/constants";
import { fetchGuildApiData } from "@/lib/fetchGuildApi";
import { getCookieClientSide } from "@/lib/getCookieClientSide";
import type { CreateGuildForm, Guild } from "@/lib/schemas/guild";
import { CheckCircle, XCircle } from "@phosphor-icons/react/dist/ssr";
import { useMutation } from "@tanstack/react-query";
Expand All @@ -21,10 +19,6 @@ const CreateGuildButton = () => {

const { mutate: onSubmit, isPending } = useMutation({
mutationFn: async (data: CreateGuildForm) => {
const token = getCookieClientSide(GUILD_AUTH_COOKIE_NAME);

if (!token) throw new Error("Unauthorized"); // TODO: custom errors?

const guild = {
...data,
contact: undefined,
Expand Down
7 changes: 4 additions & 3 deletions src/app/(dashboard)/explorer/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { fetchGuildApiData } from "@/lib/fetchGuildApi";
import { userOptions } from "@/lib/options";
import type { Leaderboard } from "@/lib/schemas/leaderboard";
import { tryGetParsedToken } from "@/lib/token";
import type { PaginatedResponse } from "@/lib/types";
import type { Schemas } from "@guildxyz/types";
import { useQuery } from "@tanstack/react-query";
import { PAGE_SIZE } from "./constants";

export const fetchAssociatedGuilds = async () => {
const { userId } = await tryGetParsedToken();
const { data: user } = useQuery(userOptions());
return fetchGuildApiData<PaginatedResponse<Schemas["Guild"]>>(
`guild/search?page=1&pageSize=${Number.MAX_SAFE_INTEGER}&sortBy=name&reverse=false&customQuery=@owner:{${userId}}`,
`guild/search?page=1&pageSize=${Number.MAX_SAFE_INTEGER}&sortBy=name&reverse=false&customQuery=@owner:{${user?.id}}`,
);
};

Expand All @@ -23,7 +24,7 @@

export const fetchLeaderboard = async ({
rewardId,
offset = 0,

Check notice on line 27 in src/app/(dashboard)/explorer/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/correctness/noUnusedVariables

This variable is unused.
}: { rewardId: string; offset?: number }) => {
console.log("fetching leaderboard", `reward/${rewardId}/leaderboard`);
return fetchGuildApiData<
Expand Down
4 changes: 2 additions & 2 deletions src/app/(dashboard)/explorer/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getQueryClient } from "@/lib/getQueryClient";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import type { PropsWithChildren } from "react";
import { guildSearchOptions } from "./options";
import { associatedGuildsOption, guildSearchOptions } from "./options";

const ExplorerLayout = async ({ children }: PropsWithChildren) => {
const queryClient = getQueryClient();
await queryClient.prefetchInfiniteQuery(guildSearchOptions({}));
// await queryClient.prefetchQuery(associatedGuildsOption());
await queryClient.prefetchQuery(associatedGuildsOption());

return (
<HydrationBoundary state={dehydrate(queryClient)}>
Expand Down
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import "@/styles/globals.css";
import { PrefetchUserBoundary } from "@/components/PrefetchUserBoundary";
import { PreloadResources } from "@/components/PreloadResources";
import { Providers } from "@/components/Providers";
import { SignInDialog } from "@/components/SignInDialog";
Expand Down Expand Up @@ -27,7 +28,7 @@ const RootLayout = ({
<body className={cn(dystopian.variable)}>
<PreloadResources />
<Providers>
{children}
<PrefetchUserBoundary>{children}</PrefetchUserBoundary>

<SignInDialog />
<Toaster />
Expand Down
18 changes: 10 additions & 8 deletions src/components/AuthBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { tryGetToken } from "@/lib/token";
"use client";

export const AuthBoundary = async ({
import { userOptions } from "@/lib/options";
import { useQuery } from "@tanstack/react-query";

export const AuthBoundary = ({
fallback,
children,
}: Readonly<{
fallback: React.ReactNode;
children: React.ReactNode;
}>) => {
try {
await tryGetToken();
return <>{children}</>;
} catch {
return <>{fallback}</>;
}
const { data: user } = useQuery(userOptions());

if (user?.id) return children;

return fallback;
};
15 changes: 15 additions & 0 deletions src/components/PrefetchUserBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getQueryClient } from "@/lib/getQueryClient";
import { userOptions } from "@/lib/options";
import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
import type { PropsWithChildren } from "react";

export const PrefetchUserBoundary = async ({ children }: PropsWithChildren) => {
const queryClient = getQueryClient();
await queryClient.prefetchQuery(userOptions());

return (
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
);
};
24 changes: 21 additions & 3 deletions src/components/SignInDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { signIn } from "@/actions/auth";
import { signInDialogOpenAtom } from "@/config/atoms";
import { fetchGuildApi } from "@/lib/fetchGuildApi";
import { authSchema } from "@/lib/schemas/user";
import { SignIn, User, Wallet, XCircle } from "@phosphor-icons/react/dist/ssr";
import { DialogDescription } from "@radix-ui/react-dialog";
import { useMutation } from "@tanstack/react-query";
Expand All @@ -21,7 +21,6 @@ import {
ResponsiveDialogHeader,
ResponsiveDialogTitle,
} from "./ui/ResponsiveDialog";

const CUSTOM_CONNECTOR_ICONS = {
"com.brave.wallet": "/walletLogos/brave.svg",
walletConnect: "/walletLogos/walletconnect.svg",
Expand Down Expand Up @@ -155,7 +154,26 @@ const SignInWithEthereum = () => {

const signature = await signMessageAsync({ message });

return signIn({ message, signature });
const requestInit = {
method: "POST",
body: JSON.stringify({
message,
signature,
}),
} satisfies RequestInit;

const signInRes = await fetchGuildApi("auth/siwe/login", requestInit);
let json = signInRes.data;
if (signInRes.response.status === 401) {
const registerRes = await fetchGuildApi(
"auth/siwe/register",
requestInit,
);
json = registerRes.data;
}
const authData = authSchema.parse(json);

return authData;
},
onSuccess: () => setSignInDialogOpen(false),
onError: (error) => {
Expand Down
27 changes: 23 additions & 4 deletions src/components/SignOutButton.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
"use client";

import { signOut } from "@/actions/auth";
import { associatedGuildsOption } from "@/app/(dashboard)/explorer/options";
import { fetchGuildApi } from "@/lib/fetchGuildApi";
import { userOptions } from "@/lib/options";
import { SignOut } from "@phosphor-icons/react/dist/ssr";
import { usePathname } from "next/navigation";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "./ui/Button";

export const SignOutButton = () => {
const pathname = usePathname();
const queryClient = useQueryClient();

const { mutate: signOut, isPending } = useMutation({
mutationFn: () =>
fetchGuildApi("auth/logout", {
method: "POST",
}),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [userOptions().queryKey],
});
queryClient.invalidateQueries({
queryKey: associatedGuildsOption().queryKey,
});
},
});

return (
<Button
variant="ghost"
leftIcon={<SignOut weight="bold" />}
onClick={() => signOut(pathname)}
onClick={() => signOut()}
isLoading={isPending}
>
Sign out
</Button>
Expand Down
24 changes: 10 additions & 14 deletions src/lib/fetchGuildApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { signOut } from "@/actions/auth";
import { tryGetToken } from "@/lib/token";
import { isServer } from "@tanstack/react-query";
import { env } from "./env";
import type { ErrorLike } from "./types";

Expand Down Expand Up @@ -71,30 +70,27 @@ export const fetchGuildApi = async <Data = object, Error = ErrorLike>(
}
const url = new URL(`api/${pathname}`, env.NEXT_PUBLIC_API);

let token: string | undefined;
try {
token = await tryGetToken();
} catch (_) {}

const headers = new Headers(requestInit?.headers);
if (token) {
headers.set("X-Auth-Token", token);
}

if (requestInit?.body instanceof FormData) {
headers.set("Content-Type", "multipart/form-data");
} else if (requestInit?.body) {
headers.set("Content-Type", "application/json");
}

// Next.js won't include cookies automatically, so we include them manually on the server
if (isServer) {
const { cookies } = await import("next/headers");
const cookiesHeader = (await cookies()).toString();
headers.set("cookie", cookiesHeader);
}

const response = await fetch(url, {
...requestInit,
headers,
credentials: "include",
});

if (response.status === 401) {
signOut();
}

const contentType = response.headers.get("content-type");
if (!contentType?.includes("application/json")) {
throw new Error("Guild API failed to respond with json");
Expand Down
7 changes: 1 addition & 6 deletions src/lib/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { fetchGuildApiData } from "@/lib/fetchGuildApi";
import { resolveIdLikeRequest } from "@/lib/resolveIdLikeRequest";
import { tryGetParsedToken } from "@/lib/token";
import type {
Entity,
EntitySchema,
Expand All @@ -26,11 +25,7 @@
};

export const fetchUser = async () => {
const { userId } = await tryGetParsedToken();
return fetchEntity({
entity: "user",
idLike: userId,
});
return fetchGuildApiData<Schemas["User"]>("auth/me");
};

export const fetchGuildLeave = async ({ guildId }: { guildId: string }) => {
Expand All @@ -43,7 +38,7 @@
const guild = await fetchEntity({ entity: "guild", idLike: guildIdLike });
return fetchGuildApiData<Schemas["Page"][]>("page/batch", {
method: "POST",
body: JSON.stringify({ ids: guild.pages?.map((p) => p.pageId!) ?? [] }),

Check warning on line 41 in src/lib/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/style/noNonNullAssertion

Forbidden non-null assertion.
});
};

Expand All @@ -58,16 +53,16 @@
entity: "guild",
idLike: guildIdLike,
});
pageIdLikeWithHome = homePageId!;

Check warning on line 56 in src/lib/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/style/noNonNullAssertion

Forbidden non-null assertion.
}
const page = await fetchEntity({
entity: "page",
idLike: pageIdLikeWithHome!,

Check warning on line 60 in src/lib/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/style/noNonNullAssertion

Forbidden non-null assertion.
});

return fetchGuildApiData<Role[]>("role/batch", {
method: "POST",
body: JSON.stringify({ ids: page.roles?.map((p) => p.roleId!) ?? [] }),

Check warning on line 65 in src/lib/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/style/noNonNullAssertion

Forbidden non-null assertion.
});
};

Expand All @@ -77,7 +72,7 @@
return fetchGuildApiData<Schemas["Reward"][]>("reward/batch", {
method: "POST",
body: JSON.stringify({
ids: role.rewards?.map((r) => r.rewardId!) ?? [],

Check warning on line 75 in src/lib/fetchers.ts

View workflow job for this annotation

GitHub Actions / quality-assurance

lint/style/noNonNullAssertion

Forbidden non-null assertion.
}),
});
};
Loading
Loading