diff --git a/.env.sample b/.env.sample
index e6faff3..7f4e76d 100644
--- a/.env.sample
+++ b/.env.sample
@@ -15,4 +15,9 @@ SPACES_REGION=
# SMTP Settings (Optional)
NEXT_PUBLIC_EMAIL_PROVIDER=
EMAIL_FROM=
-EMAIL_SERVER=
\ No newline at end of file
+EMAIL_SERVER=
+
+# Stripe settings (You don't need these, it's for the cloud instance payments)
+STRIPE_SECRET_KEY=
+PRICE_ID=
+STRIPE_BILLING_PORTAL_URL=
\ No newline at end of file
diff --git a/components/Modal/User/ChangePassword.tsx b/components/Modal/User/ChangePassword.tsx
index 55e8c60..6237291 100644
--- a/components/Modal/User/ChangePassword.tsx
+++ b/components/Modal/User/ChangePassword.tsx
@@ -80,7 +80,7 @@ export default function ChangePassword({
value={newPassword}
onChange={(e) => setNewPassword1(e.target.value)}
type="password"
- placeholder="***********"
+ placeholder="••••••••••••••"
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
Email
-
All Links
+
+ All Links
+
{
if (!router.pathname.startsWith("/public")) {
- if (
+ if (status === "authenticated" && data.user.isSubscriber === false) {
+ router.push("/subscribe").then(() => {
+ setRedirect(false);
+ });
+ } else if (
status === "authenticated" &&
(router.pathname === "/login" ||
router.pathname === "/register" ||
router.pathname === "/confirmation" ||
+ router.pathname === "/subscribe" ||
router.pathname === "/forgot")
) {
router.push("/").then(() => {
diff --git a/layouts/MainLayout.tsx b/layouts/MainLayout.tsx
index 7bcac66..6bcc600 100644
--- a/layouts/MainLayout.tsx
+++ b/layouts/MainLayout.tsx
@@ -1,6 +1,6 @@
import Navbar from "@/components/Navbar";
import Sidebar from "@/components/Sidebar";
-import { ReactNode, useEffect } from "react";
+import { ReactNode } from "react";
import { useSession } from "next-auth/react";
import Loader from "../components/Loader";
import useRedirect from "@/hooks/useRedirect";
@@ -12,7 +12,7 @@ interface Props {
}
export default function MainLayout({ children }: Props) {
- const { status } = useSession();
+ const { status, data } = useSession();
const router = useRouter();
const redirect = useRedirect();
const routeExists = router.route === "/_error" ? false : true;
diff --git a/lib/api/checkSubscription.ts b/lib/api/checkSubscription.ts
new file mode 100644
index 0000000..4734566
--- /dev/null
+++ b/lib/api/checkSubscription.ts
@@ -0,0 +1,50 @@
+import Stripe from "stripe";
+
+export default async function checkSubscription(
+ stripeSecretKey: string,
+ email: string,
+ priceId: 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 secondsInTwoWeeks = 1209600;
+
+ subscriptionCanceledAt = subscription.canceled_at;
+
+ const isNotCanceledOrHasTime = !(
+ subscription.canceled_at &&
+ new Date() >
+ new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000)
+ );
+
+ return (
+ subscription?.items?.data?.some(
+ (subscriptionItem) => subscriptionItem?.plan?.id === priceId
+ ) && isNotCanceledOrHasTime
+ );
+ }
+ );
+
+ return (
+ customer.email?.toLowerCase() === email.toLowerCase() &&
+ hasValidSubscription
+ );
+ });
+
+ return {
+ isSubscriber,
+ subscriptionCanceledAt,
+ };
+}
diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts
index 62fcf3e..f6f8131 100644
--- a/lib/api/controllers/users/updateUser.ts
+++ b/lib/api/controllers/users/updateUser.ts
@@ -3,10 +3,16 @@ import { AccountSettings } from "@/types/global";
import bcrypt from "bcrypt";
import removeFile from "@/lib/api/storage/removeFile";
import createFile from "@/lib/api/storage/createFile";
+import updateCustomerEmail from "../../updateCustomerEmail";
export default async function updateUser(
user: AccountSettings,
- userId: number
+ sessionUser: {
+ id: number;
+ username: string;
+ email: string;
+ isSubscriber: boolean;
+ }
) {
if (!user.username || !user.email)
return {
@@ -24,7 +30,7 @@ export default async function updateUser(
const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, "");
await createFile({
- filePath: `uploads/avatar/${userId}.jpg`,
+ filePath: `uploads/avatar/${sessionUser.id}.jpg`,
data: base64Data,
isBase64: true,
});
@@ -39,7 +45,7 @@ export default async function updateUser(
};
}
} else if (profilePic == "") {
- removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
+ removeFile({ filePath: `uploads/avatar/${sessionUser.id}.jpg` });
}
// Other settings
@@ -49,7 +55,7 @@ export default async function updateUser(
const updatedUser = await prisma.user.update({
where: {
- id: userId,
+ id: sessionUser.id,
},
data: {
name: user.name,
@@ -64,6 +70,17 @@ export default async function updateUser(
},
});
+ const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
+ const PRICE_ID = process.env.PRICE_ID;
+
+ if (STRIPE_SECRET_KEY && PRICE_ID)
+ await updateCustomerEmail(
+ STRIPE_SECRET_KEY,
+ PRICE_ID,
+ sessionUser.email,
+ user.email
+ );
+
const { password, ...userInfo } = updatedUser;
const response: Omit
= {
diff --git a/lib/api/paymentCheckout.ts b/lib/api/paymentCheckout.ts
new file mode 100644
index 0000000..5cda18d
--- /dev/null
+++ b/lib/api/paymentCheckout.ts
@@ -0,0 +1,73 @@
+import Stripe from "stripe";
+import checkSubscription from "./checkSubscription";
+
+export default async function paymentCheckout(
+ stripeSecretKey: string,
+ email: string,
+ action: "register" | "login",
+ priceId: string
+) {
+ const stripe = new Stripe(stripeSecretKey, {
+ apiVersion: "2022-11-15",
+ });
+
+ // const a = await stripe.prices.retrieve("price_1NTn3PDaRUw6CJPLkw4dcwlJ");
+
+ // const listBySub = await stripe.subscriptions.list({
+ // customer: "cus_OGUzJrRea8Qbxx",
+ // });
+
+ const listByEmail = await stripe.customers.list({
+ email: email.toLowerCase(),
+ expand: ["data.subscriptions"],
+ });
+
+ const isExistingCostomer = listByEmail?.data[0]?.id || undefined;
+
+ // const hasPreviouslySubscribed = listByEmail.data.find((customer, i) => {
+ // const hasValidSubscription = customer.subscriptions?.data.some(
+ // (subscription) => {
+ // return subscription?.items?.data?.some(
+ // (subscriptionItem) => subscriptionItem?.plan?.id === priceId
+ // );
+ // }
+ // );
+
+ // return (
+ // customer.email?.toLowerCase() === email.toLowerCase() &&
+ // hasValidSubscription
+ // );
+ // });
+
+ // const previousSubscriptionId =
+ // hasPreviouslySubscribed?.subscriptions?.data[0].id;
+
+ // if (previousSubscriptionId) {
+ // console.log(previousSubscriptionId);
+ // const subscription = await stripe.subscriptions.resume(
+ // previousSubscriptionId
+ // );
+ // }
+
+ const session = await stripe.checkout.sessions.create({
+ customer: isExistingCostomer ? isExistingCostomer : undefined,
+ line_items: [
+ {
+ price: priceId,
+ quantity: 1,
+ },
+ ],
+ mode: "subscription",
+ customer_email: isExistingCostomer ? undefined : email.toLowerCase(),
+ success_url: "http://localhost:3000?session_id={CHECKOUT_SESSION_ID}",
+ cancel_url: "http://localhost:3000/login",
+ automatic_tax: {
+ enabled: true,
+ },
+ subscription_data: {
+ trial_period_days: 14,
+ },
+ });
+
+ return { response: session.url, status: 200 };
+}
diff --git a/lib/api/updateCustomerEmail.ts b/lib/api/updateCustomerEmail.ts
new file mode 100644
index 0000000..0254d88
--- /dev/null
+++ b/lib/api/updateCustomerEmail.ts
@@ -0,0 +1,47 @@
+import Stripe from "stripe";
+
+export default async function updateCustomerEmail(
+ stripeSecretKey: string,
+ priceId: string,
+ email: string,
+ newEmail: string
+) {
+ const stripe = new Stripe(stripeSecretKey, {
+ apiVersion: "2022-11-15",
+ });
+
+ const listByEmail = await stripe.customers.list({
+ email: email.toLowerCase(),
+ expand: ["data.subscriptions"],
+ });
+
+ const customer = listByEmail.data.find((customer, i) => {
+ const hasValidSubscription = customer.subscriptions?.data.some(
+ (subscription) => {
+ const secondsInTwoWeeks = 1209600;
+
+ const isNotCanceledOrHasTime = !(
+ subscription.canceled_at &&
+ new Date() >
+ new Date((subscription.canceled_at + secondsInTwoWeeks) * 1000)
+ );
+
+ return (
+ subscription?.items?.data?.some(
+ (subscriptionItem) => subscriptionItem?.plan?.id === priceId
+ ) && isNotCanceledOrHasTime
+ );
+ }
+ );
+
+ return (
+ customer.email?.toLowerCase() === email.toLowerCase() &&
+ hasValidSubscription
+ );
+ });
+
+ if (customer)
+ await stripe.customers.update(customer?.id, {
+ email: newEmail.toLowerCase(),
+ });
+}
diff --git a/package.json b/package.json
index 5e33803..cc005e9 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@headlessui/react": "^1.7.15",
"@next/font": "13.4.9",
"@prisma/client": "^4.16.2",
+ "@stripe/stripe-js": "^1.54.1",
"@types/crypto-js": "^4.1.1",
"@types/node": "20.3.3",
"@types/nodemailer": "^6.4.8",
@@ -43,6 +44,7 @@
"react-image-file-resizer": "^0.4.8",
"react-select": "^5.7.3",
"sharp": "^0.32.1",
+ "stripe": "^12.13.0",
"typescript": "4.9.4",
"zustand": "^4.3.8"
},
diff --git a/pages/api/archives/[...params].ts b/pages/api/archives/[...params].ts
index 11edb4a..bfbc4a5 100644
--- a/pages/api/archives/[...params].ts
+++ b/pages/api/archives/[...params].ts
@@ -15,6 +15,11 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!session?.user?.username)
return res.status(401).json({ response: "You must be logged in." });
+ else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
const collectionIsAccessible = await getPermission(
session.user.id,
diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts
index fc66184..1aa4ca2 100644
--- a/pages/api/auth/[...nextauth].ts
+++ b/pages/api/auth/[...nextauth].ts
@@ -9,46 +9,45 @@ import { PrismaAdapter } from "@auth/prisma-adapter";
import { Adapter } from "next-auth/adapters";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import { Provider } from "next-auth/providers";
+import checkSubscription from "@/lib/api/checkSubscription";
-let email;
+const emailEnabled =
+ process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
const providers: Provider[] = [
CredentialsProvider({
type: "credentials",
- credentials: {
- username: {
- label: "Username",
- type: "text",
- },
- password: {
- label: "Password",
- type: "password",
- },
- },
+ credentials: {},
async authorize(credentials, req) {
if (!credentials) return null;
+ const { username, password } = credentials as {
+ username: string;
+ password: string;
+ };
+
const findUser = await prisma.user.findFirst({
- where: {
- OR: [
- {
- username: credentials.username.toLowerCase(),
+ where: emailEnabled
+ ? {
+ OR: [
+ {
+ username: username.toLowerCase(),
+ },
+ {
+ email: username?.toLowerCase(),
+ },
+ ],
+ emailVerified: { not: null },
+ }
+ : {
+ username: username.toLowerCase(),
},
- {
- email: credentials.username.toLowerCase(),
- },
- ],
- emailVerified: { not: null },
- },
});
let passwordMatches: boolean = false;
if (findUser?.password) {
- passwordMatches = bcrypt.compareSync(
- credentials.password,
- findUser.password
- );
+ passwordMatches = bcrypt.compareSync(password, findUser.password);
}
if (passwordMatches) {
@@ -58,14 +57,13 @@ const providers: Provider[] = [
}),
];
-if (process.env.EMAIL_SERVER && process.env.EMAIL_FROM)
+if (emailEnabled)
providers.push(
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
- maxAge: 600,
+ maxAge: 1200,
sendVerificationRequest(params) {
- email = params.identifier;
sendVerificationRequest(params);
},
})
@@ -75,6 +73,7 @@ export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma) as Adapter,
session: {
strategy: "jwt",
+ maxAge: 30 * 24 * 60 * 60, // 30 days
},
providers,
pages: {
@@ -85,11 +84,43 @@ export const authOptions: AuthOptions = {
session: async ({ session, token }: { session: Session; token: JWT }) => {
session.user.id = parseInt(token.id as string);
session.user.username = token.username as string;
+ session.user.isSubscriber = token.isSubscriber as boolean;
return session;
},
// Using the `...rest` parameter to be able to narrow down the type based on `trigger`
- jwt({ token, trigger, session, user }) {
+ async jwt({ token, trigger, session, user }) {
+ const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
+ const PRICE_ID = process.env.PRICE_ID;
+
+ console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaa", token);
+
+ const secondsInTwoWeeks = 1209600;
+ const subscriptionIsTimesUp =
+ token.subscriptionCanceledAt &&
+ new Date() >
+ new Date(
+ ((token.subscriptionCanceledAt as number) + secondsInTwoWeeks) *
+ 1000
+ );
+
+ if (STRIPE_SECRET_KEY && PRICE_ID && (trigger || subscriptionIsTimesUp)) {
+ console.log("EXECUTED!!!");
+ const subscription = await checkSubscription(
+ STRIPE_SECRET_KEY,
+ token.email as string,
+ PRICE_ID
+ );
+
+ subscription.isSubscriber;
+
+ if (subscription.subscriptionCanceledAt) {
+ token.subscriptionCanceledAt = subscription.subscriptionCanceledAt;
+ } else token.subscriptionCanceledAt = undefined;
+
+ token.isSubscriber = subscription.isSubscriber;
+ }
+
if (trigger === "signIn") {
token.id = user.id;
token.username = (user as any).username;
diff --git a/pages/api/auth/register.ts b/pages/api/auth/register.ts
index fab6f2d..9e91bee 100644
--- a/pages/api/auth/register.ts
+++ b/pages/api/auth/register.ts
@@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
-const EmailProvider =
+const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
interface Data {
@@ -22,7 +22,7 @@ export default async function Index(
) {
const body: User = req.body;
- const checkHasEmptyFields = EmailProvider
+ const checkHasEmptyFields = emailEnabled
? !body.username || !body.password || !body.name || !body.email
: !body.username || !body.password || !body.name;
@@ -34,7 +34,7 @@ export default async function Index(
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
// Remove user's who aren't verified for more than 10 minutes
- if (EmailProvider)
+ if (emailEnabled)
await prisma.user.deleteMany({
where: {
OR: [
@@ -53,10 +53,12 @@ export default async function Index(
});
const checkIfUserExists = await prisma.user.findFirst({
- where: EmailProvider
+ where: emailEnabled
? {
OR: [
- { username: body.username.toLowerCase() },
+ {
+ username: body.username.toLowerCase(),
+ },
{
email: body.email?.toLowerCase(),
},
diff --git a/pages/api/avatar/[id].ts b/pages/api/avatar/[id].ts
index 977247b..0287229 100644
--- a/pages/api/avatar/[id].ts
+++ b/pages/api/avatar/[id].ts
@@ -11,17 +11,22 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const userName = session?.user.username?.toLowerCase();
const queryId = Number(req.query.id);
- if (!queryId)
- return res
- .setHeader("Content-Type", "text/plain")
- .status(401)
- .send("Invalid parameters.");
-
if (!userId || !userName)
return res
.setHeader("Content-Type", "text/plain")
.status(401)
.send("You must be logged in.");
+ else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
+
+ if (!queryId)
+ return res
+ .setHeader("Content-Type", "text/plain")
+ .status(401)
+ .send("Invalid parameters.");
if (userId !== queryId) {
const targetUser = await prisma.user.findUnique({
diff --git a/pages/api/payment/index.ts b/pages/api/payment/index.ts
new file mode 100644
index 0000000..66566b7
--- /dev/null
+++ b/pages/api/payment/index.ts
@@ -0,0 +1,26 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
+import paymentCheckout from "@/lib/api/paymentCheckout";
+
+export default async function users(req: NextApiRequest, res: NextApiResponse) {
+ const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
+ const PRICE_ID = process.env.PRICE_ID;
+ const session = await getServerSession(req, res, authOptions);
+
+ if (!session?.user?.username)
+ return res.status(401).json({ response: "You must be logged in." });
+ else if (!STRIPE_SECRET_KEY || !PRICE_ID) {
+ return res.status(400).json({ response: "Payment is disabled." });
+ }
+
+ if (req.method === "GET") {
+ const users = await paymentCheckout(
+ STRIPE_SECRET_KEY,
+ session?.user.email,
+ "register",
+ PRICE_ID
+ );
+ return res.status(users.status).json({ response: users.response });
+ }
+}
diff --git a/pages/api/routes/collections/index.ts b/pages/api/routes/collections/index.ts
index 4d83918..25fec73 100644
--- a/pages/api/routes/collections/index.ts
+++ b/pages/api/routes/collections/index.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "pages/api/auth/[...nextauth]";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
import getCollections from "@/lib/api/controllers/collections/getCollections";
import postCollection from "@/lib/api/controllers/collections/postCollection";
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
@@ -14,7 +14,11 @@ export default async function collections(
if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." });
- }
+ } else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
if (req.method === "GET") {
const collections = await getCollections(session.user.id);
diff --git a/pages/api/routes/links/index.ts b/pages/api/routes/links/index.ts
index 7306de8..4a226d4 100644
--- a/pages/api/routes/links/index.ts
+++ b/pages/api/routes/links/index.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "pages/api/auth/[...nextauth]";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
import getLinks from "@/lib/api/controllers/links/getLinks";
import postLink from "@/lib/api/controllers/links/postLink";
import deleteLink from "@/lib/api/controllers/links/deleteLink";
@@ -11,7 +11,11 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." });
- }
+ } else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
if (req.method === "GET") {
const links = await getLinks(session.user.id, req?.query?.body as string);
diff --git a/pages/api/routes/tags/index.ts b/pages/api/routes/tags/index.ts
index 482cd69..0df83a8 100644
--- a/pages/api/routes/tags/index.ts
+++ b/pages/api/routes/tags/index.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "pages/api/auth/[...nextauth]";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
import getTags from "@/lib/api/controllers/tags/getTags";
export default async function tags(req: NextApiRequest, res: NextApiResponse) {
@@ -8,7 +8,11 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) {
if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." });
- }
+ } else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
if (req.method === "GET") {
const tags = await getTags(session.user.id);
diff --git a/pages/api/routes/users/index.ts b/pages/api/routes/users/index.ts
index a64b630..91b2bda 100644
--- a/pages/api/routes/users/index.ts
+++ b/pages/api/routes/users/index.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "pages/api/auth/[...nextauth]";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
import getUsers from "@/lib/api/controllers/users/getUsers";
import updateUser from "@/lib/api/controllers/users/updateUser";
@@ -9,7 +9,11 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
if (!session?.user.username) {
return res.status(401).json({ response: "You must be logged in." });
- }
+ } else if (session?.user?.isSubscriber === false)
+ res.status(401).json({
+ response:
+ "You are not a subscriber, feel free to reach out to us at hello@linkwarden.app in case of any issues.",
+ });
const lookupUsername = req.query.username as string;
const isSelf = session.user.username === lookupUsername ? true : false;
@@ -18,7 +22,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
const users = await getUsers(lookupUsername, isSelf, session.user.username);
return res.status(users.status).json({ response: users.response });
} else if (req.method === "PUT" && !req.body.password) {
- const updated = await updateUser(req.body, session.user.id);
+ const updated = await updateUser(req.body, session.user);
return res.status(updated.status).json({ response: updated.response });
}
}
diff --git a/pages/forgot.tsx b/pages/forgot.tsx
index 5d5557d..2b1f23d 100644
--- a/pages/forgot.tsx
+++ b/pages/forgot.tsx
@@ -39,7 +39,7 @@ export default function Forgot() {
return (
<>
-
+
-
Password Reset
+
Password Reset
diff --git a/pages/login.tsx b/pages/login.tsx
index 24d7412..f17798c 100644
--- a/pages/login.tsx
+++ b/pages/login.tsx
@@ -10,7 +10,7 @@ interface FormData {
password: string;
}
-const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
+const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
export default function Login() {
const [submitLoader, setSubmitLoader] = useState(false);
@@ -46,7 +46,7 @@ export default function Login() {
return (
<>
-
+
-
Welcome back
+
Welcome back
Sign in to your account
@@ -66,7 +66,7 @@ export default function Login() {
Username
- {EmailProvider ? "/Email" : undefined}
+ {emailEnabled ? "/Email" : undefined}
setForm({ ...form, password: e.target.value })}
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
/>
- {EmailProvider && (
+ {emailEnabled && (
Forgot Password?
diff --git a/pages/register.tsx b/pages/register.tsx
index 60ee1fe..84a0844 100644
--- a/pages/register.tsx
+++ b/pages/register.tsx
@@ -5,7 +5,7 @@ import SubmitButton from "@/components/SubmitButton";
import { signIn } from "next-auth/react";
import Image from "next/image";
-const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
+const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
type FormData = {
name: string;
@@ -21,14 +21,14 @@ export default function Register() {
const [form, setForm] = useState
({
name: "",
username: "",
- email: EmailProvider ? "" : undefined,
+ email: emailEnabled ? "" : undefined,
password: "",
passwordConfirmation: "",
});
async function registerUser() {
const checkHasEmptyFields = () => {
- if (EmailProvider) {
+ if (emailEnabled) {
return (
form.name !== "" &&
form.username !== "" &&
@@ -78,7 +78,7 @@ export default function Register() {
if (form.email) await sendConfirmation();
toast.success(
- EmailProvider
+ emailEnabled
? "User Created! Please check you email."
: "User Created!"
);
@@ -95,7 +95,7 @@ export default function Register() {
return (
<>
-
+
-
Get started
+
Get started
Create a new account
@@ -140,7 +140,7 @@ export default function Register() {
/>
- {EmailProvider ? (
+ {emailEnabled ? (
Email
@@ -156,29 +156,29 @@ export default function Register() {
) : undefined}
-
-
+
+
-
+
Confirm Password
setForm({ ...form, passwordConfirmation: e.target.value })
diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx
new file mode 100644
index 0000000..ba1223a
--- /dev/null
+++ b/pages/subscribe.tsx
@@ -0,0 +1,72 @@
+import SubmitButton from "@/components/SubmitButton";
+import { signOut } from "next-auth/react";
+import Image from "next/image";
+import { useEffect, useState } from "react";
+import { toast } from "react-hot-toast";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/router";
+
+export default function Subscribe() {
+ const [submitLoader, setSubmitLoader] = useState(false);
+
+ const { data, status } = useSession();
+ const router = useRouter();
+
+ useEffect(() => {
+ console.log(data?.user);
+ }, [status]);
+
+ async function loginUser() {
+ setSubmitLoader(true);
+
+ const redirectionToast = toast.loading("Redirecting to Stripe...");
+
+ const res = await fetch("/api/payment");
+ const data = await res.json();
+
+ console.log(data);
+ router.push(data.response);
+ }
+
+ return (
+ <>
+
+
+
+
+
14 days free trial
+
+ Then $5/month afterwards
+
+
+
+
+
+
+ You will be redirected to Stripe.
+
+
+
+
+
+
signOut()}
+ className="w-fit mx-auto cursor-pointer text-gray-500 font-semibold "
+ >
+ Sign Out
+
+
+ >
+ );
+}
diff --git a/prisma/migrations/20230711222012_init/migration.sql b/prisma/migrations/20230715102805_init/migration.sql
similarity index 100%
rename from prisma/migrations/20230711222012_init/migration.sql
rename to prisma/migrations/20230715102805_init/migration.sql
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 13aec07..a3882aa 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -35,12 +35,12 @@ model Session {
}
model User {
- id Int @id @default(autoincrement())
+ id Int @id @default(autoincrement())
name String
username String @unique
- email String? @unique
+ email String? @unique
emailVerified DateTime?
image String?
@@ -60,7 +60,6 @@ model User {
createdAt DateTime @default(now())
}
-
model VerificationToken {
identifier String
token String @unique
@@ -69,7 +68,6 @@ model VerificationToken {
@@unique([identifier, token])
}
-
model Collection {
id Int @id @default(autoincrement())
name String
diff --git a/types/enviornment.d.ts b/types/enviornment.d.ts
index 62a004b..9b5619a 100644
--- a/types/enviornment.d.ts
+++ b/types/enviornment.d.ts
@@ -16,6 +16,10 @@ declare global {
NEXT_PUBLIC_EMAIL_PROVIDER?: true;
EMAIL_FROM?: string;
EMAIL_SERVER?: string;
+
+ STRIPE_SECRET_KEY?: string;
+ PRICE_ID?: string;
+ STRIPE_BILLING_PORTAL_URL?: string;
}
}
}
diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts
index ca9c127..6df7f6c 100644
--- a/types/next-auth.d.ts
+++ b/types/next-auth.d.ts
@@ -6,6 +6,7 @@ declare module "next-auth" {
id: number;
username: string;
email: string;
+ isSubscriber: boolean;
};
}
}
diff --git a/yarn.lock b/yarn.lock
index d6e0f86..532967f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1470,6 +1470,11 @@
"@smithy/types" "^1.1.0"
tslib "^2.5.0"
+"@stripe/stripe-js@^1.54.1":
+ version "1.54.1"
+ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.1.tgz#e298b80c2963d9e622ea355db6c35df48e08cd89"
+ integrity sha512-smEXPu1GKMcAj9g2luT16+oXfg2jAwyc68t2Dm5wdtYl3p8PqQaZEiI8tQmboaQAjgF8pIGma6byz1T1vgmpbA==
+
"@swc/helpers@0.4.14":
version "0.4.14"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
@@ -1499,6 +1504,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6"
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==
+"@types/node@>=8.1.0":
+ version "20.4.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.2.tgz#129cc9ae69f93824f92fac653eebfb4812ab4af9"
+ integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
+
"@types/nodemailer@^6.4.8":
version "6.4.8"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.8.tgz#f06c661e9b201fc2acc3a00a0fded42ba7eaca9d"
@@ -4061,6 +4071,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+qs@^6.11.0:
+ version "6.11.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
+ integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
+ dependencies:
+ side-channel "^1.0.4"
+
qs@~6.5.2:
version "6.5.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
@@ -4482,6 +4499,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+stripe@^12.13.0:
+ version "12.13.0"
+ resolved "https://registry.yarnpkg.com/stripe/-/stripe-12.13.0.tgz#7a8b5705a6f633384e901f512fe1a834277f3123"
+ integrity sha512-mn7CxL71FCRWkQp33jcJ7+xfRF7HGzPYZlq2c87U+6kxL1qd7f/N3S1g1E5uaSWe83V5v3jN/IiWqg9y8+kWRw==
+ dependencies:
+ "@types/node" ">=8.1.0"
+ qs "^6.11.0"
+
strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"