implemented stripe for the cloud instance
This commit is contained in:
parent
0b66e16123
commit
f82582a0bd
|
@ -15,4 +15,9 @@ SPACES_REGION=
|
||||||
# SMTP Settings (Optional)
|
# SMTP Settings (Optional)
|
||||||
NEXT_PUBLIC_EMAIL_PROVIDER=
|
NEXT_PUBLIC_EMAIL_PROVIDER=
|
||||||
EMAIL_FROM=
|
EMAIL_FROM=
|
||||||
EMAIL_SERVER=
|
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=
|
|
@ -80,7 +80,7 @@ export default function ChangePassword({
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword1(e.target.value)}
|
onChange={(e) => setNewPassword1(e.target.value)}
|
||||||
type="password"
|
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"
|
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-sky-500">Confirm New Password</p>
|
<p className="text-sm text-sky-500">Confirm New Password</p>
|
||||||
|
@ -89,7 +89,7 @@ export default function ChangePassword({
|
||||||
value={newPassword2}
|
value={newPassword2}
|
||||||
onChange={(e) => setNewPassword2(e.target.value)}
|
onChange={(e) => setNewPassword2(e.target.value)}
|
||||||
type="password"
|
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"
|
className="w-full rounded-md p-3 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ type Props = {
|
||||||
user: AccountSettings;
|
user: AccountSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||||
|
|
||||||
export default function ProfileSettings({
|
export default function ProfileSettings({
|
||||||
toggleSettingsModal,
|
toggleSettingsModal,
|
||||||
|
@ -165,7 +165,7 @@ export default function ProfileSettings({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{EmailProvider ? (
|
{emailEnabled ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-sky-500 mb-2">Email</p>
|
<p className="text-sm text-sky-500 mb-2">Email</p>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
active === "/dashboard"
|
active === "/dashboard"
|
||||||
? "bg-sky-200"
|
? "bg-sky-200"
|
||||||
: "hover:bg-slate-200 bg-gray-100"
|
: "hover:bg-slate-200 bg-gray-100"
|
||||||
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1`}
|
} outline-sky-100 outline-1 duration-100 py-1 px-2 rounded-md cursor-pointer flex justify-center flex-col items-center gap-1 w-full`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faChartSimple}
|
icon={faChartSimple}
|
||||||
|
@ -81,7 +81,9 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
icon={faLink}
|
icon={faLink}
|
||||||
className={`w-8 h-8 drop-shadow text-sky-500`}
|
className={`w-8 h-8 drop-shadow text-sky-500`}
|
||||||
/>
|
/>
|
||||||
<p className="text-sky-600 text-xs font-semibold">All Links</p>
|
<p className="text-sky-600 text-xs font-semibold">
|
||||||
|
<span className="hidden xl:inline-block">All</span> Links
|
||||||
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -11,18 +11,23 @@ interface Props {
|
||||||
|
|
||||||
export default function AuthRedirect({ children }: Props) {
|
export default function AuthRedirect({ children }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { status } = useSession();
|
const { status, data } = useSession();
|
||||||
const [redirect, setRedirect] = useState(true);
|
const [redirect, setRedirect] = useState(true);
|
||||||
|
|
||||||
useInitialData();
|
useInitialData();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.pathname.startsWith("/public")) {
|
if (!router.pathname.startsWith("/public")) {
|
||||||
if (
|
if (status === "authenticated" && data.user.isSubscriber === false) {
|
||||||
|
router.push("/subscribe").then(() => {
|
||||||
|
setRedirect(false);
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
status === "authenticated" &&
|
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 === "/forgot")
|
router.pathname === "/forgot")
|
||||||
) {
|
) {
|
||||||
router.push("/").then(() => {
|
router.push("/").then(() => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { ReactNode, useEffect } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Loader from "../components/Loader";
|
import Loader from "../components/Loader";
|
||||||
import useRedirect from "@/hooks/useRedirect";
|
import useRedirect from "@/hooks/useRedirect";
|
||||||
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MainLayout({ children }: Props) {
|
export default function MainLayout({ children }: Props) {
|
||||||
const { status } = useSession();
|
const { status, data } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const redirect = useRedirect();
|
const redirect = useRedirect();
|
||||||
const routeExists = router.route === "/_error" ? false : true;
|
const routeExists = router.route === "/_error" ? false : true;
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,10 +3,16 @@ import { AccountSettings } from "@/types/global";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import removeFile from "@/lib/api/storage/removeFile";
|
import removeFile from "@/lib/api/storage/removeFile";
|
||||||
import createFile from "@/lib/api/storage/createFile";
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
|
import updateCustomerEmail from "../../updateCustomerEmail";
|
||||||
|
|
||||||
export default async function updateUser(
|
export default async function updateUser(
|
||||||
user: AccountSettings,
|
user: AccountSettings,
|
||||||
userId: number
|
sessionUser: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
isSubscriber: boolean;
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
if (!user.username || !user.email)
|
if (!user.username || !user.email)
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +30,7 @@ export default async function updateUser(
|
||||||
const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, "");
|
const base64Data = profilePic.replace(/^data:image\/jpeg;base64,/, "");
|
||||||
|
|
||||||
await createFile({
|
await createFile({
|
||||||
filePath: `uploads/avatar/${userId}.jpg`,
|
filePath: `uploads/avatar/${sessionUser.id}.jpg`,
|
||||||
data: base64Data,
|
data: base64Data,
|
||||||
isBase64: true,
|
isBase64: true,
|
||||||
});
|
});
|
||||||
|
@ -39,7 +45,7 @@ export default async function updateUser(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (profilePic == "") {
|
} else if (profilePic == "") {
|
||||||
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
removeFile({ filePath: `uploads/avatar/${sessionUser.id}.jpg` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other settings
|
// Other settings
|
||||||
|
@ -49,7 +55,7 @@ export default async function updateUser(
|
||||||
|
|
||||||
const updatedUser = await prisma.user.update({
|
const updatedUser = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: sessionUser.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
name: user.name,
|
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 { password, ...userInfo } = updatedUser;
|
||||||
|
|
||||||
const response: Omit<AccountSettings, "password"> = {
|
const response: Omit<AccountSettings, "password"> = {
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
});
|
||||||
|
}
|
|
@ -22,6 +22,7 @@
|
||||||
"@headlessui/react": "^1.7.15",
|
"@headlessui/react": "^1.7.15",
|
||||||
"@next/font": "13.4.9",
|
"@next/font": "13.4.9",
|
||||||
"@prisma/client": "^4.16.2",
|
"@prisma/client": "^4.16.2",
|
||||||
|
"@stripe/stripe-js": "^1.54.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/node": "20.3.3",
|
"@types/node": "20.3.3",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.8",
|
||||||
|
@ -43,6 +44,7 @@
|
||||||
"react-image-file-resizer": "^0.4.8",
|
"react-image-file-resizer": "^0.4.8",
|
||||||
"react-select": "^5.7.3",
|
"react-select": "^5.7.3",
|
||||||
"sharp": "^0.32.1",
|
"sharp": "^0.32.1",
|
||||||
|
"stripe": "^12.13.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,11 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
if (!session?.user?.username)
|
if (!session?.user?.username)
|
||||||
return res.status(401).json({ response: "You must be logged in." });
|
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(
|
const collectionIsAccessible = await getPermission(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
|
|
|
@ -9,46 +9,45 @@ 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";
|
||||||
|
|
||||||
let email;
|
const emailEnabled =
|
||||||
|
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||||
|
|
||||||
const providers: Provider[] = [
|
const providers: Provider[] = [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
type: "credentials",
|
type: "credentials",
|
||||||
credentials: {
|
credentials: {},
|
||||||
username: {
|
|
||||||
label: "Username",
|
|
||||||
type: "text",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
label: "Password",
|
|
||||||
type: "password",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async authorize(credentials, req) {
|
async authorize(credentials, req) {
|
||||||
if (!credentials) return null;
|
if (!credentials) return null;
|
||||||
|
|
||||||
|
const { username, password } = credentials as {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
const findUser = await prisma.user.findFirst({
|
const findUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: emailEnabled
|
||||||
OR: [
|
? {
|
||||||
{
|
OR: [
|
||||||
username: credentials.username.toLowerCase(),
|
{
|
||||||
|
username: username.toLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: username?.toLowerCase(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
emailVerified: { not: null },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
username: username.toLowerCase(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
email: credentials.username.toLowerCase(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
emailVerified: { not: null },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let passwordMatches: boolean = false;
|
let passwordMatches: boolean = false;
|
||||||
|
|
||||||
if (findUser?.password) {
|
if (findUser?.password) {
|
||||||
passwordMatches = bcrypt.compareSync(
|
passwordMatches = bcrypt.compareSync(password, findUser.password);
|
||||||
credentials.password,
|
|
||||||
findUser.password
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordMatches) {
|
if (passwordMatches) {
|
||||||
|
@ -58,14 +57,13 @@ const providers: Provider[] = [
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.EMAIL_SERVER && process.env.EMAIL_FROM)
|
if (emailEnabled)
|
||||||
providers.push(
|
providers.push(
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
server: process.env.EMAIL_SERVER,
|
server: process.env.EMAIL_SERVER,
|
||||||
from: process.env.EMAIL_FROM,
|
from: process.env.EMAIL_FROM,
|
||||||
maxAge: 600,
|
maxAge: 1200,
|
||||||
sendVerificationRequest(params) {
|
sendVerificationRequest(params) {
|
||||||
email = params.identifier;
|
|
||||||
sendVerificationRequest(params);
|
sendVerificationRequest(params);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -75,6 +73,7 @@ export const authOptions: AuthOptions = {
|
||||||
adapter: PrismaAdapter(prisma) as Adapter,
|
adapter: PrismaAdapter(prisma) as Adapter,
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
},
|
},
|
||||||
providers,
|
providers,
|
||||||
pages: {
|
pages: {
|
||||||
|
@ -85,11 +84,43 @@ export const authOptions: AuthOptions = {
|
||||||
session: async ({ session, token }: { session: Session; token: JWT }) => {
|
session: async ({ session, token }: { session: Session; token: JWT }) => {
|
||||||
session.user.id = parseInt(token.id as string);
|
session.user.id = parseInt(token.id as string);
|
||||||
session.user.username = token.username as string;
|
session.user.username = token.username as string;
|
||||||
|
session.user.isSubscriber = token.isSubscriber as boolean;
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
// Using the `...rest` parameter to be able to narrow down the type based on `trigger`
|
// 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") {
|
if (trigger === "signIn") {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
token.username = (user as any).username;
|
token.username = (user as any).username;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
const EmailProvider =
|
const emailEnabled =
|
||||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
|
@ -22,7 +22,7 @@ export default async function Index(
|
||||||
) {
|
) {
|
||||||
const body: User = req.body;
|
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 || !body.email
|
||||||
: !body.username || !body.password || !body.name;
|
: !body.username || !body.password || !body.name;
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ export default async function Index(
|
||||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
|
||||||
|
|
||||||
// Remove user's who aren't verified for more than 10 minutes
|
// Remove user's who aren't verified for more than 10 minutes
|
||||||
if (EmailProvider)
|
if (emailEnabled)
|
||||||
await prisma.user.deleteMany({
|
await prisma.user.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
|
@ -53,10 +53,12 @@ export default async function Index(
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkIfUserExists = await prisma.user.findFirst({
|
const checkIfUserExists = await prisma.user.findFirst({
|
||||||
where: EmailProvider
|
where: emailEnabled
|
||||||
? {
|
? {
|
||||||
OR: [
|
OR: [
|
||||||
{ username: body.username.toLowerCase() },
|
{
|
||||||
|
username: body.username.toLowerCase(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
email: body.email?.toLowerCase(),
|
email: body.email?.toLowerCase(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,17 +11,22 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const userName = session?.user.username?.toLowerCase();
|
const userName = session?.user.username?.toLowerCase();
|
||||||
const queryId = Number(req.query.id);
|
const queryId = Number(req.query.id);
|
||||||
|
|
||||||
if (!queryId)
|
|
||||||
return res
|
|
||||||
.setHeader("Content-Type", "text/plain")
|
|
||||||
.status(401)
|
|
||||||
.send("Invalid parameters.");
|
|
||||||
|
|
||||||
if (!userId || !userName)
|
if (!userId || !userName)
|
||||||
return res
|
return res
|
||||||
.setHeader("Content-Type", "text/plain")
|
.setHeader("Content-Type", "text/plain")
|
||||||
.status(401)
|
.status(401)
|
||||||
.send("You must be logged in.");
|
.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) {
|
if (userId !== queryId) {
|
||||||
const targetUser = await prisma.user.findUnique({
|
const targetUser = await prisma.user.findUnique({
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/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 getCollections from "@/lib/api/controllers/collections/getCollections";
|
||||||
import postCollection from "@/lib/api/controllers/collections/postCollection";
|
import postCollection from "@/lib/api/controllers/collections/postCollection";
|
||||||
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
|
import updateCollection from "@/lib/api/controllers/collections/updateCollection";
|
||||||
|
@ -14,7 +14,11 @@ export default async function collections(
|
||||||
|
|
||||||
if (!session?.user?.username) {
|
if (!session?.user?.username) {
|
||||||
return res.status(401).json({ response: "You must be logged in." });
|
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") {
|
if (req.method === "GET") {
|
||||||
const collections = await getCollections(session.user.id);
|
const collections = await getCollections(session.user.id);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/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 getLinks from "@/lib/api/controllers/links/getLinks";
|
||||||
import postLink from "@/lib/api/controllers/links/postLink";
|
import postLink from "@/lib/api/controllers/links/postLink";
|
||||||
import deleteLink from "@/lib/api/controllers/links/deleteLink";
|
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) {
|
if (!session?.user?.username) {
|
||||||
return res.status(401).json({ response: "You must be logged in." });
|
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") {
|
if (req.method === "GET") {
|
||||||
const links = await getLinks(session.user.id, req?.query?.body as string);
|
const links = await getLinks(session.user.id, req?.query?.body as string);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/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";
|
import getTags from "@/lib/api/controllers/tags/getTags";
|
||||||
|
|
||||||
export default async function tags(req: NextApiRequest, res: NextApiResponse) {
|
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) {
|
if (!session?.user?.username) {
|
||||||
return res.status(401).json({ response: "You must be logged in." });
|
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") {
|
if (req.method === "GET") {
|
||||||
const tags = await getTags(session.user.id);
|
const tags = await getTags(session.user.id);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { getServerSession } from "next-auth/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 getUsers from "@/lib/api/controllers/users/getUsers";
|
||||||
import updateUser from "@/lib/api/controllers/users/updateUser";
|
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) {
|
if (!session?.user.username) {
|
||||||
return res.status(401).json({ response: "You must be logged in." });
|
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 lookupUsername = req.query.username as string;
|
||||||
const isSelf = session.user.username === lookupUsername ? true : false;
|
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);
|
const users = await getUsers(lookupUsername, isSelf, session.user.username);
|
||||||
return res.status(users.status).json({ response: users.response });
|
return res.status(users.status).json({ response: users.response });
|
||||||
} else if (req.method === "PUT" && !req.body.password) {
|
} 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 });
|
return res.status(updated.status).json({ response: updated.response });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default function Forgot() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-5 mt-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
<div className="p-2 mt-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
||||||
<Image
|
<Image
|
||||||
src="/linkwarden.png"
|
src="/linkwarden.png"
|
||||||
|
@ -49,7 +49,7 @@ export default function Forgot() {
|
||||||
className="h-12 w-fit"
|
className="h-12 w-fit"
|
||||||
/>
|
/>
|
||||||
<div className="text-center sm:text-right">
|
<div className="text-center sm:text-right">
|
||||||
<p className="text-3xl font-bold text-sky-500">Password Reset</p>
|
<p className="text-3xl text-sky-500">Password Reset</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface FormData {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
@ -46,7 +46,7 @@ export default function Login() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-5 my-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
<div className="p-2 my-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
||||||
<div className="text-right flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
<div className="text-right flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
||||||
<Image
|
<Image
|
||||||
src="/linkwarden.png"
|
src="/linkwarden.png"
|
||||||
|
@ -56,7 +56,7 @@ export default function Login() {
|
||||||
className="h-12 w-fit"
|
className="h-12 w-fit"
|
||||||
/>
|
/>
|
||||||
<div className="text-center sm:text-right">
|
<div className="text-center sm:text-right">
|
||||||
<p className="text-3xl font-bold text-sky-500">Welcome back</p>
|
<p className="text-3xl text-sky-500">Welcome back</p>
|
||||||
<p className="text-md font-semibold text-sky-400">
|
<p className="text-md font-semibold text-sky-400">
|
||||||
Sign in to your account
|
Sign in to your account
|
||||||
</p>
|
</p>
|
||||||
|
@ -66,7 +66,7 @@ export default function Login() {
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
||||||
Username
|
Username
|
||||||
{EmailProvider ? "/Email" : undefined}
|
{emailEnabled ? "/Email" : undefined}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
@ -85,12 +85,12 @@ export default function Login() {
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="***********"
|
placeholder="••••••••••••••"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => 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"
|
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 && (
|
||||||
<div className="w-fit ml-auto mt-1">
|
<div className="w-fit ml-auto mt-1">
|
||||||
<Link href={"/forgot"} className="text-gray-500 font-semibold">
|
<Link href={"/forgot"} className="text-gray-500 font-semibold">
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
|
|
|
@ -5,7 +5,7 @@ import SubmitButton from "@/components/SubmitButton";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const EmailProvider = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -21,14 +21,14 @@ export default function Register() {
|
||||||
const [form, setForm] = useState<FormData>({
|
const [form, setForm] = useState<FormData>({
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
email: EmailProvider ? "" : undefined,
|
email: emailEnabled ? "" : undefined,
|
||||||
password: "",
|
password: "",
|
||||||
passwordConfirmation: "",
|
passwordConfirmation: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
async function registerUser() {
|
async function registerUser() {
|
||||||
const checkHasEmptyFields = () => {
|
const checkHasEmptyFields = () => {
|
||||||
if (EmailProvider) {
|
if (emailEnabled) {
|
||||||
return (
|
return (
|
||||||
form.name !== "" &&
|
form.name !== "" &&
|
||||||
form.username !== "" &&
|
form.username !== "" &&
|
||||||
|
@ -78,7 +78,7 @@ export default function Register() {
|
||||||
if (form.email) await sendConfirmation();
|
if (form.email) await sendConfirmation();
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
EmailProvider
|
emailEnabled
|
||||||
? "User Created! Please check you email."
|
? "User Created! Please check you email."
|
||||||
: "User Created!"
|
: "User Created!"
|
||||||
);
|
);
|
||||||
|
@ -95,7 +95,7 @@ export default function Register() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="p-5 mx-auto my-10 flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
<div className="p-2 mx-auto my-10 flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
||||||
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
||||||
<Image
|
<Image
|
||||||
src="/linkwarden.png"
|
src="/linkwarden.png"
|
||||||
|
@ -105,7 +105,7 @@ export default function Register() {
|
||||||
className="h-12 w-fit"
|
className="h-12 w-fit"
|
||||||
/>
|
/>
|
||||||
<div className="text-center sm:text-right">
|
<div className="text-center sm:text-right">
|
||||||
<p className="text-3xl font-bold text-sky-500">Get started</p>
|
<p className="text-3xl text-sky-500">Get started</p>
|
||||||
<p className="text-md font-semibold text-sky-400">
|
<p className="text-md font-semibold text-sky-400">
|
||||||
Create a new account
|
Create a new account
|
||||||
</p>
|
</p>
|
||||||
|
@ -140,7 +140,7 @@ export default function Register() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{EmailProvider ? (
|
{emailEnabled ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
||||||
Email
|
Email
|
||||||
|
@ -156,29 +156,29 @@ export default function Register() {
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<div className="flex item-center gap-5">
|
<div className="flex item-center gap-2">
|
||||||
<div>
|
<div className="w-full">
|
||||||
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
||||||
Password
|
Password
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="***********"
|
placeholder="••••••••••••••"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => 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"
|
className="w-full rounded-md p-2 mx-auto border-sky-100 border-solid border outline-none focus:border-sky-500 duration-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="w-full">
|
||||||
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
<p className="text-sm text-sky-500 w-fit font-semibold mb-1">
|
||||||
Confirm Password
|
Confirm Password
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="***********"
|
placeholder="••••••••••••••"
|
||||||
value={form.passwordConfirmation}
|
value={form.passwordConfirmation}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, passwordConfirmation: e.target.value })
|
setForm({ ...form, passwordConfirmation: e.target.value })
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="p-2 mt-10 mx-auto flex flex-col gap-3 justify-between sm:w-[28rem] w-80 bg-slate-50 rounded-md border border-sky-100">
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row justify-between items-center mb-5">
|
||||||
|
<Image
|
||||||
|
src="/linkwarden.png"
|
||||||
|
width={1694}
|
||||||
|
height={483}
|
||||||
|
alt="Linkwarden"
|
||||||
|
className="h-12 w-fit"
|
||||||
|
/>
|
||||||
|
<div className="text-center sm:text-right">
|
||||||
|
<p className="text-3xl text-sky-500">14 days free trial</p>
|
||||||
|
<p className="text-md font-semibold text-sky-400">
|
||||||
|
Then $5/month afterwards
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-md text-gray-500 mt-1">
|
||||||
|
You will be redirected to Stripe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubmitButton
|
||||||
|
onClick={loginUser}
|
||||||
|
label="Complete your Subscription"
|
||||||
|
className="mt-2 w-full text-center"
|
||||||
|
loading={submitLoader}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => signOut()}
|
||||||
|
className="w-fit mx-auto cursor-pointer text-gray-500 font-semibold "
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -35,12 +35,12 @@ model Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
|
|
||||||
username String @unique
|
username String @unique
|
||||||
|
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime?
|
emailVerified DateTime?
|
||||||
image String?
|
image String?
|
||||||
|
|
||||||
|
@ -60,7 +60,6 @@ model User {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
identifier String
|
identifier String
|
||||||
token String @unique
|
token String @unique
|
||||||
|
@ -69,7 +68,6 @@ model VerificationToken {
|
||||||
@@unique([identifier, token])
|
@@unique([identifier, token])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model Collection {
|
model Collection {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
|
|
|
@ -16,6 +16,10 @@ declare global {
|
||||||
NEXT_PUBLIC_EMAIL_PROVIDER?: true;
|
NEXT_PUBLIC_EMAIL_PROVIDER?: true;
|
||||||
EMAIL_FROM?: string;
|
EMAIL_FROM?: string;
|
||||||
EMAIL_SERVER?: string;
|
EMAIL_SERVER?: string;
|
||||||
|
|
||||||
|
STRIPE_SECRET_KEY?: string;
|
||||||
|
PRICE_ID?: string;
|
||||||
|
STRIPE_BILLING_PORTAL_URL?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ declare module "next-auth" {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
isSubscriber: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1470,6 +1470,11 @@
|
||||||
"@smithy/types" "^1.1.0"
|
"@smithy/types" "^1.1.0"
|
||||||
tslib "^2.5.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":
|
"@swc/helpers@0.4.14":
|
||||||
version "0.4.14"
|
version "0.4.14"
|
||||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.3.tgz#329842940042d2b280897150e023e604d11657d6"
|
||||||
integrity sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==
|
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":
|
"@types/nodemailer@^6.4.8":
|
||||||
version "6.4.8"
|
version "6.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.8.tgz#f06c661e9b201fc2acc3a00a0fded42ba7eaca9d"
|
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"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
|
||||||
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
|
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:
|
qs@~6.5.2:
|
||||||
version "6.5.3"
|
version "6.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
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"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
|
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:
|
strnum@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
|
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
|
||||||
|
|
Ŝarĝante…
Reference in New Issue