refactored/cleaned up API + added support for renaming tags

This commit is contained in:
daniel31x13
2023-10-23 00:28:39 -04:00
parent 24cced9dba
commit ed24685aaf
48 changed files with 603 additions and 305 deletions
+40
View File
@@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getPermission from "@/lib/api/getPermission";
import readFile from "@/lib/api/storage/readFile";
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
if (!req.query.params)
return res.status(401).json({ response: "Invalid parameters." });
const collectionId = req.query.params[0];
const linkId = req.query.params[1];
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.username)
return res.status(401).json({ response: "You must be logged in." });
else if (session?.user?.isSubscriber === false)
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.",
});
const collectionIsAccessible = await getPermission({
userId: session.user.id,
collectionId: Number(collectionId),
});
if (!collectionIsAccessible)
return res
.status(401)
.json({ response: "You don't have access to this collection." });
const { file, contentType, status } = await readFile(
`archives/${collectionId}/${linkId}`
);
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
}
+146
View File
@@ -0,0 +1,146 @@
import { prisma } from "@/lib/api/db";
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
import { AuthOptions, Session } from "next-auth";
import bcrypt from "bcrypt";
import EmailProvider from "next-auth/providers/email";
import { JWT } from "next-auth/jwt";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { Adapter } from "next-auth/adapters";
import sendVerificationRequest from "@/lib/api/sendVerificationRequest";
import { Provider } from "next-auth/providers";
import checkSubscription from "@/lib/api/checkSubscription";
const emailEnabled =
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
const providers: Provider[] = [
CredentialsProvider({
type: "credentials",
credentials: {},
async authorize(credentials, req) {
if (!credentials) return null;
const { username, password } = credentials as {
username: string;
password: string;
};
const findUser = await prisma.user.findFirst({
where: emailEnabled
? {
OR: [
{
username: username.toLowerCase(),
},
{
email: username?.toLowerCase(),
},
],
emailVerified: { not: null },
}
: {
username: username.toLowerCase(),
},
});
let passwordMatches: boolean = false;
if (findUser?.password) {
passwordMatches = bcrypt.compareSync(password, findUser.password);
}
if (passwordMatches) {
return findUser;
} else return null as any;
},
}),
];
if (emailEnabled)
providers.push(
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
maxAge: 1200,
sendVerificationRequest(params) {
sendVerificationRequest(params);
},
})
);
export const authOptions: AuthOptions = {
adapter: PrismaAdapter(prisma) as Adapter,
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
providers,
pages: {
signIn: "/login",
verifyRequest: "/confirmation",
},
callbacks: {
session: async ({ session, token }: { session: Session; token: JWT }) => {
session.user.id = parseInt(token.id as string);
session.user.username = token.username as string;
session.user.isSubscriber = token.isSubscriber as boolean;
return session;
},
async jwt({ token, trigger, user }) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const NEXT_PUBLIC_TRIAL_PERIOD_DAYS =
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS;
const secondsInTwoWeeks = NEXT_PUBLIC_TRIAL_PERIOD_DAYS
? Number(NEXT_PUBLIC_TRIAL_PERIOD_DAYS) * 86400
: 1209600;
const subscriptionIsTimesUp =
token.subscriptionCanceledAt &&
new Date() >
new Date(
((token.subscriptionCanceledAt as number) + secondsInTwoWeeks) *
1000
);
if (
STRIPE_SECRET_KEY &&
(trigger || subscriptionIsTimesUp || !token.isSubscriber)
) {
const subscription = await checkSubscription(
STRIPE_SECRET_KEY,
token.email as string
);
if (subscription.subscriptionCanceledAt) {
token.subscriptionCanceledAt = subscription.subscriptionCanceledAt;
} else token.subscriptionCanceledAt = undefined;
token.isSubscriber = subscription.isSubscriber;
}
if (trigger === "signIn") {
token.id = user.id;
token.username = (user as any).username;
} else if (trigger === "update" && token.id) {
console.log(token);
const user = await prisma.user.findUnique({
where: {
id: token.id as number,
},
});
if (user) {
token.name = user.name;
token.username = user.username?.toLowerCase();
token.email = user.email?.toLowerCase();
}
}
return token;
},
},
};
export default NextAuth(authOptions);
+60
View File
@@ -0,0 +1,60 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import { prisma } from "@/lib/api/db";
import readFile from "@/lib/api/storage/readFile";
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id;
const username = session?.user.username?.toLowerCase();
const queryId = Number(req.query.id);
if (!userId || !username)
return res
.setHeader("Content-Type", "text/html")
.status(401)
.send("You must be logged in.");
else if (session?.user?.isSubscriber === false)
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.",
});
if (!queryId)
return res
.setHeader("Content-Type", "text/html")
.status(401)
.send("Invalid parameters.");
if (userId !== queryId) {
const targetUser = await prisma.user.findUnique({
where: {
id: queryId,
},
include: {
whitelistedUsers: true,
},
});
const whitelistedUsernames = targetUser?.whitelistedUsers.map(
(whitelistedUsername) => whitelistedUsername.username
);
if (targetUser?.isPrivate && !whitelistedUsernames?.includes(username)) {
return res
.setHeader("Content-Type", "text/html")
.send("This profile is private.");
}
}
const { file, contentType, status } = await readFile(
`uploads/avatar/${queryId}.jpg`
);
return res
.setHeader("Content-Type", contentType)
.status(status as number)
.send(file);
}
+35
View File
@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import updateCollectionById from "@/lib/api/controllers/collections/collectionId/updateCollectionById";
import deleteCollectionById from "@/lib/api/controllers/collections/collectionId/deleteCollectionById";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "PUT") {
const updated = await updateCollectionById(
session.user.id,
Number(req.query.id) as number,
req.body
);
return res.status(updated.status).json({ response: updated.response });
} else if (req.method === "DELETE") {
const deleted = await deleteCollectionById(
session.user.id,
Number(req.query.id) as number
);
return res.status(deleted.status).json({ response: deleted.response });
}
}
+32
View File
@@ -0,0 +1,32 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getCollections from "@/lib/api/controllers/collections/getCollections";
import postCollection from "@/lib/api/controllers/collections/postCollection";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const collections = await getCollections(session.user.id);
return res
.status(collections.status)
.json({ response: collections.response });
} else if (req.method === "POST") {
const newCollection = await postCollection(req.body, session.user.id);
return res
.status(newCollection.status)
.json({ response: newCollection.response });
}
}
+16
View File
@@ -0,0 +1,16 @@
// For future...
// import { getToken } from "next-auth/jwt";
// export default async (req, res) => {
// // If you don't have NEXTAUTH_SECRET set, you will have to pass your secret as `secret` to `getToken`
// console.log({ req });
// const token = await getToken({ req, raw: true });
// if (token) {
// // Signed in
// console.log("JSON Web Token", JSON.stringify(token, null, 2));
// } else {
// // Not Signed in
// res.status(401);
// }
// res.end();
// };
+33
View File
@@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import deleteLinkById from "@/lib/api/controllers/links/linkId/deleteLinkById";
import updateLinkById from "@/lib/api/controllers/links/linkId/updateLinkById";
export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "PUT") {
const updated = await updateLinkById(
session.user.id,
Number(req.query.id),
req.body
);
return res.status(updated.status).json({
response: updated.response,
});
} else if (req.method === "DELETE") {
const deleted = await deleteLinkById(session.user.id, Number(req.query.id));
return res.status(deleted.status).json({
response: deleted.response,
});
}
}
+27
View File
@@ -0,0 +1,27 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getLinks from "@/lib/api/controllers/links/getLinks";
import postLink from "@/lib/api/controllers/links/postLink";
export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const links = await getLinks(session.user.id, req?.query?.body as string);
return res.status(links.status).json({ response: links.response });
} else if (req.method === "POST") {
const newlink = await postLink(req.body, session.user.id);
return res.status(newlink.status).json({
response: newlink.response,
});
}
}
+41
View File
@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import exportData from "@/lib/api/controllers/migration/exportData";
import importFromHTMLFile from "@/lib/api/controllers/migration/importFromHTMLFile";
import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden";
import { MigrationFormat, MigrationRequest } from "@/types/global";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user.id) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const data = await exportData(session.user.id);
if (data.status === 200)
return res
.setHeader("Content-Type", "application/json")
.setHeader("Content-Disposition", "attachment; filename=backup.json")
.status(data.status)
.json(data.response);
} else if (req.method === "POST") {
const request: MigrationRequest = JSON.parse(req.body);
let data;
if (request.format === MigrationFormat.htmlFile)
data = await importFromHTMLFile(session.user.id, request.data);
if (request.format === MigrationFormat.linkwarden)
data = await importFromLinkwarden(session.user.id, request.data);
if (data) return res.status(data.status).json({ response: data.response });
}
}
+34
View File
@@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import paymentCheckout from "@/lib/api/paymentCheckout";
import { Plan } from "@/types/global";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const MONTHLY_PRICE_ID = process.env.MONTHLY_PRICE_ID;
const YEARLY_PRICE_ID = process.env.YEARLY_PRICE_ID;
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id)
return res.status(401).json({ response: "You must be logged in." });
else if (!STRIPE_SECRET_KEY || !MONTHLY_PRICE_ID || !YEARLY_PRICE_ID) {
return res.status(400).json({ response: "Payment is disabled." });
}
let PRICE_ID = MONTHLY_PRICE_ID;
if ((Number(req.query.plan) as unknown as Plan) === Plan.monthly)
PRICE_ID = MONTHLY_PRICE_ID;
else if ((Number(req.query.plan) as unknown as Plan) === Plan.yearly)
PRICE_ID = YEARLY_PRICE_ID;
if (req.method === "GET") {
const users = await paymentCheckout(
STRIPE_SECRET_KEY,
session?.user.email,
PRICE_ID
);
return res.status(users.status).json({ response: users.response });
}
}
+20
View File
@@ -0,0 +1,20 @@
import getCollection from "@/lib/api/controllers/public/getCollection";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function collections(
req: NextApiRequest,
res: NextApiResponse
) {
if (!req?.query?.body) {
return res
.status(401)
.json({ response: "Please choose a valid collection." });
}
if (req.method === "GET") {
const collection = await getCollection(req?.query?.body as string);
return res
.status(collection.status)
.json({ response: collection.response });
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import updateTag from "@/lib/api/controllers/tags/tagId/updeteTagById";
export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." });
} else if (session?.user?.isSubscriber === false)
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.",
});
const tagId = Number(req.query.id);
if (req.method === "PUT") {
const tags = await updateTag(session.user.id, tagId, req.body);
return res.status(tags.status).json({ response: tags.response });
}
}
+21
View File
@@ -0,0 +1,21 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getTags from "@/lib/api/controllers/tags/getTags";
export default async function tags(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.username) {
return res.status(401).json({ response: "You must be logged in." });
} else if (session?.user?.isSubscriber === false)
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const tags = await getTags(session.user.id);
return res.status(tags.status).json({ response: tags.response });
}
}
+40
View File
@@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getUserById from "@/lib/api/controllers/users/userId/getUserById";
import getPublicUserById from "@/lib/api/controllers/users/userId/getPublicUserById";
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
const userId = session?.user.id;
const username = session?.user.username;
const lookupId = req.query.id as string;
const isSelf =
userId === Number(lookupId) || username === lookupId ? true : false;
// Check if "lookupId" is the user "id" or their "username"
const isId = lookupId.split("").every((e) => Number.isInteger(parseInt(e)));
if (req.method === "GET" && !isSelf) {
const users = await getPublicUserById(lookupId, isId, username);
return res.status(users.status).json({ response: users.response });
}
if (!userId) {
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 support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const users = await getUserById(session.user.id);
return res.status(users.status).json({ response: users.response });
} else if (req.method === "PUT") {
const updated = await updateUserById(session.user, req.body);
return res.status(updated.status).json({ response: updated.response });
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import postUser from "@/lib/api/controllers/users/postUser";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
const response = await postUser(req, res);
return response;
}
}