refactor: remove supabase auth

This commit is contained in:
Henry
2025-05-05 15:16:30 +01:00
parent 091fea6f38
commit b6daaa41de
42 changed files with 319 additions and 1591 deletions

View File

@@ -26,7 +26,7 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
watch,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(EmailSchema), // Apply the zodResolver
resolver: zodResolver(EmailSchema),
});
const email = watch("email");
@@ -37,22 +37,21 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
},
});
const loginWithOAuth = api.auth.loginWithOAuth.useMutation({
onSuccess: (data) => {
if (data.url) window.open(data.url);
},
});
const handleLoginWithEmail = async (email: string) => {
const { data, error } = await authClient.signIn.magicLink({
email,
callbackURL: "/boards",
});
const onSubmit = (values: FormValues) => {
try {
loginWithEmail.mutate({
email: values.email,
});
} catch (error) {
console.error(error);
if (!error) {
setIsMagicLinkSent(true, email);
}
};
const onSubmit = async (values: FormValues) => {
await handleLoginWithEmail(values.email);
};
return (
<div className="space-y-6">
<div>
@@ -80,12 +79,12 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
{...register("email", { required: true })}
placeholder="Enter your email address"
/>
{!loginWithEmail.error && !loginWithOAuth.error && errors.email && (
{!loginWithEmail.error && errors.email && (
<p className="mt-2 text-xs text-red-400">
Please enter a valid email address
</p>
)}
{(loginWithEmail.error ?? loginWithOAuth.error) ? (
{loginWithEmail.error ? (
<p className="mt-2 text-xs text-red-400">
Something went wrong, please try again later or contact customer
support.

View File

@@ -3,9 +3,9 @@ import { useRouter } from "next/navigation";
import { Menu, Transition } from "@headlessui/react";
import { Fragment } from "react";
import { authClient } from "@kan/auth";
import { useTheme } from "~/providers/theme";
import createClient from "~/utils/supabase/client";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
interface UserMenuProps {
imageUrl: string | undefined;
@@ -22,17 +22,17 @@ export default function UserMenu({
email,
isLoading,
}: UserMenuProps) {
const db = createClient();
const router = useRouter();
const { themePreference, switchTheme } = useTheme();
const handleLogout = async () => {
await db.auth.signOut();
await authClient.signOut();
router.push("/login");
};
const avatarUrl = imageUrl ? getPublicUrl(imageUrl) : null;
// const avatarUrl = imageUrl ? getPublicUrl(imageUrl) : null;
const avatarUrl = "";
return (
<Menu as="div" className="relative inline-block w-full text-left">

View File

@@ -25,19 +25,15 @@ export const env = createEnv({
* For them to be exposed to the client, prefix them with `NEXT_PUBLIC_`.
*/
client: {
NEXT_PUBLIC_SUPABASE_AUTH_COOKIE_NAME: z.string(),
NEXT_PUBLIC_SUPABASE_STORAGE_URL: z.string(),
NEXT_PUBLIC_KAN_ENV: z.string(),
NEXT_PUBLIC_UMAMI_ID: z.string().optional(),
},
/**
* Destructure all variables from `process.env` to make sure they aren't tree-shaken away.
*/
experimental__runtimeEnv: {
NEXT_PUBLIC_KAN_ENV: process.env.NEXT_PUBLIC_KAN_ENV,
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_SUPABASE_AUTH_COOKIE_NAME:
process.env.NEXT_PUBLIC_SUPABASE_AUTH_COOKIE_NAME,
NEXT_PUBLIC_SUPABASE_STORAGE_URL:
process.env.NEXT_PUBLIC_SUPABASE_STORAGE_URL,
NEXT_PUBLIC_UMAMI_ID: process.env.NEXT_PUBLIC_UMAMI_ID,
},
skipValidation:

View File

@@ -4,3 +4,7 @@ import { auth } from "@kan/auth";
export const config = { api: { bodyParser: false } };
export default toNodeHandler(auth.handler);
// export const runtime = "edge";
// export const preferredRegion = "lhr1";
// export const dynamic = "force-dynamic";

View File

@@ -1,112 +1,102 @@
import type { EmailOtpType } from "@supabase/supabase-js";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Stripe } from "stripe";
// import type { EmailOtpType } from "@supabase/supabase-js";
// import type { NextRequest } from "next/server";
// import { NextResponse } from "next/server";
import { createDrizzleClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import { createNextClient } from "@kan/supabase/clients";
// import { createDrizzleClient } from "@kan/db/client";
// import * as memberRepo from "@kan/db/repository/member.repo";
// import * as userRepo from "@kan/db/repository/user.repo";
// import { stripe } from "@kan/stripe";
// import { createNextApiClient } from "@kan/";
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
// export default async function handler(req: NextRequest) {
// if (req.method !== "GET") {
// return new NextResponse(null, {
// status: 405,
// headers: { Allow: "GET" },
// });
// }
if (!stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is not defined");
}
// if (!req.url) {
// return new NextResponse(null, {
// status: 400,
// });
// }
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
});
// const url = new URL(req.url);
// const queryParams = Object.fromEntries(url.searchParams.entries());
export default async function handler(req: NextRequest) {
if (req.method !== "GET") {
return new NextResponse(null, {
status: 405,
headers: { Allow: "GET" },
});
}
// const tokenHash = queryParams.token_hash;
// const type = queryParams.type;
// const code = queryParams.code;
// const memberPublicId = queryParams.memberPublicId;
if (!req.url) {
return new NextResponse(null, {
status: 400,
});
}
// let next = "/error";
const url = new URL(req.url);
const queryParams = Object.fromEntries(url.searchParams.entries());
// let authRes;
const tokenHash = queryParams.token_hash;
const type = queryParams.type;
const code = queryParams.code;
const memberPublicId = queryParams.memberPublicId;
// const response = NextResponse.next();
let next = "/error";
// if ((tokenHash && type) ?? code) {
// const supabaseClient = createNextClient(req, response);
let authRes;
// if (tokenHash && type) {
// authRes = await supabaseClient.auth.verifyOtp({
// type: type as EmailOtpType,
// token_hash: tokenHash,
// });
// }
const response = NextResponse.next();
// if (code) {
// authRes = await supabaseClient.auth.exchangeCodeForSession(code);
// }
if ((tokenHash && type) ?? code) {
const supabaseClient = createNextClient(req, response);
// const user = authRes?.data.user;
if (tokenHash && type) {
authRes = await supabaseClient.auth.verifyOtp({
type: type as EmailOtpType,
token_hash: tokenHash,
});
}
// const db = createDrizzleClient();
if (code) {
authRes = await supabaseClient.auth.exchangeCodeForSession(code);
}
// if (user?.id && user.email) {
// const existingUser = await userRepo.getById(db, user.id);
const user = authRes?.data.user;
// if (!existingUser) {
// const stripeCustomer = await stripe.customers.create({
// email: user.email,
// metadata: {
// userId: user.id,
// },
// });
const db = createDrizzleClient();
// await userRepo.create(db, {
// id: user.id,
// email: user.email,
// stripeCustomerId: stripeCustomer.id,
// });
// }
// }
if (user?.id && user.email) {
const existingUser = await userRepo.getById(db, user.id);
// if (memberPublicId) {
// const member = await memberRepo.getByPublicId(db, memberPublicId);
if (!existingUser) {
const stripeCustomer = await stripe.customers.create({
email: user.email,
metadata: {
userId: user.id,
},
});
// if (member?.id) {
// await memberRepo.acceptInvite(db, member.id);
// }
// }
await userRepo.create(db, {
id: user.id,
email: user.email,
stripeCustomerId: stripeCustomer.id,
});
}
}
// if (authRes?.error) {
// console.error(authRes.error);
// } else {
// next = queryParams.next ?? "/boards";
// }
// }
if (memberPublicId) {
const member = await memberRepo.getByPublicId(db, memberPublicId);
// const redirectResponse = NextResponse.redirect(new URL(next, req.url));
if (member?.id) {
await memberRepo.acceptInvite(db, member.id);
}
}
// response.headers.getSetCookie().forEach((cookie) => {
// redirectResponse.headers.append("Set-Cookie", cookie);
// });
if (authRes?.error) {
console.error(authRes.error);
} else {
next = queryParams.next ?? "/boards";
}
}
// return redirectResponse;
// }
const redirectResponse = NextResponse.redirect(new URL(next, req.url));
response.headers.getSetCookie().forEach((cookie) => {
redirectResponse.headers.append("Set-Cookie", cookie);
});
return redirectResponse;
}
export const runtime = "edge";
export const preferredRegion = "lhr1";
export const dynamic = "force-dynamic";
// export const runtime = "edge";
// export const preferredRegion = "lhr1";
// export const dynamic = "force-dynamic";

View File

@@ -1,22 +1,11 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Stripe } from "stripe";
import { createDrizzleClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo";
import { createNextClient } from "@kan/supabase/clients";
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is not defined");
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
});
import { createNextApiContext } from "@kan/api/trpc";
import { createStripeClient } from "@kan/stripe";
export default async function handler(req: NextRequest) {
const stripe = createStripeClient();
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
@@ -25,20 +14,7 @@ export default async function handler(req: NextRequest) {
}
try {
const response = NextResponse.next();
const supabaseClient = createNextClient(req, response);
const { data } = await supabaseClient.auth.getUser();
if (!data.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
const db = createDrizzleClient();
const user = await userRepo.getById(db, data.user.id);
const { user } = await createNextApiContext(req);
if (!user?.stripeCustomerId) {
return new Response(

View File

@@ -1,22 +1,9 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Stripe } from "stripe";
import { z } from "zod";
import { createDrizzleClient } from "@kan/db/client";
import * as userRepo from "@kan/db/repository/user.repo";
import { createNextApiContext } from "@kan/api/trpc";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createNextClient } from "@kan/supabase/clients";
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is not defined");
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
});
import { createStripeClient } from "@kan/stripe";
const workspaceSlugSchema = z
.string()
@@ -33,6 +20,8 @@ interface CheckoutSessionRequest {
}
export default async function handler(req: NextRequest) {
const stripe = createStripeClient();
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
@@ -41,22 +30,7 @@ export default async function handler(req: NextRequest) {
}
try {
const response = NextResponse.next();
const supabaseClient = createNextClient(req, response);
const { data } = await supabaseClient.auth.getUser();
if (!data.user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}
const db = createDrizzleClient();
const user = await userRepo.getById(db, data.user.id);
const { user, db } = await createNextApiContext(req);
if (!user) {
return new Response(JSON.stringify({ error: "User not found" }), {

View File

@@ -1,24 +1,15 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { Stripe } from "stripe";
import { createNextApiContext } from "@kan/api/trpc";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createNextClient } from "@kan/supabase/clients";
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is not defined");
}
import { createStripeClient } from "@kan/stripe";
export const webCrypto = Stripe.createSubtleCryptoProvider();
const stripe: Stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
httpClient: Stripe.createFetchHttpClient(),
});
export default async function handler(req: NextRequest) {
const stripe = createStripeClient();
if (req.method !== "POST") {
return new Response(JSON.stringify({ message: "Method not allowed" }), {
status: 405,
@@ -44,9 +35,7 @@ export default async function handler(req: NextRequest) {
webCrypto,
);
const response = NextResponse.next();
const db = createNextClient(req, response);
const { db } = await createNextApiContext(req);
switch (event.type) {
case "checkout.session.completed": {

View File

@@ -1,12 +0,0 @@
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@kan/db/types/database.types";
export default function createClient() {
const supabase = createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
return supabase;
}

View File

@@ -1,7 +0,0 @@
import createClient from "~/utils/supabase/client";
export const getPublicUrl = (fileName: string) => {
const supabase = createClient();
return supabase.storage.from("avatars").getPublicUrl(fileName).data.publicUrl;
};

View File

@@ -1,7 +1,6 @@
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import LabelIcon from "~/components/LabelIcon";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
const Card = ({
title,
@@ -32,9 +31,7 @@ const Card = ({
{members.map(({ user }) => {
if (!user) return null;
const avatarUrl = user.image
? getPublicUrl(user.image)
: undefined;
const avatarUrl = user.image ? "" : undefined;
return (
<Avatar

View File

@@ -11,7 +11,6 @@ import Button from "~/components/Button";
import CheckboxDropdown from "~/components/CheckboxDropdown";
import LabelIcon from "~/components/LabelIcon";
import { formatMemberDisplayName, formatToArray } from "~/utils/helpers";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
interface Member {
publicId: string;
@@ -66,9 +65,7 @@ const Filters = ({
<Avatar
size="xs"
name={member.user?.name ?? ""}
imageUrl={
member.user?.image ? getPublicUrl(member.user.image) : undefined
}
imageUrl={member.user?.image ? "" : undefined}
email={member.user?.email ?? ""}
/>
),

View File

@@ -19,7 +19,6 @@ import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import { formatMemberDisplayName } from "~/utils/helpers";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
type NewCardFormInput = NewCardInput & {
isCreateAnotherEnabled: boolean;
@@ -89,7 +88,7 @@ export function NewCardForm({
args.labelPublicIds.includes(label.publicId),
),
members:
oldBoard.workspace?.members.filter((member) =>
oldBoard.workspace.members.filter((member) =>
args.memberPublicIds.includes(member.publicId),
) ?? [],
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
@@ -146,20 +145,18 @@ export function NewCardForm({
})) ?? [];
const formattedMembers =
boardData?.workspace?.members.map((member) => ({
boardData?.workspace.members.map((member) => ({
key: member.publicId,
value: formatMemberDisplayName(
member.user?.name ?? null,
member.user?.email ?? null,
member.user.name ?? null,
member.user.email ?? null,
),
leftIcon: (
<Avatar
size="xs"
name={member.user?.name ?? ""}
imageUrl={
member.user?.image ? getPublicUrl(member.user.image) : undefined
}
email={member.user?.email ?? ""}
name={member.user.name ?? ""}
imageUrl={member.user.image ? "" : undefined}
email={member.user.email ?? ""}
/>
),
})) ?? [];

View File

@@ -15,7 +15,6 @@ import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { formatMemberDisplayName } from "~/utils/helpers";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
import ActivityList from "./components/ActivityList";
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
@@ -51,11 +50,11 @@ export default function CardPage() {
if (cardId) await utils.card.byId.refetch({ cardPublicId: cardId });
};
const board = card?.list?.board;
const board = card?.list.board;
const boardId = board?.publicId;
const labels = board?.labels;
const activities = card?.activities;
const workspaceMembers = board?.workspace?.members;
const workspaceMembers = board?.workspace.members;
const selectedLabels = card?.labels;
const selectedMembers = card?.members;
@@ -77,7 +76,7 @@ export default function CardPage() {
board?.lists.map((list) => ({
key: list.publicId,
value: list.name,
selected: list.publicId === card?.list?.publicId,
selected: list.publicId === card?.list.publicId,
})) ?? [];
const formattedMembers =
@@ -89,21 +88,21 @@ export default function CardPage() {
return {
key: member.publicId,
value: formatMemberDisplayName(
member.user?.name ?? null,
member.user?.email ?? null,
member.user.name ?? null,
member.user.email ?? null,
),
imageUrl: member.user?.image
imageUrl: member.user.image
? getPublicUrl(member.user.image)
: undefined,
selected: isSelected ?? false,
leftIcon: (
<Avatar
size="xs"
name={member.user?.name ?? ""}
name={member.user.name ?? ""}
imageUrl={
member.user?.image ? getPublicUrl(member.user.image) : undefined
member.user.image ? getPublicUrl(member.user.image) : undefined
}
email={member.user?.email ?? ""}
email={member.user.email ?? ""}
/>
),
};

View File

@@ -12,9 +12,11 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const token =
typeof window !== "undefined"
? Cookies.get(env.NEXT_PUBLIC_SUPABASE_AUTH_COOKIE_NAME)
? Cookies.get('kan.session_token')
: null;
const getSession = async () => {
const { data } = api.user.getUser.useQuery(undefined, {
enabled: !!token,
});

View File

@@ -11,7 +11,6 @@ import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { getInitialsFromName, inferInitialsFromEmail } from "~/utils/helpers";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
import { DeleteMemberConfirmation } from "./components/DeleteMemberConfirmation";
import { InviteMemberForm } from "./components/InviteMemberForm";
@@ -58,7 +57,7 @@ export default function MembersPage() {
<Avatar
name={memberName ?? ""}
email={memberEmail ?? ""}
imageUrl={memberImage ? getPublicUrl(memberImage) : undefined}
imageUrl={memberImage ? "" : undefined}
/>
)}
</div>
@@ -189,9 +188,9 @@ export default function MembersPage() {
<TableRow
key={member.publicId}
memberPublicId={member.publicId}
memberName={member.user?.name}
memberEmail={member.user?.email}
memberImage={member.user?.image}
memberName={member.user.name}
memberEmail={member.user.email}
memberImage={member.user.image}
memberRole={member.role}
memberStatus={member.status}
isLastRow={index === data.members.length - 1}

View File

@@ -3,8 +3,6 @@ import { useState } from "react";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import createClient from "~/utils/supabase/client";
import { getPublicUrl } from "~/utils/supabase/getPublicUrl";
export default function Avatar({
userId,
@@ -13,7 +11,6 @@ export default function Avatar({
userId: string | undefined;
userImage: string | null | undefined;
}) {
const supabase = createClient();
const utils = api.useUtils();
const { showPopup } = usePopup();
const [uploading, setUploading] = useState(false);
@@ -41,7 +38,7 @@ export default function Avatar({
},
});
const avatarUrl = userImage ? getPublicUrl(userImage) : undefined;
const avatarUrl = userImage ? "" : undefined;
const uploadAvatar = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
@@ -65,13 +62,13 @@ export default function Avatar({
const fileName = `${userId}/avatar.${fileExt}`;
const filePath = `${fileName}`;
const { error: uploadError } = await supabase.storage
.from("avatars")
.upload(filePath, file, { upsert: true });
// const { error: uploadError } = await supabase.storage
// .from("avatars")
// .upload(filePath, file, { upsert: true });
if (uploadError) {
throw uploadError;
}
// if (uploadError) {
// throw uploadError;
// }
updateUser.mutate({ image: filePath });
} catch (error) {