Accepted incoming changes
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { prisma } from "@/lib/api/db";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import bcrypt from "bcrypt";
|
||||
import isServerAdmin from "../../isServerAdmin";
|
||||
import { PostUserSchema } from "@/lib/shared/schemaValidation";
|
||||
import isAuthenticatedRequest from "../../isAuthenticatedRequest";
|
||||
import { Subscription, User } from "@prisma/client";
|
||||
|
||||
const emailEnabled =
|
||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||
@@ -12,66 +14,59 @@ interface Data {
|
||||
status: number;
|
||||
}
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export default async function postUser(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
): Promise<Data> {
|
||||
let isAdmin = await isServerAdmin({ req });
|
||||
const parentUser = await isAuthenticatedRequest({ req });
|
||||
const isAdmin =
|
||||
parentUser && parentUser.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
||||
|
||||
if (process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" && !isAdmin) {
|
||||
return { response: "Registration is disabled.", status: 400 };
|
||||
}
|
||||
|
||||
const body: User = req.body;
|
||||
const dataValidation = PostUserSchema().safeParse(req.body);
|
||||
|
||||
const checkHasEmptyFields = emailEnabled
|
||||
? !body.password || !body.name || !body.email
|
||||
: !body.username || !body.password || !body.name;
|
||||
if (!dataValidation.success) {
|
||||
return {
|
||||
response: `Error: ${
|
||||
dataValidation.error.issues[0].message
|
||||
} [${dataValidation.error.issues[0].path.join(", ")}]`,
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
|
||||
if (!body.password || body.password.length < 8)
|
||||
return { response: "Password must be at least 8 characters.", status: 400 };
|
||||
const { name, email, password, invite } = dataValidation.data;
|
||||
let { username } = dataValidation.data;
|
||||
|
||||
if (checkHasEmptyFields)
|
||||
return { response: "Please fill out all the fields.", status: 400 };
|
||||
|
||||
// Check email (if enabled)
|
||||
const checkEmail =
|
||||
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
|
||||
if (emailEnabled && !checkEmail.test(body.email?.toLowerCase() || ""))
|
||||
return { response: "Please enter a valid email.", status: 400 };
|
||||
|
||||
// Check username (if email was disabled)
|
||||
const checkUsername = RegExp("^[a-z0-9_-]{3,31}$");
|
||||
if (invite && (!stripeEnabled || !emailEnabled)) {
|
||||
return { response: "You are not authorized to invite users.", status: 401 };
|
||||
} else if (invite && !parentUser) {
|
||||
return { response: "You must be logged in to invite users.", status: 401 };
|
||||
}
|
||||
|
||||
const autoGeneratedUsername = "user" + Math.round(Math.random() * 1000000000);
|
||||
|
||||
if (body.username && !checkUsername.test(body.username?.toLowerCase()))
|
||||
if (!username) {
|
||||
username = autoGeneratedUsername;
|
||||
}
|
||||
|
||||
if (!emailEnabled && !password) {
|
||||
return {
|
||||
response:
|
||||
"Username has to be between 3-30 characters, no spaces and special characters are allowed.",
|
||||
response: "Password is required.",
|
||||
status: 400,
|
||||
};
|
||||
else if (!body.username) {
|
||||
body.username = autoGeneratedUsername;
|
||||
}
|
||||
|
||||
const checkIfUserExists = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
email: body.email ? body.email.toLowerCase().trim() : undefined,
|
||||
email: email ? email.toLowerCase().trim() : undefined,
|
||||
},
|
||||
{
|
||||
username: body.username
|
||||
? body.username.toLowerCase().trim()
|
||||
: undefined,
|
||||
username: username ? username.toLowerCase().trim() : undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -83,65 +78,57 @@ export default async function postUser(
|
||||
|
||||
const saltRounds = 10;
|
||||
|
||||
const hashedPassword = bcrypt.hashSync(body.password, saltRounds);
|
||||
const hashedPassword = bcrypt.hashSync(password || "", saltRounds);
|
||||
|
||||
// Subscription dates
|
||||
const currentPeriodStart = new Date();
|
||||
const currentPeriodEnd = new Date();
|
||||
currentPeriodEnd.setFullYear(currentPeriodEnd.getFullYear() + 1000); // end date is in 1000 years...
|
||||
|
||||
if (isAdmin) {
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
username: emailEnabled
|
||||
? (body.username as string).toLowerCase().trim() ||
|
||||
autoGeneratedUsername
|
||||
: (body.username as string).toLowerCase().trim(),
|
||||
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
|
||||
password: hashedPassword,
|
||||
emailVerified: new Date(),
|
||||
subscriptions: stripeEnabled
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
name: name,
|
||||
username: emailEnabled ? username || autoGeneratedUsername : username,
|
||||
email: emailEnabled ? email : undefined,
|
||||
emailVerified: isAdmin ? new Date() : undefined,
|
||||
password: password ? hashedPassword : undefined,
|
||||
parentSubscription:
|
||||
parentUser && invite
|
||||
? {
|
||||
connect: {
|
||||
id: (parentUser.subscriptions as Subscription).id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
subscriptions:
|
||||
stripeEnabled && isAdmin
|
||||
? {
|
||||
create: {
|
||||
stripeSubscriptionId:
|
||||
"fake_sub_" + Math.round(Math.random() * 10000000000000),
|
||||
active: true,
|
||||
currentPeriodStart,
|
||||
currentPeriodEnd,
|
||||
currentPeriodStart: new Date(),
|
||||
currentPeriodEnd: new Date(
|
||||
new Date().setFullYear(new Date().getFullYear() + 1000)
|
||||
), // 1000 years from now
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
subscriptions: {
|
||||
select: {
|
||||
active: true,
|
||||
},
|
||||
select: isAdmin
|
||||
? {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
password: true,
|
||||
subscriptions: {
|
||||
select: {
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
createdAt: true,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return { response: user, status: 201 };
|
||||
} else {
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
name: body.name,
|
||||
username: emailEnabled
|
||||
? autoGeneratedUsername
|
||||
: (body.username as string).toLowerCase().trim(),
|
||||
email: emailEnabled ? body.email?.toLowerCase().trim() : undefined,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return { response: "User successfully created.", status: 201 };
|
||||
}
|
||||
const { password: pass, ...userWithoutPassword } = user as User;
|
||||
return { response: userWithoutPassword, status: 201 };
|
||||
} else {
|
||||
return { response: "Email or Username already exists.", status: 400 };
|
||||
}
|
||||
|
||||
@@ -4,15 +4,28 @@ import removeFolder from "@/lib/api/storage/removeFolder";
|
||||
import Stripe from "stripe";
|
||||
import { DeleteUserBody } from "@/types/global";
|
||||
import removeFile from "@/lib/api/storage/removeFile";
|
||||
import updateSeats from "@/lib/api/stripe/updateSeats";
|
||||
|
||||
export default async function deleteUserById(
|
||||
userId: number,
|
||||
body: DeleteUserBody,
|
||||
isServerAdmin?: boolean
|
||||
isServerAdmin: boolean,
|
||||
queryId: number
|
||||
) {
|
||||
// First, we retrieve the user from the database
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
include: {
|
||||
subscriptions: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
parentSubscription: {
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
@@ -23,24 +36,70 @@ export default async function deleteUserById(
|
||||
}
|
||||
|
||||
if (!isServerAdmin) {
|
||||
if (user.password) {
|
||||
const isPasswordValid = bcrypt.compareSync(
|
||||
body.password,
|
||||
user.password as string
|
||||
);
|
||||
if (queryId === userId) {
|
||||
if (user.password) {
|
||||
const isPasswordValid = bcrypt.compareSync(
|
||||
body.password,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isPasswordValid && !isServerAdmin) {
|
||||
if (!isPasswordValid && !isServerAdmin) {
|
||||
return {
|
||||
response: "Invalid credentials.",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
response: "Invalid credentials.",
|
||||
status: 401, // Unauthorized
|
||||
response:
|
||||
"User has no password. Please reset your password from the forgot password page.",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
response:
|
||||
"User has no password. Please reset your password from the forgot password page.",
|
||||
status: 401, // Unauthorized
|
||||
};
|
||||
if (user.parentSubscriptionId) {
|
||||
return {
|
||||
response: "Permission denied.",
|
||||
status: 401,
|
||||
};
|
||||
} else {
|
||||
if (!user.subscriptions) {
|
||||
return {
|
||||
response: "User has no subscription.",
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const findChild = await prisma.user.findFirst({
|
||||
where: { id: queryId, parentSubscriptionId: user.subscriptions?.id },
|
||||
});
|
||||
|
||||
if (!findChild)
|
||||
return {
|
||||
response: "Permission denied.",
|
||||
status: 401,
|
||||
};
|
||||
|
||||
const removeUser = await prisma.user.update({
|
||||
where: { id: findChild.id },
|
||||
data: {
|
||||
parentSubscription: {
|
||||
disconnect: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (removeUser.emailVerified)
|
||||
await updateSeats(
|
||||
user.subscriptions.stripeSubscriptionId,
|
||||
user.subscriptions.quantity - 1
|
||||
);
|
||||
|
||||
return {
|
||||
response: "Account removed from subscription.",
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,27 +109,27 @@ export default async function deleteUserById(
|
||||
async (prisma) => {
|
||||
// Delete Access Tokens
|
||||
await prisma.accessToken.deleteMany({
|
||||
where: { userId },
|
||||
where: { userId: queryId },
|
||||
});
|
||||
|
||||
// Delete whitelisted users
|
||||
await prisma.whitelistedUser.deleteMany({
|
||||
where: { userId },
|
||||
where: { userId: queryId },
|
||||
});
|
||||
|
||||
// Delete links
|
||||
await prisma.link.deleteMany({
|
||||
where: { collection: { ownerId: userId } },
|
||||
where: { collection: { ownerId: queryId } },
|
||||
});
|
||||
|
||||
// Delete tags
|
||||
await prisma.tag.deleteMany({
|
||||
where: { ownerId: userId },
|
||||
where: { ownerId: queryId },
|
||||
});
|
||||
|
||||
// Find collections that the user owns
|
||||
const collections = await prisma.collection.findMany({
|
||||
where: { ownerId: userId },
|
||||
where: { ownerId: queryId },
|
||||
});
|
||||
|
||||
for (const collection of collections) {
|
||||
@@ -89,29 +148,29 @@ export default async function deleteUserById(
|
||||
|
||||
// Delete collections after cleaning up related data
|
||||
await prisma.collection.deleteMany({
|
||||
where: { ownerId: userId },
|
||||
where: { ownerId: queryId },
|
||||
});
|
||||
|
||||
// Delete subscription
|
||||
if (process.env.STRIPE_SECRET_KEY)
|
||||
await prisma.subscription
|
||||
.delete({
|
||||
where: { userId },
|
||||
where: { userId: queryId },
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
await prisma.usersAndCollections.deleteMany({
|
||||
where: {
|
||||
OR: [{ userId: userId }, { collection: { ownerId: userId } }],
|
||||
OR: [{ userId: queryId }, { collection: { ownerId: queryId } }],
|
||||
},
|
||||
});
|
||||
|
||||
// Delete user's avatar
|
||||
await removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
||||
await removeFile({ filePath: `uploads/avatar/${queryId}.jpg` });
|
||||
|
||||
// Finally, delete the user
|
||||
await prisma.user.delete({
|
||||
where: { id: userId },
|
||||
where: { id: queryId },
|
||||
});
|
||||
},
|
||||
{ timeout: 20000 }
|
||||
@@ -124,24 +183,36 @@ export default async function deleteUserById(
|
||||
});
|
||||
|
||||
try {
|
||||
const listByEmail = await stripe.customers.list({
|
||||
email: user.email?.toLowerCase(),
|
||||
expand: ["data.subscriptions"],
|
||||
});
|
||||
if (user.subscriptions?.id) {
|
||||
const listByEmail = await stripe.customers.list({
|
||||
email: user.email?.toLowerCase(),
|
||||
expand: ["data.subscriptions"],
|
||||
});
|
||||
|
||||
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
||||
const deleted = await stripe.subscriptions.cancel(
|
||||
listByEmail.data[0].subscriptions?.data[0].id,
|
||||
{
|
||||
cancellation_details: {
|
||||
comment: body.cancellation_details?.comment,
|
||||
feedback: body.cancellation_details?.feedback,
|
||||
},
|
||||
}
|
||||
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
||||
const deleted = await stripe.subscriptions.cancel(
|
||||
listByEmail.data[0].subscriptions?.data[0].id,
|
||||
{
|
||||
cancellation_details: {
|
||||
comment: body.cancellation_details?.comment,
|
||||
feedback: body.cancellation_details?.feedback,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
response: deleted,
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
} else if (user.parentSubscription?.id && user && user.emailVerified) {
|
||||
await updateSeats(
|
||||
user.parentSubscription.stripeSubscriptionId,
|
||||
user.parentSubscription.quantity - 1
|
||||
);
|
||||
|
||||
return {
|
||||
response: deleted,
|
||||
response: "User account and all related data deleted successfully.",
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ export default async function updateUserById(
|
||||
archiveAsWaybackMachine: data.archiveAsWaybackMachine,
|
||||
linksRouteTo: data.linksRouteTo,
|
||||
preventDuplicateLinks: data.preventDuplicateLinks,
|
||||
referredBy:
|
||||
!user?.referredBy && data.referredBy ? data.referredBy : undefined,
|
||||
password:
|
||||
data.newPassword && data.newPassword !== ""
|
||||
? newHashedPassword
|
||||
|
||||
Reference in New Issue
Block a user