code refactoring + many security/bug fixes

This commit is contained in:
daniel31x13 2023-11-06 08:25:57 -05:00
parent b5a28f68ad
commit c8edc3844b
48 changed files with 472 additions and 317 deletions

View File

@ -47,6 +47,7 @@ export default function TeamManagement({
id: null, id: null,
name: "", name: "",
username: "", username: "",
image: "",
}); });
useEffect(() => { useEffect(() => {
@ -401,7 +402,7 @@ export default function TeamManagement({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ProfilePhoto <ProfilePhoto
src={`uploads/avatar/${collection.ownerId}.jpg`} src={collectionOwner.image ? collectionOwner.image : undefined}
className="border-[3px]" className="border-[3px]"
/> />
<div> <div>

View File

@ -113,7 +113,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
</div> </div>
</Link> </Link>
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? ( {process.env.NEXT_PUBLIC_STRIPE ? (
<Link href="/settings/billing"> <Link href="/settings/billing">
<div <div
className={`${ className={`${

View File

@ -9,17 +9,21 @@ export default function useInitialData() {
const { setCollections } = useCollectionStore(); const { setCollections } = useCollectionStore();
const { setTags } = useTagStore(); const { setTags } = useTagStore();
// const { setLinks } = useLinkStore(); // const { setLinks } = useLinkStore();
const { setAccount } = useAccountStore(); const { account, setAccount } = useAccountStore();
// Get account info
useEffect(() => { useEffect(() => {
if ( if (status === "authenticated") {
status === "authenticated" && setAccount(data?.user.id as number);
(!process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE || data.user.isSubscriber) }
) { }, [status, data]);
// Get the rest of the data
useEffect(() => {
if (account.id && (!process.env.NEXT_PUBLIC_STRIPE || account.username)) {
setCollections(); setCollections();
setTags(); setTags();
// setLinks(); // setLinks();
setAccount(data.user.id);
} }
}, [status, data]); }, [account]);
} }

View File

@ -1,30 +0,0 @@
import { useEffect, useState } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
export default function useRedirect() {
const router = useRouter();
const { status } = useSession();
const [redirect, setRedirect] = useState(true);
useEffect(() => {
if (
status === "authenticated" &&
(router.pathname === "/login" || router.pathname === "/register")
) {
router.push("/").then(() => {
setRedirect(false);
});
} else if (
status === "unauthenticated" &&
!(router.pathname === "/login" || router.pathname === "/register")
) {
router.push("/login").then(() => {
setRedirect(false);
});
} else if (status === "loading") setRedirect(true);
else setRedirect(false);
}, [status]);
return redirect;
}

View File

@ -16,17 +16,29 @@ export default function AuthRedirect({ children }: Props) {
const [redirect, setRedirect] = useState(true); const [redirect, setRedirect] = useState(true);
const { account } = useAccountStore(); const { account } = useAccountStore();
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
const stripeEnabled = process.env.NEXT_PUBLIC_STRIPE === "true";
useInitialData(); useInitialData();
useEffect(() => { useEffect(() => {
if (!router.pathname.startsWith("/public")) { if (!router.pathname.startsWith("/public")) {
if ( if (
status === "authenticated" &&
account.id &&
!account.subscription?.active &&
stripeEnabled
) {
router.push("/subscribe").then(() => {
setRedirect(false);
});
}
// Redirect to "/choose-username" if user is authenticated and is either a subscriber OR subscription is undefiend, and doesn't have a username
else if (
emailEnabled && emailEnabled &&
status === "authenticated" && status === "authenticated" &&
(data.user.isSubscriber === true || account.subscription?.active &&
data.user.isSubscriber === undefined) && stripeEnabled &&
account.id && account.id &&
!account.username !account.username
) { ) {
@ -35,21 +47,16 @@ export default function AuthRedirect({ children }: Props) {
}); });
} else if ( } else if (
status === "authenticated" && status === "authenticated" &&
data.user.isSubscriber === false account.id &&
) {
router.push("/subscribe").then(() => {
setRedirect(false);
});
} else if (
status === "authenticated" &&
(router.pathname === "/login" || (router.pathname === "/login" ||
router.pathname === "/register" || router.pathname === "/register" ||
router.pathname === "/confirmation" || router.pathname === "/confirmation" ||
router.pathname === "/subscribe" || router.pathname === "/subscribe" ||
router.pathname === "/choose-username" || router.pathname === "/choose-username" ||
router.pathname === "/forgot") router.pathname === "/forgot" ||
router.pathname === "/")
) { ) {
router.push("/").then(() => { router.push("/dashboard").then(() => {
setRedirect(false); setRedirect(false);
}); });
} else if ( } else if (
@ -69,7 +76,7 @@ export default function AuthRedirect({ children }: Props) {
} else { } else {
setRedirect(false); setRedirect(false);
} }
}, [status, account]); }, [status, account, router.pathname]);
if (status !== "loading" && !redirect) return <>{children}</>; if (status !== "loading" && !redirect) return <>{children}</>;
else return <></>; else return <></>;

View File

@ -10,10 +10,20 @@ interface Props {
export default function CenteredForm({ text, children }: Props) { export default function CenteredForm({ text, children }: Props) {
const { theme } = useTheme(); const { theme } = useTheme();
return ( return (
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5"> <div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
<div className="m-auto flex flex-col gap-2 w-full"> <div className="m-auto flex flex-col gap-2 w-full">
{theme === "dark" ? ( {theme ? (
<Image
src={`/linkwarden_${theme === "dark" ? "dark" : "li"}.png`}
width={640}
height={136}
alt="Linkwarden"
className="h-12 w-fit mx-auto"
/>
) : undefined}
{/* {theme === "dark" ? (
<Image <Image
src="/linkwarden_dark.png" src="/linkwarden_dark.png"
width={640} width={640}
@ -29,7 +39,7 @@ export default function CenteredForm({ text, children }: Props) {
alt="Linkwarden" alt="Linkwarden"
className="h-12 w-fit mx-auto" className="h-12 w-fit mx-auto"
/> />
)} )} */}
{text ? ( {text ? (
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold text-black dark:text-white px-2 text-center"> <p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold text-black dark:text-white px-2 text-center">
{text} {text}

View File

@ -93,11 +93,14 @@ export default function LinkLayout({ children }: Props) {
</div> */} </div> */}
<div <div
onClick={() => router.back()} onClick={() => router.push(`/collections/${linkCollection?.id}`)}
className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100" className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100"
> >
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" /> <FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
Back Back{" "}
<span className="hidden sm:inline-block">
to <span className="capitalize">{linkCollection?.name}</span>
</span>
</div> </div>
<div className="lg:hidden"> <div className="lg:hidden">

View File

@ -1,31 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { prisma } from "./db";
import { User } from "@prisma/client";
type Props = {
req: NextApiRequest;
res: NextApiResponse;
};
export default async function authenticateUser({
req,
res,
}: Props): Promise<User | null> {
const token = await getToken({ req });
const userId = token?.id;
if (!userId) {
res.status(401).json({ message: "You must be logged in." });
return null;
} else if (process.env.STRIPE_SECRET_KEY && token.isSubscriber === false) {
res.status(401).json({
message:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app if you think this is an issue.",
});
return null;
}
const user = await prisma.user.findUnique({ where: { id: userId } });
return user;
}

View File

@ -1,49 +0,0 @@
import Stripe from "stripe";
export default async function checkSubscription(
stripeSecretKey: string,
email: string
) {
const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2022-11-15",
});
const listByEmail = await stripe.customers.list({
email: email.toLowerCase(),
expand: ["data.subscriptions"],
});
let subscriptionCanceledAt: number | null | undefined;
const isSubscriber = listByEmail.data.some((customer, i) => {
const hasValidSubscription = customer.subscriptions?.data.some(
(subscription) => {
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
const secondsInTwoWeeks = NEXT_PUBLIC_TRIAL_PERIOD_DAYS
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS) * 86400
: 1209600;
subscriptionCanceledAt = subscription.canceled_at;
const isNotCanceledOrHasTime = !(
subscription.canceled_at &&
new Date() >
new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000)
);
return subscription?.items?.data[0].plan && isNotCanceledOrHasTime;
}
);
return (
customer.email?.toLowerCase() === email.toLowerCase() &&
hasValidSubscription
);
});
return {
isSubscriber,
subscriptionCanceledAt,
};
}

View File

@ -0,0 +1,52 @@
import Stripe from "stripe";
const MONTHLY_PRICE_ID = process.env.MONTHLY_PRICE_ID;
const YEARLY_PRICE_ID = process.env.YEARLY_PRICE_ID;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export default async function checkSubscriptionByEmail(email: string) {
let active: boolean | undefined,
stripeSubscriptionId: string | undefined,
currentPeriodStart: number | undefined,
currentPeriodEnd: number | undefined;
if (!STRIPE_SECRET_KEY)
return {
active,
stripeSubscriptionId,
currentPeriodStart,
currentPeriodEnd,
};
const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: "2022-11-15",
});
console.log("Request made to Stripe by:", email);
const listByEmail = await stripe.customers.list({
email: email.toLowerCase(),
expand: ["data.subscriptions"],
});
listByEmail.data.some((customer) => {
customer.subscriptions?.data.some((subscription) => {
subscription.current_period_end;
active = subscription.items.data.some(
(e) =>
(e.price.id === MONTHLY_PRICE_ID && e.price.active === true) ||
(e.price.id === YEARLY_PRICE_ID && e.price.active === true)
);
stripeSubscriptionId = subscription.id;
currentPeriodStart = subscription.current_period_start * 1000;
currentPeriodEnd = subscription.current_period_end * 1000;
});
});
return {
active,
stripeSubscriptionId,
currentPeriodStart,
currentPeriodEnd,
};
}

View File

@ -3,7 +3,7 @@ import { prisma } from "@/lib/api/db";
export default async function getPublicUserById( export default async function getPublicUserById(
targetId: number | string, targetId: number | string,
isId: boolean, isId: boolean,
requestingUsername?: string requestingId?: number
) { ) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: isId where: isId
@ -29,11 +29,22 @@ export default async function getPublicUserById(
(usernames) => usernames.username (usernames) => usernames.username
); );
if (user?.isPrivate) {
if (requestingId) {
const requestingUsername = (
await prisma.user.findUnique({ where: { id: requestingId } })
)?.username;
if ( if (
user?.isPrivate && !requestingUsername ||
(!requestingUsername || !whitelistedUsernames.includes(requestingUsername?.toLowerCase())
!whitelistedUsernames.includes(requestingUsername?.toLowerCase()))
) { ) {
return {
response: "User not found or profile is private.",
status: 404,
};
}
} else
return { response: "User not found or profile is private.", status: 404 }; return { response: "User not found or profile is private.", status: 404 };
} }

View File

@ -68,6 +68,11 @@ export default async function deleteUserById(
where: { ownerId: userId }, where: { ownerId: userId },
}); });
// Delete subscription
await prisma.subscription.delete({
where: { userId },
});
// Delete user's avatar // Delete user's avatar
removeFile({ filePath: `uploads/avatar/${userId}.jpg` }); removeFile({ filePath: `uploads/avatar/${userId}.jpg` });

View File

@ -11,6 +11,7 @@ export default async function getUserById(userId: number) {
username: true, username: true,
}, },
}, },
subscriptions: true,
}, },
}); });
@ -21,11 +22,14 @@ export default async function getUserById(userId: number) {
(usernames) => usernames.username (usernames) => usernames.username
); );
const { password, ...lessSensitiveInfo } = user; const { password, subscriptions, ...lessSensitiveInfo } = user;
const data = { const data = {
...lessSensitiveInfo, ...lessSensitiveInfo,
whitelistedUsers: whitelistedUsernames, whitelistedUsers: whitelistedUsernames,
subscription: {
active: subscriptions?.active,
},
}; };
return { response: data, status: 200 }; return { response: data, status: 200 };

View File

@ -139,10 +139,12 @@ export default async function updateUserById(
}, },
include: { include: {
whitelistedUsers: true, whitelistedUsers: true,
subscriptions: true,
}, },
}); });
const { whitelistedUsers, password, ...userInfo } = updatedUser; const { whitelistedUsers, password, subscriptions, ...userInfo } =
updatedUser;
// If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed // If user.whitelistedUsers is not provided, we will assume the whitelistedUsers should be removed
const newWhitelistedUsernames: string[] = data.whitelistedUsers || []; const newWhitelistedUsernames: string[] = data.whitelistedUsers || [];
@ -196,6 +198,7 @@ export default async function updateUserById(
...userInfo, ...userInfo,
whitelistedUsers: newWhitelistedUsernames, whitelistedUsers: newWhitelistedUsernames,
image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "", image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
subscription: { active: subscriptions?.active },
}; };
return { response, status: 200 }; return { response, status: 200 };

