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" />

Confirm New Password

@@ -89,7 +89,7 @@ export default function ChangePassword({ value={newPassword2} onChange={(e) => setNewPassword2(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" /> diff --git a/components/Modal/User/ProfileSettings.tsx b/components/Modal/User/ProfileSettings.tsx index 19d364f..2a705db 100644 --- a/components/Modal/User/ProfileSettings.tsx +++ b/components/Modal/User/ProfileSettings.tsx @@ -16,7 +16,7 @@ type Props = { user: AccountSettings; }; -const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; +const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER; export default function ProfileSettings({ toggleSettingsModal, @@ -165,7 +165,7 @@ export default function ProfileSettings({ /> - {EmailProvider ? ( + {emailEnabled ? (

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} -
-
+
+

Password

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" />
-
+

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 ( + <> +
+
+ Linkwarden +
+

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"