Revert "undo commit"

This reverts commit 9103f67db5.
This commit is contained in:
daniel31x13
2024-11-03 03:27:52 -05:00
parent e37702aa14
commit dbd096ab76
176 changed files with 2362 additions and 9401 deletions
+11 -115
View File
@@ -11,7 +11,6 @@ import fs from "fs";
import verifyToken from "@/lib/api/verifyToken";
import generatePreview from "@/lib/api/generatePreview";
import createFolder from "@/lib/api/storage/createFolder";
import { UploadFileSchema } from "@/lib/shared/schemaValidation";
export const config = {
api: {
@@ -106,6 +105,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
response: "Collection is not accessible.",
});
// await uploadHandler(linkId, )
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER || 30000);
const numberOfLinksTheUserHas = await prisma.link.count({
@@ -118,7 +119,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
return res.status(400).json({
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
response:
"Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.",
});
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
@@ -139,20 +141,6 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
"image/jpeg",
];
const dataValidation = UploadFileSchema.safeParse({
id: Number(req.query.linkId),
format: Number(req.query.format),
file: files.file,
});
if (!dataValidation.success) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
if (
err ||
!files.file ||
@@ -178,12 +166,8 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
where: { id: linkId },
});
const { mimetype } = files.file[0];
const isPDF = mimetype?.includes("pdf");
const isImage = mimetype?.includes("image");
if (linkStillExists && isImage) {
const collectionId = collectionPermissions.id;
if (linkStillExists && files.file[0].mimetype?.includes("image")) {
const collectionId = collectionPermissions.id as number;
createFolder({
filePath: `archives/preview/${collectionId}`,
});
@@ -200,11 +184,13 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
await prisma.link.update({
where: { id: linkId },
data: {
preview: isPDF ? "unavailable" : undefined,
image: isImage
preview: files.file[0].mimetype?.includes("pdf")
? "unavailable"
: undefined,
image: files.file[0].mimetype?.includes("image")
? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
pdf: isPDF
pdf: files.file[0].mimetype?.includes("pdf")
? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
lastPreserved: new Date().toISOString(),
@@ -220,94 +206,4 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
});
});
}
// To update the link preview
else if (req.method === "PUT") {
if (process.env.NEXT_PUBLIC_DEMO === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const user = await verifyUser({ req, res });
if (!user) return;
const collectionPermissions = await getPermission({
userId: user.id,
linkId,
});
if (!collectionPermissions)
return res.status(400).json({
response: "Collection is not accessible.",
});
const memberHasAccess = collectionPermissions.members.some(
(e: UsersAndCollections) => e.userId === user.id && e.canCreate
);
if (!(collectionPermissions.ownerId === user.id || memberHasAccess))
return res.status(400).json({
response: "Collection is not accessible.",
});
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
);
const form = formidable({
maxFields: 1,
maxFiles: 1,
maxFileSize: NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024,
});
form.parse(req, async (err, fields, files) => {
const allowedMIMETypes = ["image/png", "image/jpg", "image/jpeg"];
if (
err ||
!files.file ||
!files.file[0] ||
!allowedMIMETypes.includes(files.file[0].mimetype || "")
) {
// Handle parsing error
return res.status(400).json({
response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
});
} else {
const fileBuffer = fs.readFileSync(files.file[0].filepath);
if (
Buffer.byteLength(fileBuffer) >
1024 * 1024 * Number(NEXT_PUBLIC_MAX_FILE_BUFFER)
)
return res.status(400).json({
response: `Sorry, we couldn't process your file. Please ensure it's a PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
});
const linkStillExists = await prisma.link.update({
where: { id: linkId },
data: {
updatedAt: new Date(),
},
});
if (linkStillExists) {
const collectionId = collectionPermissions.id;
createFolder({
filePath: `archives/preview/${collectionId}`,
});
await generatePreview(fileBuffer, collectionId, linkId);
}
fs.unlinkSync(files.file[0].filepath);
if (linkStillExists)
return res.status(200).json({
response: linkStillExists,
});
else return res.status(400).json({ response: "Link not found." });
}
});
}
}
+4 -102
View File
@@ -1,11 +1,9 @@
import { prisma } from "@/lib/api/db";
import sendInvitationRequest from "@/lib/api/sendInvitationRequest";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import updateSeats from "@/lib/api/stripe/updateSeats";
import verifySubscription from "@/lib/api/stripe/verifySubscription";
import verifySubscription from "@/lib/api/verifySubscription";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { User } from "@prisma/client";
import bcrypt from "bcrypt";
import { randomBytes } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { Adapter } from "next-auth/adapters";
import NextAuth from "next-auth/next";
@@ -135,7 +133,6 @@ if (process.env.NEXT_PUBLIC_CREDENTIALS_ENABLED !== "false") {
if (emailEnabled) {
providers.push(
EmailProvider({
id: "email",
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
maxAge: 1200,
@@ -160,56 +157,6 @@ if (emailEnabled) {
token,
});
},
}),
EmailProvider({
id: "invite",
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
maxAge: 1200,
async sendVerificationRequest({ identifier, url, provider, token }) {
const parentSubscriptionEmail = (
await prisma.user.findFirst({
where: {
email: identifier,
emailVerified: null,
},
include: {
parentSubscription: {
include: {
user: {
select: {
email: true,
},
},
},
},
},
})
)?.parentSubscription?.user.email;
if (!parentSubscriptionEmail) throw Error("Invalid email.");
const recentVerificationRequestsCount =
await prisma.verificationToken.count({
where: {
identifier,
createdAt: {
gt: new Date(new Date().getTime() - 1000 * 60 * 5), // 5 minutes
},
},
});
if (recentVerificationRequestsCount >= 4)
throw Error("Too many requests. Please try again later.");
sendInvitationRequest({
parentSubscriptionEmail,
identifier,
url,
from: provider.from as string,
token,
});
},
})
);
}
@@ -1232,52 +1179,6 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
},
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
if (
!(user as User).emailVerified &&
!email?.verificationRequest
// && (account?.provider === "email" || account?.provider === "google")
) {
// Email is being verified for the first time...
console.log("Email is being verified for the first time...");
const parentSubscriptionId = (user as User).parentSubscriptionId;
if (parentSubscriptionId) {
// Add seat request to Stripe
const parentSubscription = await prisma.subscription.findFirst({
where: {
id: parentSubscriptionId,
},
});
// Count child users with verified email under a specific subscription, excluding the current user
const verifiedChildUsersCount = await prisma.user.count({
where: {
parentSubscriptionId: parentSubscriptionId,
id: {
not: user.id as number,
},
emailVerified: {
not: null,
},
},
});
if (
STRIPE_SECRET_KEY &&
parentSubscription?.quantity &&
verifiedChildUsersCount + 2 > // add current user and the admin
parentSubscription.quantity
) {
// Add seat if the user count exceeds the subscription limit
await updateSeats(
parentSubscription.stripeSubscriptionId,
verifiedChildUsersCount + 2
);
}
}
}
if (account?.provider !== "credentials") {
// registration via SSO can be separately disabled
const existingUser = await prisma.account.findFirst({
@@ -1386,6 +1287,8 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
async session({ session, token }) {
session.user.id = token.id;
console.log("session", session);
if (STRIPE_SECRET_KEY) {
const user = await prisma.user.findUnique({
where: {
@@ -1393,7 +1296,6 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
},
include: {
subscriptions: true,
parentSubscription: true,
},
});
+5 -10
View File
@@ -1,6 +1,5 @@
import { prisma } from "@/lib/api/db";
import sendPasswordResetRequest from "@/lib/api/sendPasswordResetRequest";
import { ForgotPasswordSchema } from "@/lib/shared/schemaValidation";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function forgotPassword(
@@ -14,18 +13,14 @@ export default async function forgotPassword(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const dataValidation = ForgotPasswordSchema.safeParse(req.body);
const email = req.body.email;
if (!dataValidation.success) {
if (!email) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
response: "Invalid email.",
});
}
const { email } = dataValidation.data;
const recentPasswordRequestsCount = await prisma.passwordResetToken.count({
where: {
identifier: email,
@@ -50,11 +45,11 @@ export default async function forgotPassword(
if (!user || !user.email) {
return res.status(400).json({
response: "No user found with that email.",
response: "Invalid email.",
});
}
sendPasswordResetRequest(user.email, user.name || "Linkwarden User");
sendPasswordResetRequest(user.email, user.name);
return res.status(200).json({
response: "Password reset email sent.",
+9 -7
View File
@@ -1,7 +1,6 @@
import { prisma } from "@/lib/api/db";
import type { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcrypt";
import { ResetPasswordSchema } from "@/lib/shared/schemaValidation";
export default async function resetPassword(
req: NextApiRequest,
@@ -14,17 +13,20 @@ export default async function resetPassword(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const dataValidation = ResetPasswordSchema.safeParse(req.body);
const token = req.body.token;
const password = req.body.password;
if (!dataValidation.success) {
if (!password || password.length < 8) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
response: "Password must be at least 8 characters.",
});
}
const { token, password } = dataValidation.data;
if (!token || typeof token !== "string") {
return res.status(400).json({
response: "Invalid token.",
});
}
// Hashed password
const saltRounds = 10;
+4 -9
View File
@@ -1,6 +1,5 @@
import { prisma } from "@/lib/api/db";
import updateCustomerEmail from "@/lib/api/stripe/updateCustomerEmail";
import { VerifyEmailSchema } from "@/lib/shared/schemaValidation";
import updateCustomerEmail from "@/lib/api/updateCustomerEmail";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function verifyEmail(
@@ -14,18 +13,14 @@ export default async function verifyEmail(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const dataValidation = VerifyEmailSchema.safeParse(req.query);
const token = req.query.token;
if (!dataValidation.success) {
if (!token || typeof token !== "string") {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
response: "Invalid token.",
});
}
const { token } = dataValidation.data;
// Check token in db
const verifyToken = await prisma.verificationToken.findFirst({
where: {
+21 -28
View File
@@ -2,10 +2,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "@/lib/api/db";
import verifyUser from "@/lib/api/verifyUser";
import isValidUrl from "@/lib/shared/isValidUrl";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import { moveFiles, removeFiles } from "@/lib/api/manageLinkFiles";
import { Collection, Link } from "@prisma/client";
import { removeFiles } from "@/lib/api/manageLinkFiles";
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
@@ -25,16 +23,7 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
response: "Link not found.",
});
const collectionIsAccessible = await getPermission({
userId: user.id,
collectionId: link.collectionId,
});
const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === user.id && e.canUpdate
);
if (!(collectionIsAccessible?.ownerId === user.id || memberHasAccess))
if (link.collection.ownerId !== user.id)
return res.status(401).json({
response: "Permission denied.",
});
@@ -65,20 +54,7 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
response: "Invalid URL.",
});
await prisma.link.update({
where: {
id: link.id,
},
data: {
image: null,
pdf: null,
readable: null,
monolith: null,
preview: null,
},
});
await removeFiles(link.id, link.collection.id);
await deleteArchivedFiles(link);
return res.status(200).json({
response: "Link is being archived.",
@@ -96,3 +72,20 @@ const getTimezoneDifferenceInMinutes = (future: Date, past: Date) => {
return diffInMinutes;
};
const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
await prisma.link.update({
where: {
id: link.id,
},
data: {
image: null,
pdf: null,
readable: null,
monolith: null,
preview: null,
},
});
await removeFiles(link.id, link.collection.id);
};
-1
View File
@@ -60,7 +60,6 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
req.body.removePreviousTags,
req.body.newData
);
return res.status(updated.status).json({
response: updated.response,
});
@@ -1,42 +0,0 @@
import getTags from "@/lib/api/controllers/tags/getTags";
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery } from "@/types/global";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "GET") {
// Convert the type of the request query to "LinkRequestQuery"
const convertedData: Omit<LinkRequestQuery, "tagId"> = {
sort: Number(req.query.sort as string),
collectionId: req.query.collectionId
? Number(req.query.collectionId as string)
: undefined,
};
if (!convertedData.collectionId) {
return res
.status(400)
.json({ response: "Please choose a valid collection." });
}
const collection = await prisma.collection.findFirst({
where: {
id: convertedData.collectionId,
isPublic: true,
},
});
if (!collection) {
return res.status(404).json({ response: "Collection not found." });
}
const tags = await getTags({
collectionId: collection.id,
});
return res.status(tags.status).json({ response: tags.response });
}
}
+1 -12
View File
@@ -1,23 +1,12 @@
import type { NextApiRequest, NextApiResponse } from "next";
import verifyByCredentials from "@/lib/api/verifyByCredentials";
import createSession from "@/lib/api/controllers/session/createSession";
import { PostSessionSchema } from "@/lib/shared/schemaValidation";
export default async function session(
req: NextApiRequest,
res: NextApiResponse
) {
const dataValidation = PostSessionSchema.safeParse(req.body);
if (!dataValidation.success) {
return res.status(400).json({
response: `Error: ${
dataValidation.error.issues[0].message
} [${dataValidation.error.issues[0].path.join(", ")}]`,
});
}
const { username, password, sessionName } = dataValidation.data;
const { username, password, sessionName } = req.body;
const user = await verifyByCredentials({ username, password });
-5
View File
@@ -9,11 +9,6 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const tagId = Number(req.query.id);
if (!tagId)
return res.status(400).json({
response: "Please choose a valid name for the tag.",
});
if (req.method === "PUT") {
if (process.env.NEXT_PUBLIC_DEMO === "true")
return res.status(400).json({
+1 -3
View File
@@ -7,9 +7,7 @@ export default async function tags(req: NextApiRequest, res: NextApiResponse) {
if (!user) return;
if (req.method === "GET") {
const tags = await getTags({
userId: user.id,
});
const tags = await getTags(user.id);
return res.status(tags.status).json({ response: tags.response });
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default async function tokens(
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const token = await postToken(req.body, user.id);
const token = await postToken(JSON.parse(req.body), user.id);
return res.status(token.status).json({ response: token.response });
} else if (req.method === "GET") {
const token = await getTokens(user.id);
+6 -21
View File
@@ -3,7 +3,7 @@ import getUserById from "@/lib/api/controllers/users/userId/getUserById";
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
import deleteUserById from "@/lib/api/controllers/users/userId/deleteUserById";
import { prisma } from "@/lib/api/db";
import verifySubscription from "@/lib/api/stripe/verifySubscription";
import verifySubscription from "@/lib/api/verifySubscription";
import verifyToken from "@/lib/api/verifyToken";
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
@@ -11,12 +11,6 @@ const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const token = await verifyToken({ req });
const queryId = Number(req.query.id);
if (!queryId) {
return res.status(400).json({ response: "Invalid request." });
}
if (typeof token === "string") {
res.status(401).json({ response: token });
return null;
@@ -30,12 +24,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
const isServerAdmin = user?.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
const userId = token.id;
const userId = isServerAdmin ? Number(req.query.id) : token.id;
if (userId !== Number(req.query.id) && !isServerAdmin)
return res.status(401).json({ response: "Permission denied." });
if (req.method === "GET") {
if (userId !== queryId && !isServerAdmin)
return res.status(401).json({ response: "Permission denied." });
const users = await getUserById(userId);
return res.status(users.status).json({ response: users.response });
}
@@ -47,7 +41,6 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
},
include: {
subscriptions: true,
parentSubscription: true,
},
});
@@ -65,9 +58,6 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
}
if (req.method === "PUT") {
if (userId !== queryId && !isServerAdmin)
return res.status(401).json({ response: "Permission denied." });
if (process.env.NEXT_PUBLIC_DEMO === "true")
return res.status(400).json({
response:
@@ -83,12 +73,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
"This action is disabled because this is a read-only demo of Linkwarden.",
});
const updated = await deleteUserById(
userId,
req.body,
isServerAdmin,
queryId
);
const updated = await deleteUserById(userId, req.body, isServerAdmin);
return res.status(updated.status).json({ response: updated.response });
}
}
+3 -2
View File
@@ -16,9 +16,10 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
} else if (req.method === "GET") {
const user = await verifyUser({ req, res });
if (!user) return res.status(401).json({ response: "Unauthorized..." });
if (!user || user.id !== Number(process.env.NEXT_PUBLIC_ADMIN || 1))
return res.status(401).json({ response: "Unauthorized..." });
const response = await getUsers(user);
const response = await getUsers();
return res.status(response.status).json({ response: response.response });
}
}
-119
View File
@@ -1,119 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import handleSubscription from "@/lib/api/stripe/handleSubscription";
export const config = {
api: {
bodyParser: false,
},
};
const buffer = (req: NextApiRequest) => {
return new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => {
chunks.push(chunk);
});
req.on("end", () => {
resolve(Buffer.concat(chunks as any));
});
req.on("error", reject);
});
};
export default async function webhook(
req: NextApiRequest,
res: NextApiResponse
) {
if (process.env.NEXT_PUBLIC_DEMO === "true")
return res.status(400).json({
response:
"This action is disabled because this is a read-only demo of Linkwarden.",
});
// see if stripe is already initialized
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_WEBHOOK_SECRET) {
return res.status(400).json({
response: "This action is disabled because Stripe is not initialized.",
});
}
let event = req.body;
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2022-11-15",
});
const signature = req.headers["stripe-signature"] as any;
try {
const body = await buffer(req);
event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
} catch (err) {
console.error(err);
return res.status(400).send("Webhook signature verification failed.");
}
// Handle the event based on its type
const eventType = event.type;
const data = event.data.object;
try {
switch (eventType) {
case "customer.subscription.created":
await handleSubscription({
id: data.id,
active: data.status === "active" || data.status === "trialing",
quantity: data?.quantity ?? 1,
periodStart: data.current_period_start,
periodEnd: data.current_period_end,
});
break;
case "customer.subscription.updated":
await handleSubscription({
id: data.id,
active: data.status === "active" || data.status === "trialing",
quantity: data?.quantity ?? 1,
periodStart: data.current_period_start,
periodEnd: data.current_period_end,
});
break;
case "customer.subscription.deleted":
await handleSubscription({
id: data.id,
active: false,
quantity: data?.lines?.data[0]?.quantity ?? 1,
periodStart: data.current_period_start,
periodEnd: data.current_period_end,
});
break;
case "customer.subscription.cancelled":
await handleSubscription({
id: data.id,
active: !(data.current_period_end * 1000 < Date.now()),
quantity: data?.lines?.data[0]?.quantity ?? 1,
periodStart: data.current_period_start,
periodEnd: data.current_period_end,
});
break;
default:
console.log(`Unhandled event type ${eventType}`);
}
} catch (error) {
console.error("Error handling webhook event:", error);
return res.status(500).send("Server Error");
}
return res.status(200).json({
response: "Done!",
});
}