View File

@ -14,12 +14,12 @@ export default async function paymentCheckout(
expand: ["data.subscriptions"], expand: ["data.subscriptions"],
}); });
const isExistingCostomer = listByEmail?.data[0]?.id || undefined; const isExistingCustomer = listByEmail?.data[0]?.id || undefined;
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS = const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS; process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
const session = await stripe.checkout.sessions.create({ const session = await stripe.checkout.sessions.create({
customer: isExistingCostomer ? isExistingCostomer : undefined, customer: isExistingCustomer ? isExistingCustomer : undefined,
line_items: [ line_items: [
{ {
price: priceId, price: priceId,
@ -27,7 +27,7 @@ export default async function paymentCheckout(
}, },
], ],
mode: "subscription", mode: "subscription",
customer_email: isExistingCostomer ? undefined : email.toLowerCase(), customer_email: isExistingCustomer ? undefined : email.toLowerCase(),
success_url: `${process.env.BASE_URL}?session_id={CHECKOUT_SESSION_ID}`, success_url: `${process.env.BASE_URL}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.BASE_URL}/login`, cancel_url: `${process.env.BASE_URL}/login`,
automatic_tax: { automatic_tax: {

View File

@ -0,0 +1,70 @@
import { prisma } from "./db";
import { Subscription, User } from "@prisma/client";
import checkSubscriptionByEmail from "./checkSubscriptionByEmail";
interface UserIncludingSubscription extends User {
subscriptions: Subscription | null;
}
export default async function verifySubscription(
user?: UserIncludingSubscription
) {
if (!user) {
return null;
}
const subscription = user.subscriptions;
const currentDate = new Date();
if (
subscription &&
currentDate > subscription.currentPeriodEnd &&
!subscription.active
) {
return null;
}
if (!subscription || currentDate > subscription.currentPeriodEnd) {
const {
active,
stripeSubscriptionId,
currentPeriodStart,
currentPeriodEnd,
} = await checkSubscriptionByEmail(user.email as string);
if (
active &&
stripeSubscriptionId &&
currentPeriodStart &&
currentPeriodEnd
) {
await prisma.subscription
.upsert({
where: {
userId: user.id,
},
create: {
active,
stripeSubscriptionId,
currentPeriodStart: new Date(currentPeriodStart),
currentPeriodEnd: new Date(currentPeriodEnd),
userId: user.id,
},
update: {
active,
stripeSubscriptionId,
currentPeriodStart: new Date(currentPeriodStart),
currentPeriodEnd: new Date(currentPeriodEnd),
},
})
.catch((err) => console.log(err));
}
if (!active) {
return null;
}
}
return user;
}

60
lib/api/verifyUser.ts Normal file
View File

@ -0,0 +1,60 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { prisma } from "./db";
import { User } from "@prisma/client";
import verifySubscription from "./verifySubscription";
type Props = {
req: NextApiRequest;
res: NextApiResponse;
};
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export default async function verifyUser({
req,
res,
}: Props): Promise<User | null> {
const token = await getToken({ req });
const userId = token?.id;
if (!userId) {
res.status(401).json({ response: "You must be logged in." });
return null;
}
const user = await prisma.user.findUnique({
where: {
id: userId,
},
include: {
subscriptions: true,
},
});
if (!user) {
res.status(404).json({ response: "User not found." });
return null;
}
if (!user.username) {
res.status(401).json({
response: "Username not found.",
});
return null;
}
if (STRIPE_SECRET_KEY) {
const subscribedUser = verifySubscription(user);
if (!subscribedUser) {
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app if you think this is an issue.",
});
return null;
}
}
return user;
}

View File

@ -1,7 +1,7 @@
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
export default async function getPublicUserData(id: number | string) { export default async function getPublicUserData(id: number | string) {
const response = await fetch(`/api/v1/users/${id}`); const response = await fetch(`/api/v1/public/users/${id}`);
const data = await response.json(); const data = await response.json();

View File

@ -22,7 +22,11 @@ export default function App({
}, []); }, []);
return ( return (
<SessionProvider session={pageProps.session} basePath="/api/v1/auth"> <SessionProvider
session={pageProps.session}
refetchOnWindowFocus={false}
basePath="/api/v1/auth"
>
<Head> <Head>
<title>Linkwarden</title> <title>Linkwarden</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -1,14 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";
import readFile from "@/lib/api/storage/readFile"; import readFile from "@/lib/api/storage/readFile";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function Index(req: NextApiRequest, res: NextApiResponse) { export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!req.query.params) if (!req.query.params)
return res.status(401).json({ response: "Invalid parameters." }); return res.status(401).json({ response: "Invalid parameters." });
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
const collectionId = req.query.params[0]; const collectionId = req.query.params[0];
const linkId = req.query.params[1]; const linkId = req.query.params[1];

View File

@ -1,24 +1,26 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import NextAuth from "next-auth/next"; import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import { AuthOptions, Session, User } from "next-auth"; import { AuthOptions } from "next-auth";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import EmailProvider from "next-auth/providers/email"; import EmailProvider from "next-auth/providers/email";
import { PrismaAdapter } from "@auth/prisma-adapter"; import { PrismaAdapter } from "@auth/prisma-adapter";
import { Adapter } from "next-auth/adapters"; import { Adapter } from "next-auth/adapters";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest"; import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import { Provider } from "next-auth/providers"; import { Provider } from "next-auth/providers";
import checkSubscription from "@/lib/api/checkSubscription"; import verifySubscription from "@/lib/api/verifySubscription";
const emailEnabled = const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false; process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const providers: Provider[] = [ const providers: Provider[] = [
CredentialsProvider({ CredentialsProvider({
type: "credentials", type: "credentials",
credentials: {}, credentials: {},
async authorize(credentials, req) { async authorize(credentials, req) {
console.log("User logged in attempt..."); console.log("User log in attempt...");
if (!credentials) return null; if (!credentials) return null;
const { username, password } = credentials as { const { username, password } = credentials as {
@ -26,7 +28,7 @@ const providers: Provider[] = [
password: string; password: string;
}; };
const findUser = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: emailEnabled where: emailEnabled
? { ? {
OR: [ OR: [
@ -46,12 +48,12 @@ const providers: Provider[] = [
let passwordMatches: boolean = false; let passwordMatches: boolean = false;
if (findUser?.password) { if (user?.password) {
passwordMatches = bcrypt.compareSync(password, findUser.password); passwordMatches = bcrypt.compareSync(password, user.password);
} }
if (passwordMatches) { if (passwordMatches) {
return { id: findUser?.id }; return { id: user?.id };
} else return null as any; } else return null as any;
}, },
}), }),
@ -82,62 +84,28 @@ export const authOptions: AuthOptions = {
}, },
callbacks: { callbacks: {
async jwt({ token, trigger, user }) { async jwt({ token, trigger, user }) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; token.sub = token.sub ? Number(token.sub) : undefined;
if (trigger === "signIn") token.id = user?.id as number;
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
const secondsInTwoWeeks = NEXT_PUBLIC_TRIAL_PERIOD_DAYS
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS) * 86400
: 1209600;
const subscriptionIsTimesUp =
token.subscriptionCanceledAt &&
new Date() >
new Date(
((token.subscriptionCanceledAt as number) + secondsInTwoWeeks) *
1000
);
if (
STRIPE_SECRET_KEY &&
(trigger || subscriptionIsTimesUp || !token.isSubscriber)
) {
const user = await prisma.user.findUnique({
where: {
id: Number(token.sub),
},
});
const subscription = await checkSubscription(
STRIPE_SECRET_KEY,
user?.email as string
);
if (subscription.subscriptionCanceledAt) {
token.subscriptionCanceledAt = subscription.subscriptionCanceledAt;
} else token.subscriptionCanceledAt = undefined;
token.isSubscriber = subscription.isSubscriber;
}
if (trigger === "signIn") {
token.id = user.id as number;
} else if (trigger === "update" && token.id) {
const user = await prisma.user.findUnique({
where: {
id: token.id as number,
},
});
if (user?.name) {
token.name = user.name;
}
}
return token; return token;
}, },
async session({ session, token }) { async session({ session, token }) {
session.user.id = token.id; session.user.id = token.id;
session.user.isSubscriber = token.isSubscriber;
if (STRIPE_SECRET_KEY) {
const user = await prisma.user.findUnique({
where: {
id: token.id,
},
include: {
subscriptions: true,
},
});
if (user) {
const subscribedUser = await verifySubscription(user);
}
}
return session; return session;
}, },

View File

@ -1,13 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import readFile from "@/lib/api/storage/readFile"; import readFile from "@/lib/api/storage/readFile";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function Index(req: NextApiRequest, res: NextApiResponse) { export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const queryId = Number(req.query.id); const queryId = Number(req.query.id);
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (!queryId) if (!queryId)
return res return res

View File

@ -1,14 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import updateCollectionById from "@/lib/api/controllers/collections/collectionId/updateCollectionById"; import updateCollectionById from "@/lib/api/controllers/collections/collectionId/updateCollectionById";
import deleteCollectionById from "@/lib/api/controllers/collections/collectionId/deleteCollectionById"; import deleteCollectionById from "@/lib/api/controllers/collections/collectionId/deleteCollectionById";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function collections( export default async function collections(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "PUT") { if (req.method === "PUT") {
const updated = await updateCollectionById( const updated = await updateCollectionById(

View File

@ -1,14 +1,14 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import getCollections from "@/lib/api/controllers/collections/getCollections"; import getCollections from "@/lib/api/controllers/collections/getCollections";
import postCollection from "@/lib/api/controllers/collections/postCollection"; import postCollection from "@/lib/api/controllers/collections/postCollection";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function collections( export default async function collections(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse res: NextApiResponse
) { ) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
const collections = await getCollections(user.id); const collections = await getCollections(user.id);

View File

@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { LinkRequestQuery } from "@/types/global"; import { LinkRequestQuery } from "@/types/global";
import getDashboardData from "@/lib/api/controllers/dashboard/getDashboardData"; import getDashboardData from "@/lib/api/controllers/dashboard/getDashboardData";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
const convertedData: LinkRequestQuery = { const convertedData: LinkRequestQuery = {

View File

@ -1,13 +1,13 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import archive from "@/lib/api/archive"; import archive from "@/lib/api/archive";
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5; const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
const link = await prisma.link.findUnique({ const link = await prisma.link.findUnique({
where: { where: {

View File

@ -2,11 +2,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import deleteLinkById from "@/lib/api/controllers/links/linkId/deleteLinkById"; import deleteLinkById from "@/lib/api/controllers/links/linkId/deleteLinkById";
import updateLinkById from "@/lib/api/controllers/links/linkId/updateLinkById"; import updateLinkById from "@/lib/api/controllers/links/linkId/updateLinkById";
import getLinkById from "@/lib/api/controllers/links/linkId/getLinkById"; import getLinkById from "@/lib/api/controllers/links/linkId/getLinkById";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
const updated = await getLinkById(user.id, Number(req.query.id)); const updated = await getLinkById(user.id, Number(req.query.id));

View File

@ -2,11 +2,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import getLinks from "@/lib/api/controllers/links/getLinks"; import getLinks from "@/lib/api/controllers/links/getLinks";
import postLink from "@/lib/api/controllers/links/postLink"; import postLink from "@/lib/api/controllers/links/postLink";
import { LinkRequestQuery } from "@/types/global"; import { LinkRequestQuery } from "@/types/global";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
// Convert the type of the request query to "LinkRequestQuery" // Convert the type of the request query to "LinkRequestQuery"

View File

@ -3,7 +3,7 @@ import exportData from "@/lib/api/controllers/migration/exportData";
import importFromHTMLFile from "@/lib/api/controllers/migration/importFromHTMLFile"; import importFromHTMLFile from "@/lib/api/controllers/migration/importFromHTMLFile";
import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden"; import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden";
import { MigrationFormat, MigrationRequest } from "@/types/global"; import { MigrationFormat, MigrationRequest } from "@/types/global";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export const config = { export const config = {
api: { api: {
@ -14,8 +14,8 @@ export const config = {
}; };
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
const data = await exportData(user.id); const data = await exportData(user.id);

View File

@ -1,19 +1,27 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import paymentCheckout from "@/lib/api/paymentCheckout"; import paymentCheckout from "@/lib/api/paymentCheckout";
import { Plan } from "@/types/global"; import { Plan } from "@/types/global";
import authenticateUser from "@/lib/api/authenticateUser"; import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const MONTHLY_PRICE_ID = process.env.MONTHLY_PRICE_ID; const MONTHLY_PRICE_ID = process.env.MONTHLY_PRICE_ID;
const YEARLY_PRICE_ID = process.env.YEARLY_PRICE_ID; const YEARLY_PRICE_ID = process.env.YEARLY_PRICE_ID;
if (!STRIPE_SECRET_KEY || !MONTHLY_PRICE_ID || !YEARLY_PRICE_ID) { const token = await getToken({ req });
return res.status(400).json({ response: "Payment is disabled." });
}
const user = await authenticateUser({ req, res }); if (!STRIPE_SECRET_KEY || !MONTHLY_PRICE_ID || !YEARLY_PRICE_ID)
if (!user) return res.status(404).json({ response: "User not found." }); return res.status(400).json({ response: "Payment is disabled." });
console.log(token);
if (!token?.id) return res.status(404).json({ response: "Token invalid." });
const email = (await prisma.user.findUnique({ where: { id: token.id } }))
?.email;
if (!email) return res.status(404).json({ response: "User not found." });
let PRICE_ID = MONTHLY_PRICE_ID; let PRICE_ID = MONTHLY_PRICE_ID;
@ -25,7 +33,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") { if (req.method === "GET") {
const users = await paymentCheckout( const users = await paymentCheckout(
STRIPE_SECRET_KEY, STRIPE_SECRET_KEY,
user.email as string, email as string,
PRICE_ID PRICE_ID
); );
return res.status(users.status).json({ response: users.response }); return res.status(users.status).json({ response: users.response });

View File

@ -0,0 +1,18 @@
import type { NextApiRequest, NextApiResponse } from "next";
import getPublicUserById from "@/lib/api/controllers/public/users/getPublicUserById";
import { getToken } from "next-auth/jwt";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const token = await getToken({ req });
const requestingId = token?.id;
const lookupId = req.query.id as string;
// Check if "lookupId" is the user "id" or their "username"
const isId = lookupId.split("").every((e) => Number.isInteger(parseInt(e)));
if (req.method === "GET") {
const users = await getPublicUserById(lookupId, isId, requestingId);
return res.status(users.status).json({ response: users.response });
}
}

View File

@ -1,11 +1,11 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import updeteTagById from "@/lib/api/controllers/tags/tagId/updeteTagById"; import updeteTagById from "@/lib/api/controllers/tags/tagId/updeteTagById";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
import deleteTagById from "@/lib/api/controllers/tags/tagId/deleteTagById"; import deleteTagById from "@/lib/api/controllers/tags/tagId/deleteTagById";
export default async function tags(req: NextApiRequest, res: NextApiResponse) { export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
const tagId = Number(req.query.id); const tagId = Number(req.query.id);

View File

@ -1,10 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import getTags from "@/lib/api/controllers/tags/getTags"; import getTags from "@/lib/api/controllers/tags/getTags";
import authenticateUser from "@/lib/api/authenticateUser"; import verifyUser from "@/lib/api/verifyUser";
export default async function tags(req: NextApiRequest, res: NextApiResponse) { export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const user = await authenticateUser({ req, res }); const user = await verifyUser({ req, res });
if (!user) return res.status(404).json({ response: "User not found." }); if (!user) return;
if (req.method === "GET") { if (req.method === "GET") {
const tags = await getTags(user.id); const tags = await getTags(user.id);

View File

@ -1,48 +1,58 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import getUserById from "@/lib/api/controllers/users/userId/getUserById"; import getUserById from "@/lib/api/controllers/users/userId/getUserById";
import getPublicUserById from "@/lib/api/controllers/users/userId/getPublicUserById";
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById"; import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
import deleteUserById from "@/lib/api/controllers/users/userId/deleteUserById"; import deleteUserById from "@/lib/api/controllers/users/userId/deleteUserById";
import authenticateUser from "@/lib/api/authenticateUser";
import { prisma } from "@/lib/api/db";
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
import verifySubscription from "@/lib/api/verifySubscription";
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export default async function users(req: NextApiRequest, res: NextApiResponse) { export default async function users(req: NextApiRequest, res: NextApiResponse) {
const token = await getToken({ req }); const token = await getToken({ req });
const userId = token?.id; const userId = token?.id;
if (!token?.id) if (!userId) {
return res.status(400).json({ response: "Invalid parameters." }); return res.status(401).json({ response: "You must be logged in." });
const username = (await prisma.user.findUnique({ where: { id: token.id } }))
?.username;
if (!username) return res.status(404).json({ response: "User not found." });
const lookupId = req.query.id as string;
const isSelf =
userId === Number(lookupId) || username === lookupId ? true : false;
// Check if "lookupId" is the user "id" or their "username"
const isId = lookupId.split("").every((e) => Number.isInteger(parseInt(e)));
if (req.method === "GET" && !isSelf) {
const users = await getPublicUserById(lookupId, isId, username);
return res.status(users.status).json({ response: users.response });
} }
const user = await authenticateUser({ req, res }); if (userId !== Number(req.query.id))
if (!user) return res.status(404).json({ response: "User not found." }); return res.status(401).json({ response: "Permission denied." });
if (req.method === "GET") { if (req.method === "GET") {
const users = await getUserById(user.id); const users = await getUserById(userId);
return res.status(users.status).json({ response: users.response }); return res.status(users.status).json({ response: users.response });
} else if (req.method === "PUT") { }
const updated = await updateUserById(user.id, req.body);
if (STRIPE_SECRET_KEY) {
const user = await prisma.user.findUnique({
where: {
id: token.id,
},
include: {
subscriptions: true,
},
});
if (user) {
const subscribedUser = await verifySubscription(user);
if (!subscribedUser) {
return res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app if you think this is an issue.",
});
}
} else {
return res.status(404).json({ response: "User not found." });
}
}
if (req.method === "PUT") {
const updated = await updateUserById(userId, req.body);
return res.status(updated.status).json({ response: updated.response }); return res.status(updated.status).json({ response: updated.response });
} else if (req.method === "DELETE" && user.id === Number(req.query.id)) { } else if (req.method === "DELETE") {
console.log(req.body); console.log(req.body);
const updated = await deleteUserById(user.id, req.body); const updated = await deleteUserById(userId, req.body);
return res.status(updated.status).json({ response: updated.response }); return res.status(updated.status).json({ response: updated.response });
} }
} }

View File

@ -1,10 +1,3 @@
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function Index() { export default function Index() {
const router = useRouter(); return null;
useEffect(() => {
router.push("/dashboard");
}, []);
} }

View File

@ -96,7 +96,7 @@ export default function Register() {
return ( return (
<CenteredForm <CenteredForm
text={ text={
process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE process.env.NEXT_PUBLIC_STRIPE
? `Unlock ${ ? `Unlock ${
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14 process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
} days of Premium Service at no cost!` } days of Premium Service at no cost!`
@ -196,7 +196,7 @@ export default function Register() {
/> />
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? ( {process.env.NEXT_PUBLIC_STRIPE ? (
<div> <div>
<p className="text-xs text-gray-500 dark:text-gray-400"> <p className="text-xs text-gray-500 dark:text-gray-400">
By signing up, you agree to our{" "} By signing up, you agree to our{" "}

View File

@ -86,20 +86,6 @@ export default function Account() {
if (response.ok) { if (response.ok) {
toast.success("Settings Applied!"); toast.success("Settings Applied!");
// if (user.email !== account.email) {
// update({
// id: data?.user.id,
// });
// signOut();
// } else if (
// user.username !== account.username ||
// user.name !== account.name
// )
// update({
// id: data?.user.id,
// });
} else toast.error(response.data as string); } else toast.error(response.data as string);
setSubmitLoader(false); setSubmitLoader(false);
}; };
@ -190,7 +176,7 @@ export default function Account() {
<div> <div>
<p className="text-black dark:text-white mb-2">Email</p> <p className="text-black dark:text-white mb-2">Email</p>
{user.email !== account.email && {user.email !== account.email &&
process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE === "true" ? ( process.env.NEXT_PUBLIC_STRIPE === "true" ? (
<p className="text-gray-500 dark:text-gray-400 mb-2 text-sm"> <p className="text-gray-500 dark:text-gray-400 mb-2 text-sm">
Updating this field will change your billing email as well Updating this field will change your billing email as well
</p> </p>
@ -388,7 +374,7 @@ export default function Account() {
<p> <p>
This will permanently delete ALL the Links, Collections, Tags, and This will permanently delete ALL the Links, Collections, Tags, and
archived data you own.{" "} archived data you own.{" "}
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE {process.env.NEXT_PUBLIC_STRIPE
? "It will also cancel your subscription. " ? "It will also cancel your subscription. "
: undefined}{" "} : undefined}{" "}
You will be prompted to enter your password before the deletion You will be prompted to enter your password before the deletion

View File

@ -6,8 +6,7 @@ export default function Billing() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (!process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE) if (!process.env.NEXT_PUBLIC_STRIPE) router.push("/settings/profile");
router.push("/settings/profile");
}, []); }, []);
return ( return (

View File

@ -72,7 +72,7 @@ export default function Password() {
<p> <p>
This will permanently delete all the Links, Collections, Tags, and This will permanently delete all the Links, Collections, Tags, and
archived data you own. It will also log you out archived data you own. It will also log you out
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE {process.env.NEXT_PUBLIC_STRIPE
? " and cancel your subscription" ? " and cancel your subscription"
: undefined} : undefined}
. This action is irreversible! . This action is irreversible!
@ -91,7 +91,7 @@ export default function Password() {
/> />
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? ( {process.env.NEXT_PUBLIC_STRIPE ? (
<fieldset className="border rounded-md p-2 border-sky-500"> <fieldset className="border rounded-md p-2 border-sky-500">
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-sky-500"> <legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-sky-500">
<b>Optional</b>{" "} <b>Optional</b>{" "}

View File

@ -1,7 +1,6 @@
import SettingsLayout from "@/layouts/SettingsLayout"; import SettingsLayout from "@/layouts/SettingsLayout";
import { useState } from "react"; import { useState } from "react";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import SubmitButton from "@/components/SubmitButton"; import SubmitButton from "@/components/SubmitButton";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";

View File

@ -1,4 +1,4 @@
import { signOut } from "next-auth/react"; import { signOut, useSession } from "next-auth/react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -7,6 +7,7 @@ import { Plan } from "@/types/global";
export default function Subscribe() { export default function Subscribe() {
const [submitLoader, setSubmitLoader] = useState(false); const [submitLoader, setSubmitLoader] = useState(false);
const session = useSession();
const [plan, setPlan] = useState<Plan>(1); const [plan, setPlan] = useState<Plan>(1);
@ -83,7 +84,7 @@ export default function Subscribe() {
<p className="font-semibold"> <p className="font-semibold">
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"} Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
</p> </p>
<fieldset className="w-full px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700"> <fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700">
<legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl"> <legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl">
Total Total
</legend> </legend>

View File

@ -0,0 +1,19 @@
-- CreateTable
CREATE TABLE "Subscription" (
"id" SERIAL NOT NULL,
"active" BOOLEAN NOT NULL,
"stripeSubscriptionId" TEXT NOT NULL,
"currentPeriodStart" TIMESTAMP(3) NOT NULL,
"currentPeriodEnd" TIMESTAMP(3) NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_stripeSubscriptionId_key" ON "Subscription"("stripeSubscriptionId");
-- AddForeignKey
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[userId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_userId_key" ON "Subscription"("userId");

View File

@ -18,19 +18,21 @@ model User {
image String? image String?
password String password String
collections Collection[] collections Collection[]
tags Tag[] tags Tag[]
pinnedLinks Link[] pinnedLinks Link[]
collectionsJoined UsersAndCollections[]
whitelistedUsers WhitelistedUser[]
subscriptions Subscription?
archiveAsScreenshot Boolean @default(true) archiveAsScreenshot Boolean @default(true)
archiveAsPDF Boolean @default(true) archiveAsPDF Boolean @default(true)
archiveAsWaybackMachine Boolean @default(false) archiveAsWaybackMachine Boolean @default(false)
collectionsJoined UsersAndCollections[]
isPrivate Boolean @default(false) isPrivate Boolean @default(false)
whitelistedUsers WhitelistedUser[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now()) updatedAt DateTime @updatedAt @default(now())
} }
@ -127,3 +129,17 @@ model Tag {
@@unique([name, ownerId]) @@unique([name, ownerId])
} }
model Subscription {
id Int @id @default(autoincrement())
active Boolean
stripeSubscriptionId String @unique
currentPeriodStart DateTime
currentPeriodEnd DateTime
user User @relation(fields: [userId], references: [id])
userId Int @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
}

View File

@ -2,6 +2,11 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html,
body {
scroll-behavior: smooth;
}
/* Hide scrollbar */ /* Hide scrollbar */
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none; display: none;

View File

@ -17,11 +17,11 @@ declare global {
BUCKET_NAME?: string; BUCKET_NAME?: string;
SPACES_REGION?: string; SPACES_REGION?: string;
NEXT_PUBLIC_EMAIL_PROVIDER?: true; NEXT_PUBLIC_EMAIL_PROVIDER?: string;
EMAIL_FROM?: string; EMAIL_FROM?: string;
EMAIL_SERVER?: string; EMAIL_SERVER?: string;
NEXT_PUBLIC_STRIPE_IS_ACTIVE?: string; NEXT_PUBLIC_STRIPE?: string;
STRIPE_SECRET_KEY?: string; STRIPE_SECRET_KEY?: string;
MONTHLY_PRICE_ID?: string; MONTHLY_PRICE_ID?: string;
YEARLY_PRICE_ID?: string; YEARLY_PRICE_ID?: string;

View File

@ -40,6 +40,9 @@ export interface CollectionIncludingMembersAndLinkCount
export interface AccountSettings extends User { export interface AccountSettings extends User {
newPassword?: string; newPassword?: string;
whitelistedUsers: string[]; whitelistedUsers: string[];
subscription?: {
active?: boolean;
};
} }
interface LinksIncludingTags extends Link { interface LinksIncludingTags extends Link {

View File

@ -5,7 +5,6 @@ declare module "next-auth" {
interface Session { interface Session {
user: { user: {
id: number; id: number;
isSubscriber: boolean;
}; };
} }
@ -16,9 +15,8 @@ declare module "next-auth" {
declare module "next-auth/jwt" { declare module "next-auth/jwt" {
interface JWT { interface JWT {
sub: string; sub?: number;
id: number; id: number;
isSubscriber: boolean;
iat: number; iat: number;
exp: number; exp: number;
jti: string; jti: string;