mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-07-01 08:16:26 +00:00
241 lines
6.8 KiB
TypeScript
241 lines
6.8 KiB
TypeScript
import { prisma } from "@linkwarden/prisma";
|
|
import bcrypt from "bcrypt";
|
|
import { removeFolder, removeFile } from "@linkwarden/filesystem";
|
|
import { DeleteUserBody } from "@linkwarden/types/global";
|
|
import updateSeats from "@/lib/api/billing/updateSeats";
|
|
import { meiliClient } from "@linkwarden/lib/meilisearchClient";
|
|
import stripeSDK from "@/lib/api/billing/stripeSDK";
|
|
import transporter from "@linkwarden/lib/transporter";
|
|
|
|
export default async function deleteUserById(
|
|
userId: number,
|
|
body: DeleteUserBody,
|
|
isServerAdmin: boolean,
|
|
queryId: number,
|
|
bypassPassword: boolean = false
|
|
) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
include: {
|
|
subscriptions: {
|
|
include: {
|
|
user: true,
|
|
},
|
|
},
|
|
parentSubscription: {
|
|
include: {
|
|
user: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return {
|
|
response: "Invalid credentials.",
|
|
status: 404,
|
|
};
|
|
}
|
|
|
|
if (!isServerAdmin) {
|
|
if (queryId === userId) {
|
|
// Non-subscribers (only granted when billing is enabled) may delete their
|
|
// own account without re-entering their password.
|
|
if (!bypassPassword) {
|
|
if (user.password) {
|
|
const isPasswordValid = bcrypt.compareSync(
|
|
body.password,
|
|
user.password
|
|
);
|
|
|
|
if (!isPasswordValid) {
|
|
return {
|
|
response: "Invalid credentials.",
|
|
status: 401,
|
|
};
|
|
}
|
|
} else {
|
|
return {
|
|
response:
|
|
"User has no password. Please create one from the password settings page.",
|
|
status: 401,
|
|
};
|
|
}
|
|
}
|
|
} else {
|
|
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 &&
|
|
user.subscriptions.provider === "STRIPE" &&
|
|
user.subscriptions.stripeSubscriptionId
|
|
)
|
|
await updateSeats(
|
|
user.subscriptions.stripeSubscriptionId,
|
|
user.subscriptions.quantity - 1
|
|
);
|
|
|
|
return {
|
|
response: "Account removed from subscription.",
|
|
status: 200,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the user and all related data within a transaction
|
|
await prisma
|
|
.$transaction(
|
|
async (prisma) => {
|
|
const links = await prisma.link.findMany({
|
|
where: { collection: { ownerId: queryId } },
|
|
select: { id: true },
|
|
});
|
|
|
|
const linkIds = links.map((link) => link.id);
|
|
|
|
await meiliClient?.index("links").deleteDocuments(linkIds);
|
|
|
|
const collections = await prisma.collection.findMany({
|
|
where: { ownerId: queryId },
|
|
});
|
|
|
|
await Promise.all(
|
|
collections.map(async (collection) => {
|
|
await removeFolder({ filePath: `archives/${collection.id}` });
|
|
await removeFolder({
|
|
filePath: `archives/preview/${collection.id}`,
|
|
});
|
|
})
|
|
);
|
|
|
|
await removeFile({ filePath: `uploads/avatar/${queryId}.jpg` });
|
|
|
|
const billingEnabled =
|
|
Boolean(process.env.STRIPE_SECRET_KEY) ||
|
|
Boolean(
|
|
process.env.REVENUECAT_PROJECT_ID && process.env.REVENUECAT_API_KEY
|
|
);
|
|
|
|
// Send an email about cancellation reason if provided
|
|
if (
|
|
billingEnabled &&
|
|
(body.cancellation_details?.comment ||
|
|
body.cancellation_details?.feedback ||
|
|
user.acceptPromotionalEmails)
|
|
) {
|
|
try {
|
|
await transporter.sendMail({
|
|
from: process.env.EMAIL_FROM,
|
|
to: "hello@linkwarden.app",
|
|
subject: "Linkwarden User Cancellation",
|
|
text: `User: ${user.email}\nFeedback: ${
|
|
body.cancellation_details?.feedback || "N/A"
|
|
}\nComment: ${
|
|
body.cancellation_details?.comment || "N/A"
|
|
}\nPromotional Emails: ${String(user.acceptPromotionalEmails)}`,
|
|
});
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
if (
|
|
process.env.STRIPE_SECRET_KEY &&
|
|
user.subscriptions?.provider === "STRIPE"
|
|
) {
|
|
const stripe = stripeSDK();
|
|
|
|
try {
|
|
if (user.subscriptions?.id && queryId !== userId) {
|
|
const subscription = await prisma.subscription.findFirst({
|
|
where: { userId: queryId },
|
|
select: { stripeSubscriptionId: true },
|
|
});
|
|
|
|
if (subscription?.stripeSubscriptionId) {
|
|
await stripe.subscriptions.cancel(
|
|
subscription.stripeSubscriptionId,
|
|
{
|
|
cancellation_details: {
|
|
comment: body.cancellation_details?.comment,
|
|
feedback: body.cancellation_details?.feedback,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
} else if (
|
|
user.subscriptions?.id &&
|
|
user.subscriptions.stripeSubscriptionId &&
|
|
queryId === userId
|
|
) {
|
|
await stripe.subscriptions.cancel(
|
|
user.subscriptions.stripeSubscriptionId,
|
|
{
|
|
cancellation_details: {
|
|
comment: body.cancellation_details?.comment,
|
|
feedback: body.cancellation_details?.feedback,
|
|
},
|
|
}
|
|
);
|
|
} else if (
|
|
user.parentSubscription?.id &&
|
|
user.parentSubscription.stripeSubscriptionId &&
|
|
user &&
|
|
user.emailVerified
|
|
) {
|
|
await updateSeats(
|
|
user.parentSubscription.stripeSubscriptionId,
|
|
user.parentSubscription.quantity - 1
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// Finally, delete the user
|
|
await prisma.user.delete({
|
|
where: { id: queryId },
|
|
});
|
|
},
|
|
{ timeout: 20000 }
|
|
)
|
|
.catch((err) => console.log(err));
|
|
|
|
return {
|
|
response: "User account and all related data deleted successfully.",
|
|
status: 200,
|
|
};
|
|
}
|