diff --git a/README.md b/README.md index a9d4603..4fe5646 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # TrollCallAPIs + A set of TrollCallNotAgain-equivalent API hooks, made to let developers create third-party apps for the TrollCall service. --- @@ -6,11 +7,13 @@ A set of TrollCallNotAgain-equivalent API hooks, made to let developers create t TrollCall is great, but the merge of the client and server TrollCallNotAgain has with Next.js is problematic. (ahem, Client/Server Hydration and Server Components (those are messy grr)) There are a few issues with the existing APIs as well: + 1. Objects get duplicated if their name/first-name is changed. Doing this: + ``` -PUSH /api/user/.../troll/name1 +PUSH /api/clan/.../troll/name1 { ... @@ -18,12 +21,15 @@ PUSH /api/user/.../troll/name1 ... } ``` + results in this on the database: + ``` trolls/ |- Document {"name": ["name1", ...], ...} |- Document {"name": ["name2", ...], ...} ``` + which is not good. 2. More issues that I forgot diff --git a/package-lock.json b/package-lock.json index e12e6c9..479a758 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "license": "ISC", "dependencies": { "@types/cookie-parser": "^1.4.3", + "@types/crypto-js": "^4.1.1", "@types/react": "^18.2.14", "body-parser": "^1.20.2", "cookie": "^0.5.0", "cookie-parser": "^1.4.6", + "crypto-js": "^4.1.1", "dotenv": "^16.3.0", "express": "^4.18.2", "lodash": "^4.17.21", @@ -262,6 +264,11 @@ "@types/express": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==" + }, "node_modules/@types/express": { "version": "4.17.17", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", @@ -748,6 +755,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", diff --git a/package.json b/package.json index e1c6405..310732c 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,12 @@ "description": "", "dependencies": { "@types/cookie-parser": "^1.4.3", + "@types/crypto-js": "^4.1.1", "@types/react": "^18.2.14", "body-parser": "^1.20.2", "cookie": "^0.5.0", "cookie-parser": "^1.4.6", + "crypto-js": "^4.1.1", "dotenv": "^16.3.0", "express": "^4.18.2", "lodash": "^4.17.21", diff --git a/src/lib/db/mongodb.ts b/src/lib/db/mongodb.ts index e7792ab..b29f97c 100644 --- a/src/lib/db/mongodb.ts +++ b/src/lib/db/mongodb.ts @@ -1,6 +1,21 @@ import { MongoClient } from "mongodb"; -if (process.env.MONGODB_DATABASE == null) process.exit(); +if (process.env.ENCRYPT_CODE == null) { + console.log( + "You need to write an encryption code to run a TrollCall server!" + ); + process.exit(); +} +if (process.env.MONGODB_DATABASE == null) { + console.log("You need a MongoDB Database URI to run a TrollCall server!"); + process.exit(); +} +if (process.env.MONGODB_DATABASE_NAME == null) { + console.log( + 'You need to specify a MongoDB Database Store to run a TrollCall server! Default can be "trollcall"' + ); + process.exit(); +} export const client = new MongoClient(process.env.MONGODB_DATABASE, {}); diff --git a/src/lib/trollcall/api/clan.ts b/src/lib/trollcall/api/clan.ts new file mode 100644 index 0000000..6cb22ab --- /dev/null +++ b/src/lib/trollcall/api/clan.ts @@ -0,0 +1,28 @@ +import { ClientClan, ServerClan } from "@/types/clan"; +import { getSingleClan } from "../clan"; +import { ServerClanToClientClan } from "../convert/clan"; +import { ServerFlairToClientFlair } from "../convert/flair"; +import { getManyFlairs } from "../flair"; +import { cutArray } from "../utility/merge"; + +export async function ClanGET( + query?: Partial<{ + [key: string]: string | string[]; + }> | null, + existingClan?: ServerClan +): Promise { + const clan = + existingClan ?? + (await getSingleClan({ + name: query?.clan + })); + if (clan == null) return null; + const serverClan = await ServerClanToClientClan(clan); + serverClan.flairs = cutArray( + await getManyFlairs( + { _id: { $in: clan.flairs } }, + ServerFlairToClientFlair + ) + ); + return serverClan as ClientClan; +} diff --git a/src/lib/trollcall/clan.ts b/src/lib/trollcall/clan.ts new file mode 100644 index 0000000..f2fa52e --- /dev/null +++ b/src/lib/trollcall/clan.ts @@ -0,0 +1,87 @@ +import { ServerClan } from "@/types/clan"; +import { Sort } from "mongodb"; +import { + Filter, + createOne, + cursorToArray, + readMany, + readOne, + replaceOne +} from "../db/crud"; + +const ClanSort: Sort = { updatedDate: -1, _id: -1 }; + +/** + * A function that returns one ServerClan from the database. + * @param query A partial Find query. Can contain an ID. + * @returns A ServerClan. + */ + +export async function getSingleClan( + query: Filter +): Promise { + const clan = (await readOne("clans", query)) as ServerClan | null; + return clan; +} + +/** + * A function that returns many ServerClans from the database using a FindCursor. + * @param query A partial Find query. Can contain an ID. + * @param func A function to run on every ServerClan returned. Helps reduce loops. + * @returns An array of ServerClans. + */ + +export async function getManyClans( + query: Filter, + func?: (input: any) => T +): Promise<(Awaited | null)[]> { + const clan = (await cursorToArray( + readMany("clans", query, ClanSort), + func + )) as (Awaited | null)[]; + return clan; +} + +/** + * A function that returns many ServerClans from the database using a FindCursor, limited by count. + * @param query A partial Find query. Can contain an ID. + * @param func A function to run on every ServerClan returned. Helps reduce loops. + * @returns An array of ServerClans. + */ + +export async function getManyPagedClans( + query: Filter, + func?: (input: any) => T, + count: number = 5, + page: number = 0 +): Promise<(Awaited | null)[]> { + const find = readMany("clans", query, ClanSort) + .limit(count) + .skip(page * count); + const clan = (await cursorToArray(find, func)) as (Awaited | null)[]; + return clan; +} + +/** + * A function that puts one ServerClan into the database. + * @param clan A ServerClan. + * @returns A ServerClan, or null, depending on if the operation succeeded. + */ + +export async function createClan( + clan: Omit +): Promise | null> { + const newClan = await createOne("clans", clan); + return newClan.acknowledged ? clan : null; +} + +/** + * A function that changes one database clan with the given params. + * @param clan A ServerClan. + * @returns A ServerClan, or null, depending on if the operation succeeded. + */ + +export async function changeClan(clan: ServerClan): Promise { + const newClan = await replaceOne("clans", { _id: clan._id }, clan); + return newClan.acknowledged ? clan : null; +} diff --git a/src/lib/trollcall/convert/clan.ts b/src/lib/trollcall/convert/clan.ts new file mode 100644 index 0000000..61ef1b9 --- /dev/null +++ b/src/lib/trollcall/convert/clan.ts @@ -0,0 +1,39 @@ +import { ClientClan, ServerClan } from "@/types/clan"; +import { SubmitClan } from "@/types/client/clan"; +import { cutObject, removeCode, sanitize } from "../utility/merge"; + +export async function ServerClanToClientClan( + serverClan: ServerClan +): Promise> { + const sanitizedClan = removeCode(sanitize(serverClan)); + let clientClan: Partial = { + ...sanitizedClan, + flairs: undefined, + updatedDate: serverClan.updatedDate?.getTime() + }; + return clientClan; +} + +export function SubmitClanToServerClan( + submitClan: Partial +): Omit, "_id"> { + let serverClan: Omit, "_id"> = { + ...submitClan, + flairs: undefined, + code: submitClan.code || undefined, + updatedDate: new Date() + }; + return serverClan; +} + +export function MergeServerClans( + submitClan: ServerClan, + merge: Partial> +): ServerClan { + let serverClan: ServerClan = { + ...submitClan, + ...cutObject(merge), + updatedDate: new Date() + }; + return serverClan; +} diff --git a/src/lib/trollcall/convert/troll.ts b/src/lib/trollcall/convert/troll.ts index d993ceb..7b59277 100644 --- a/src/lib/trollcall/convert/troll.ts +++ b/src/lib/trollcall/convert/troll.ts @@ -1,19 +1,14 @@ import { Class, TrueSign } from "@/types/assist/extended_zodiac"; import { SubmitTroll } from "@/types/client/troll"; import { ClientTroll, ServerTroll } from "@/types/troll"; -import { getManyFlairs } from "../flair"; -import { cutArray, cutObject, sanitize } from "../utility/merge"; -import { ServerFlairToClientFlair } from "./flair"; +import { cutObject, sanitize } from "../utility/merge"; export async function ServerTrollToClientTroll( serverTroll: ServerTroll -): Promise { +): Promise> { const sanitizedTroll = sanitize(serverTroll); - const flairs = await getManyFlairs( - { _id: { $in: serverTroll.flairs } }, - ServerFlairToClientFlair - ); - let clientTroll: ClientTroll = { + + let clientTroll: Partial = { ...sanitizedTroll, trueSign: TrueSign[serverTroll.trueSign], falseSign: @@ -21,8 +16,8 @@ export async function ServerTrollToClientTroll( ? TrueSign[serverTroll.falseSign] : null, class: serverTroll.class ? Class[serverTroll.class] : null, - owners: [], - flairs: cutArray(flairs) + owner: undefined, + flairs: undefined }; return clientTroll; @@ -36,7 +31,7 @@ export function SubmitTrollToServerTroll( quirks: submitTroll.quirks ? Object.fromEntries(submitTroll.quirks) : undefined, - owners: undefined, + owner: undefined, flairs: undefined, updatedDate: new Date() }; diff --git a/src/lib/trollcall/convert/user.ts b/src/lib/trollcall/convert/user.ts deleted file mode 100644 index 68c696d..0000000 --- a/src/lib/trollcall/convert/user.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { TrueSign } from "@/types/assist/extended_zodiac"; -import { SubmitUser } from "@/types/client/user"; -import { ClientUser, ServerUser } from "@/types/user"; -import { getManyFlairs } from "../flair"; -import { cutArray, cutObject, removeCode, sanitize } from "../utility/merge"; -import { ServerFlairToClientFlair } from "./flair"; - -export async function ServerUserToClientUser( - serverUser: ServerUser -): Promise { - const sanitizedUser = removeCode(sanitize(serverUser)); - const flairs = await getManyFlairs( - { _id: { $in: serverUser.flairs } }, - ServerFlairToClientFlair - ); - let clientUser: ClientUser = { - ...sanitizedUser, - trueSign: serverUser.trueSign ? TrueSign[serverUser.trueSign] : null, - flairs: cutArray(flairs), - updatedDate: serverUser.updatedDate?.getTime() - }; - return clientUser; -} - -export function SubmitUserToServerUser( - submitUser: Partial -): Omit, "_id"> { - let serverUser: Omit, "_id"> = { - ...submitUser, - flairs: undefined, - code: submitUser.code || "", - updatedDate: new Date() - }; - return serverUser; -} - -export function MergeServerUsers( - submitUser: ServerUser, - merge: Partial> -): ServerUser { - let serverUser: ServerUser = { - ...submitUser, - ...cutObject(merge), - updatedDate: new Date() - }; - return serverUser; -} diff --git a/src/lib/trollcall/perms.ts b/src/lib/trollcall/perms.ts index 27f49a0..a864d1b 100644 --- a/src/lib/trollcall/perms.ts +++ b/src/lib/trollcall/perms.ts @@ -1,11 +1,13 @@ import { Levels, Permissions } from "@/permissions"; -import { ServerUser } from "@/types/user"; +import { ServerClan } from "@/types/clan"; +import CryptoJS from "crypto-js"; +import AES from "crypto-js/aes"; import { ObjectId } from "mongodb"; -export function getLevel(user: Partial & { flairs: ObjectId[] }) { - let highestLevel = "USER"; +export function getLevel(clan: Partial & { flairs: ObjectId[] }) { + let highestLevel = "CLAN"; for (let level of Permissions) { - if (user.flairs.some(oid => level.values.includes(oid.toString()))) + if (clan.flairs.some(oid => level.values.includes(oid.toString()))) highestLevel = level.name; } return highestLevel; @@ -16,13 +18,17 @@ export function compareLevels(level: string, compareLevel: string) { } export function compareCredentials( - user: ServerUser, + clan: ServerClan, cookies: Partial<{ [key: string]: string; }> ) { + const decryptCode = AES.decrypt( + clan.code, + process.env.ENCRYPT_CODE ?? "HACKTHIS" + ).toString(CryptoJS.enc.Utf8); return ( - user.code === cookies.TROLLCALL_CODE && - user.name === cookies.TROLLCALL_NAME + decryptCode === cookies.TROLLCALL_CODE && + clan.name === cookies.TROLLCALL_NAME ); } diff --git a/src/lib/trollcall/troll.ts b/src/lib/trollcall/troll.ts index 39b9699..3c00cf5 100644 --- a/src/lib/trollcall/troll.ts +++ b/src/lib/trollcall/troll.ts @@ -58,8 +58,8 @@ export async function getManyPagedTrolls( const find = readMany("trolls", query, TrollSort) .limit(count) .skip(page * count); - const user = (await cursorToArray(find, func)) as (Awaited | null)[]; - return user; + const clan = (await cursorToArray(find, func)) as (Awaited | null)[]; + return clan; } /** diff --git a/src/lib/trollcall/user.ts b/src/lib/trollcall/user.ts deleted file mode 100644 index 1d92c67..0000000 --- a/src/lib/trollcall/user.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { ServerUser } from "@/types/user"; -import { Sort } from "mongodb"; -import { - Filter, - createOne, - cursorToArray, - readMany, - readOne, - replaceOne -} from "../db/crud"; - -const UserSort: Sort = { updatedDate: -1, _id: -1 }; - -/** - * A function that returns one ServerUser from the database. - * @param query A partial Find query. Can contain an ID. - * @returns A ServerUser. - */ - -export async function getSingleUser( - query: Filter -): Promise { - const user = (await readOne("users", query)) as ServerUser | null; - return user; -} - -/** - * A function that returns many ServerUsers from the database using a FindCursor. - * @param query A partial Find query. Can contain an ID. - * @param func A function to run on every ServerUser returned. Helps reduce loops. - * @returns An array of ServerUsers. - */ - -export async function getManyUsers( - query: Filter, - func?: (input: any) => T -): Promise<(Awaited | null)[]> { - const user = (await cursorToArray( - readMany("users", query, UserSort), - func - )) as (Awaited | null)[]; - return user; -} - -/** - * A function that returns many ServerUsers from the database using a FindCursor, limited by count. - * @param query A partial Find query. Can contain an ID. - * @param func A function to run on every ServerUser returned. Helps reduce loops. - * @returns An array of ServerUsers. - */ - -export async function getManyPagedUsers( - query: Filter, - func?: (input: any) => T, - count: number = 5, - page: number = 0 -): Promise<(Awaited | null)[]> { - const find = readMany("users", query, UserSort) - .limit(count) - .skip(page * count); - const user = (await cursorToArray(find, func)) as (Awaited | null)[]; - return user; -} - -/** - * A function that puts one ServerUser into the database. - * @param user A ServerUser. - * @returns A ServerUser, or null, depending on if the operation succeeded. - */ - -export async function createUser( - user: Omit -): Promise | null> { - const newUser = await createOne("users", user); - return newUser.acknowledged ? user : null; -} - -/** - * A function that changes one database user with the given params. - * @param user A ServerUser. - * @returns A ServerUser, or null, depending on if the operation succeeded. - */ - -export async function changeUser(user: ServerUser): Promise { - const newUser = await replaceOne("users", { _id: user._id }, user); - return newUser.acknowledged ? user : null; -} diff --git a/src/pages/api/user/.../[[...page]]/index.ts b/src/pages/api/clan/.../[[...page]]/index.ts similarity index 55% rename from src/pages/api/user/.../[[...page]]/index.ts rename to src/pages/api/clan/.../[[...page]]/index.ts index 6af1929..dab6802 100644 --- a/src/pages/api/user/.../[[...page]]/index.ts +++ b/src/pages/api/clan/.../[[...page]]/index.ts @@ -1,5 +1,5 @@ -import { ServerUserToClientUser } from "@/lib/trollcall/convert/user"; -import { getManyPagedUsers } from "@/lib/trollcall/user"; +import { getManyPagedClans } from "@/lib/trollcall/clan"; +import { ServerClanToClientClan } from "@/lib/trollcall/convert/clan"; import { NextApiRequest, NextApiResponse } from "next"; export default async function handler( @@ -9,13 +9,13 @@ export default async function handler( const { method, query } = req; const page = query.page ? query.page[0] : 0; if (method === "GET") { - const users = await getManyPagedUsers( + const clans = await getManyPagedClans( {}, - ServerUserToClientUser, + ServerClanToClientClan, 5, page ); - if (users == null) return res.status(404).end(); - res.json(users); + if (clans == null) return res.status(404).end(); + res.json(clans); } else return res.status(405).end(); } diff --git a/src/pages/api/clan/[clan]/index.ts b/src/pages/api/clan/[clan]/index.ts new file mode 100644 index 0000000..b5839fd --- /dev/null +++ b/src/pages/api/clan/[clan]/index.ts @@ -0,0 +1,85 @@ +import { ClanGET } from "@/lib/trollcall/api/clan"; +import { changeClan, getSingleClan } from "@/lib/trollcall/clan"; +import { + MergeServerClans, + SubmitClanToServerClan +} from "@/lib/trollcall/convert/clan"; +import { + compareCredentials, + compareLevels, + getLevel +} from "@/lib/trollcall/perms"; +import { PartialClanSchema, SubmitClan } from "@/types/client/clan"; +import { serialize } from "cookie"; +import AES from "crypto-js/aes"; +import { nanoid } from "nanoid"; +import { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { body, cookies, query, method } = req; + if (method === "GET") { + res.json(await ClanGET(query)); + } else if (method === "PUT") { + let validatedClan: Partial; + try { + validatedClan = (await PartialClanSchema.validate(body, { + stripUnknown: true + })) as Partial; + } catch (err) { + return res.status(400).send(err); + } + const checkExistingClan = await getSingleClan({ + name: query.clan + }); + if (checkExistingClan == null) return res.status(404).end(); + let isModerator = false; + if (!compareCredentials(checkExistingClan, cookies)) { + const thisClan = await getSingleClan({ + name: cookies.TROLLCALL_NAME + }); + if (thisClan == null || !compareCredentials(thisClan, cookies)) + return res.status(403).end(); + if (!compareLevels(getLevel(thisClan), "MODERATOR")) + return res.status(403).end(); + isModerator = true; + } + const serverClan = SubmitClanToServerClan(validatedClan); + if (serverClan.code == null) + serverClan.code = checkExistingClan.code || nanoid(16); + + // Encrypt code lole + serverClan.code = AES.encrypt( + serverClan.code, + process.env.ENCRYPT_CODE ?? "HACKTHIS" + ).toString(); + + if (!compareLevels(getLevel(checkExistingClan), "SUPPORTER")) { + serverClan.bgimage = null; + serverClan.css = null; + } + const bothClans = MergeServerClans(checkExistingClan, serverClan); + const newClan = await changeClan(bothClans); + if (newClan == null) return res.status(503).end(); + // Give cookies, redundant style + if (!isModerator) + // don't set cookies if moderator is changing credentials + res.setHeader("Set-Cookie", [ + serialize("TROLLCALL_NAME", newClan.name, { + path: "/", + maxAge: 31540000 + }), + serialize("TROLLCALL_CODE", newClan.code, { + path: "/", + maxAge: 31540000 + }), + serialize("TROLLCALL_PFP", newClan.pfp ?? "", { + path: "/", + maxAge: 31540000 + }) + ]).json(newClan); + else res.json(newClan); + } else return res.status(405).end(); +} diff --git a/src/pages/api/clan/index.ts b/src/pages/api/clan/index.ts new file mode 100644 index 0000000..6e49bb6 --- /dev/null +++ b/src/pages/api/clan/index.ts @@ -0,0 +1,63 @@ +import { createClan, getSingleClan } from "@/lib/trollcall/clan"; +import { SubmitClanToServerClan } from "@/lib/trollcall/convert/clan"; +import { compareLevels, getLevel } from "@/lib/trollcall/perms"; +import { ServerClan } from "@/types/clan"; +import { SubmitClanSchema } from "@/types/client/clan"; +import { serialize } from "cookie"; +import AES from "crypto-js/aes"; +import { nanoid } from "nanoid"; +import { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { body, method } = req; + if (method === "POST") { + let validatedClan; + try { + validatedClan = await SubmitClanSchema.validate(body, { + stripUnknown: true + }); + } catch (err) { + return res.status(400).send(err); + } + const checkExistingClan = await getSingleClan({ + name: validatedClan.name + }); + if (checkExistingClan != null) return res.status(409).end(); + // we are sure this object is full, so cast partial + const serverClan = SubmitClanToServerClan(validatedClan) as Omit< + ServerClan, + "_id" + >; + if (serverClan.code == null) serverClan.code = nanoid(16); + + // Encrypt code lole + serverClan.code = AES.encrypt( + serverClan.code, + process.env.ENCRYPT_CODE ?? "HACKTHIS" + ).toString(); + + if (!compareLevels(getLevel(serverClan), "SUPPORTER")) + serverClan.bgimage = null; + const newClan = await createClan(serverClan); + if (newClan == null) return res.status(503).end(); + // Give cookies + res.setHeader("Set-Cookie", [ + serialize("TROLLCALL_NAME", newClan.name, { + path: "/", + maxAge: 31540000 + }), + serialize("TROLLCALL_CODE", newClan.code, { + path: "/", + maxAge: 31540000 + }), + serialize("TROLLCALL_PFP", newClan.pfp ?? "", { + path: "/", + maxAge: 31540000 + }) + ]); + res.json(newClan); + } else return res.status(405).end(); +} diff --git a/src/pages/api/troll/[user]/.../[[...page]]/index.ts b/src/pages/api/troll/[clan]/.../[[...page]]/index.ts similarity index 76% rename from src/pages/api/troll/[user]/.../[[...page]]/index.ts rename to src/pages/api/troll/[clan]/.../[[...page]]/index.ts index 3f961fd..9b0445c 100644 --- a/src/pages/api/troll/[user]/.../[[...page]]/index.ts +++ b/src/pages/api/troll/[clan]/.../[[...page]]/index.ts @@ -1,6 +1,6 @@ +import { getSingleClan } from "@/lib/trollcall/clan"; import { ServerTrollToClientTroll } from "@/lib/trollcall/convert/troll"; import { getManyPagedTrolls } from "@/lib/trollcall/troll"; -import { getSingleUser } from "@/lib/trollcall/user"; import { NextApiRequest, NextApiResponse } from "next"; export default async function handler( @@ -10,13 +10,13 @@ export default async function handler( const { method, query } = req; const page = query.page ? query.page[0] : 0; if (method === "GET") { - const user = await getSingleUser({ - name: query.user + const clan = await getSingleClan({ + name: query.clan }); - if (user == null) return res.status(404).end(); + if (clan == null) return res.status(404).end(); const trolls = await getManyPagedTrolls( { - "owners.0": user._id + "owner": clan._id }, ServerTrollToClientTroll, 5, diff --git a/src/pages/api/troll/[user]/[troll]/index.ts b/src/pages/api/troll/[clan]/[troll]/index.ts similarity index 59% rename from src/pages/api/troll/[user]/[troll]/index.ts rename to src/pages/api/troll/[clan]/[troll]/index.ts index e082f3e..f2eef77 100644 --- a/src/pages/api/troll/[user]/[troll]/index.ts +++ b/src/pages/api/troll/[clan]/[troll]/index.ts @@ -1,15 +1,20 @@ +import { ClanGET } from "@/lib/trollcall/api/clan"; +import { getSingleClan } from "@/lib/trollcall/clan"; +import { ServerFlairToClientFlair } from "@/lib/trollcall/convert/flair"; import { MergeServerTrolls, ServerTrollToClientTroll, SubmitTrollToServerTroll } from "@/lib/trollcall/convert/troll"; +import { getManyFlairs } from "@/lib/trollcall/flair"; import { compareCredentials, compareLevels, getLevel } from "@/lib/trollcall/perms"; import { changeTroll, getSingleTroll } from "@/lib/trollcall/troll"; -import { getSingleUser } from "@/lib/trollcall/user"; +import { cutArray } from "@/lib/trollcall/utility/merge"; +import { ClientClan } from "@/types/clan"; import { PartialTrollSchema, SubmitTroll } from "@/types/client/troll"; import { NextApiRequest, NextApiResponse } from "next"; @@ -19,16 +24,24 @@ export default async function handler( ) { const { query, cookies, method, body } = req; if (method === "GET") { - const user = await getSingleUser({ - name: query.user + const clan = await getSingleClan({ + name: query.clan }); - if (user == null) return res.status(404).end(); + if (clan == null) return res.status(404).end(); const troll = await getSingleTroll({ "name.0": query.troll, - "owners.0": user._id + "owner": clan._id }); if (troll == null) return res.status(404).end(); const serverTroll = await ServerTrollToClientTroll(troll); + serverTroll.flairs = cutArray( + await getManyFlairs( + { _id: { $in: troll.flairs } }, + ServerFlairToClientFlair + ) + ); + // we know this is not null, as we passed in our own clan + serverTroll.owner = (await ClanGET(null, clan)) as ClientClan; res.json(serverTroll); } else if (method === "PUT") { let validatedTroll: Partial; @@ -39,23 +52,23 @@ export default async function handler( } catch (err) { return res.status(400).send(err); } - const checkUser = await getSingleUser({ - name: query.user + const checkClan = await getSingleClan({ + name: query.clan }); - if (checkUser == null) return res.status(404).end(); - if (!compareCredentials(checkUser, cookies)) { - const thisUser = await getSingleUser({ + if (checkClan == null) return res.status(404).end(); + if (!compareCredentials(checkClan, cookies)) { + const thisClan = await getSingleClan({ name: cookies.TROLLCALL_NAME }); - if (thisUser == null || !compareCredentials(thisUser, cookies)) + if (thisClan == null || !compareCredentials(thisClan, cookies)) return res.status(403).end(); - console.log(getLevel(thisUser)); - if (!compareLevels(getLevel(thisUser), "MODERATOR")) + console.log(getLevel(thisClan)); + if (!compareLevels(getLevel(thisClan), "MODERATOR")) return res.status(403).end(); } const editingTroll = await getSingleTroll({ "name.0": query.troll, - "owners.0": checkUser._id + "owner": checkClan._id }); if (editingTroll == null) return res.status(404).end(); const serverTroll = SubmitTrollToServerTroll(validatedTroll); diff --git a/src/pages/api/troll/[user]/[troll]/server/index.ts b/src/pages/api/troll/[clan]/[troll]/server/index.ts similarity index 62% rename from src/pages/api/troll/[user]/[troll]/server/index.ts rename to src/pages/api/troll/[clan]/[troll]/server/index.ts index d3625cc..faa2625 100644 --- a/src/pages/api/troll/[user]/[troll]/server/index.ts +++ b/src/pages/api/troll/[clan]/[troll]/server/index.ts @@ -1,10 +1,10 @@ +import { getSingleClan } from "@/lib/trollcall/clan"; import { compareCredentials, compareLevels, getLevel } from "@/lib/trollcall/perms"; import { getSingleTroll } from "@/lib/trollcall/troll"; -import { getSingleUser } from "@/lib/trollcall/user"; import { NextApiRequest, NextApiResponse } from "next"; export default async function handler( @@ -13,22 +13,22 @@ export default async function handler( ) { const { query, cookies, method, body } = req; if (method === "GET") { - const user = await getSingleUser({ - name: query.user + const clan = await getSingleClan({ + name: query.clan }); - if (user == null) return res.status(404).end(); - if (!compareCredentials(user, cookies)) { - const thisUser = await getSingleUser({ + if (clan == null) return res.status(404).end(); + if (!compareCredentials(clan, cookies)) { + const thisClan = await getSingleClan({ name: cookies.TROLLCALL_NAME }); - if (thisUser == null || !compareCredentials(thisUser, cookies)) + if (thisClan == null || !compareCredentials(thisClan, cookies)) return res.status(403).end(); - if (!compareLevels(getLevel(thisUser), "MODERATOR")) + if (!compareLevels(getLevel(thisClan), "MODERATOR")) return res.status(403).end(); } const troll = await getSingleTroll({ "name.0": query.troll, - "owners.0": user._id + "owner": clan._id }); if (troll == null) return res.status(404).end(); res.json(troll); diff --git a/src/pages/api/troll/index.ts b/src/pages/api/troll/index.ts index 9254d9c..88428e8 100644 --- a/src/pages/api/troll/index.ts +++ b/src/pages/api/troll/index.ts @@ -1,7 +1,7 @@ +import { getSingleClan } from "@/lib/trollcall/clan"; import { SubmitTrollToServerTroll } from "@/lib/trollcall/convert/troll"; import { compareCredentials } from "@/lib/trollcall/perms"; import { createTroll, getSingleTroll } from "@/lib/trollcall/troll"; -import { getSingleUser } from "@/lib/trollcall/user"; import { SubmitTrollSchema } from "@/types/client/troll"; import { ServerTroll } from "@/types/troll"; import { NextApiRequest, NextApiResponse } from "next"; @@ -20,15 +20,15 @@ export default async function handler( } catch (err) { return res.status(400).send(err); } - const checkUser = await getSingleUser({ + const checkClan = await getSingleClan({ name: cookies.TROLLCALL_NAME }); - if (checkUser == null) return res.status(404).end(); - if (!compareCredentials(checkUser, cookies)) + if (checkClan == null) return res.status(404).end(); + if (!compareCredentials(checkClan, cookies)) return res.status(403).end(); const checkExistingTroll = await getSingleTroll({ "name.0": validatedTroll.name[0], - "owners": checkUser._id + "owner": checkClan._id }); if (checkExistingTroll != null) return res.status(409).end(); // we are sure this object is full, so cast partial @@ -36,7 +36,7 @@ export default async function handler( ServerTroll, "_id" >; - serverTroll.owners[0] = checkUser._id; + serverTroll.owner = checkClan._id; const newTroll = await createTroll(serverTroll); if (newTroll == null) return res.status(503).end(); res.json(newTroll); diff --git a/src/pages/api/user/[user]/index.ts b/src/pages/api/user/[user]/index.ts deleted file mode 100644 index dc9729d..0000000 --- a/src/pages/api/user/[user]/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - MergeServerUsers, - ServerUserToClientUser, - SubmitUserToServerUser -} from "@/lib/trollcall/convert/user"; -import { - compareCredentials, - compareLevels, - getLevel -} from "@/lib/trollcall/perms"; -import { changeUser, getSingleUser } from "@/lib/trollcall/user"; -import { PartialUserSchema, SubmitUser } from "@/types/client/user"; -import { serialize } from "cookie"; -import { nanoid } from "nanoid"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { body, cookies, query, method } = req; - if (method === "GET") { - const user = await getSingleUser({ - name: query.user - }); - if (user == null) return res.status(404).end(); - const serverUser = await ServerUserToClientUser(user); - res.json(serverUser); - } else if (method === "PUT") { - let validatedUser: Partial; - try { - validatedUser = (await PartialUserSchema.validate(body, { - stripUnknown: true - })) as Partial; - } catch (err) { - return res.status(400).send(err); - } - const checkExistingUser = await getSingleUser({ - name: query.user - }); - if (checkExistingUser == null) return res.status(404).end(); - let isModerator = false; - if (!compareCredentials(checkExistingUser, cookies)) { - const thisUser = await getSingleUser({ - name: cookies.TROLLCALL_NAME - }); - if (thisUser == null || !compareCredentials(thisUser, cookies)) - return res.status(403).end(); - if (!compareLevels(getLevel(thisUser), "MODERATOR")) - return res.status(403).end(); - isModerator = true; - } - const serverUser = SubmitUserToServerUser(validatedUser); - if (serverUser.code === "") - serverUser.code = checkExistingUser.code || nanoid(16); - if (!compareLevels(getLevel(checkExistingUser), "SUPPORTER")) { - serverUser.bgimage = null; - serverUser.css = null; - } - const bothUsers = MergeServerUsers(checkExistingUser, serverUser); - const newUser = await changeUser(bothUsers); - if (newUser == null) return res.status(503).end(); - // Give cookies, redundant style - if (!isModerator) - // don't set cookies if moderator is changing credentials - res.setHeader("Set-Cookie", [ - serialize("TROLLCALL_NAME", newUser.name, { - path: "/", - maxAge: 31540000 - }), - serialize("TROLLCALL_CODE", newUser.code, { - path: "/", - maxAge: 31540000 - }), - serialize("TROLLCALL_PFP", newUser.pfp ?? "", { - path: "/", - maxAge: 31540000 - }) - ]).json(newUser); - else res.json(newUser); - } else return res.status(405).end(); -} diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts deleted file mode 100644 index 5f10f5f..0000000 --- a/src/pages/api/user/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { SubmitUserToServerUser } from "@/lib/trollcall/convert/user"; -import { compareLevels, getLevel } from "@/lib/trollcall/perms"; -import { createUser, getSingleUser } from "@/lib/trollcall/user"; -import { SubmitUserSchema } from "@/types/client/user"; -import { ServerUser } from "@/types/user"; -import { serialize } from "cookie"; -import { nanoid } from "nanoid"; -import { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const { body, method } = req; - if (method === "POST") { - let validatedUser; - try { - validatedUser = await SubmitUserSchema.validate(body, { - stripUnknown: true - }); - } catch (err) { - return res.status(400).send(err); - } - const checkExistingUser = await getSingleUser({ - name: validatedUser.name - }); - if (checkExistingUser != null) return res.status(409).end(); - // we are sure this object is full, so cast partial - const serverUser = SubmitUserToServerUser(validatedUser) as Omit< - ServerUser, - "_id" - >; - if (serverUser.code === "") serverUser.code = nanoid(16); - if (!compareLevels(getLevel(serverUser), "SUPPORTER")) - serverUser.bgimage = null; - const newUser = await createUser(serverUser); - if (newUser == null) return res.status(503).end(); - // Give cookies - res.setHeader("Set-Cookie", [ - serialize("TROLLCALL_NAME", newUser.name, { - path: "/", - maxAge: 31540000 - }), - serialize("TROLLCALL_CODE", newUser.code, { - path: "/", - maxAge: 31540000 - }), - serialize("TROLLCALL_PFP", newUser.pfp ?? "", { - path: "/", - maxAge: 31540000 - }) - ]); - res.json(newUser); - } else return res.status(405).end(); -} diff --git a/src/types/clan.ts b/src/types/clan.ts new file mode 100644 index 0000000..0dbb6c2 --- /dev/null +++ b/src/types/clan.ts @@ -0,0 +1,20 @@ +import { WithId } from "@/lib/db/crud"; +import * as yup from "yup"; +import { ObjectIdSchema } from "./assist/mongo"; +import { SubmitClanSchema } from "./client/clan"; +import { ClientFlairSchema } from "./flair"; + +export const ServerClanSchema = SubmitClanSchema.shape({ + flairs: yup.array().of(ObjectIdSchema.required()).required(), + code: yup.string().required().max(10000), + updatedDate: yup.date().notRequired() +}); + +export type ServerClan = WithId>; + +export const ClientClanSchema = SubmitClanSchema.shape({ + flairs: yup.array().of(ClientFlairSchema.required()).required(), + updatedDate: yup.number().notRequired() +}); + +export type ClientClan = yup.InferType; diff --git a/src/types/client/clan.ts b/src/types/client/clan.ts new file mode 100644 index 0000000..1ac411c --- /dev/null +++ b/src/types/client/clan.ts @@ -0,0 +1,139 @@ +import * as yup from "yup"; +import { PolicySchema } from "../assist/generics"; + +export const SubmitClanSchema = yup + .object({ + name: yup + .string() + .required() + .matches(/^[\w-]+$/, "No special characters or spaces") + .min(3) + .max(50) + .lowercase(), + members: yup + .array() + .of( + yup + .object({ + name: yup.string().required().min(3).max(50), + pronouns: yup + .array() + .of( + yup.tuple([ + yup + .string() + .required() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase(), // she, he, they + yup + .string() + .required() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase(), // her, him, them + yup + .string() + .required() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase() // hers, his, theirs + ]) + ) + .required() + .min(1) + }) + .required() + ) + .required() + .min(1) + .max(20), + description: yup.string().max(10000).ensure(), + url: yup.string().notRequired().url(), + color: yup + .tuple([ + yup.number().min(0).max(255), + yup.number().min(0).max(255), + yup.number().min(0).max(255) + ]) + .notRequired(), + policies: yup + .object({ + fanart: PolicySchema.required(), + fanartOthers: PolicySchema.required(), + kinning: PolicySchema.required(), + shipping: PolicySchema.required(), + fanfiction: PolicySchema.required() + }) + .required(), + pfp: yup.string().notRequired().url(), + bgimage: yup.string().notRequired().url(), + css: yup.string().notRequired(), + code: yup.string().notRequired().max(256, "Too secure!!") + // flairs: yup.array().of(ClientFlairSchema).required(), + }) + .required(); + +export type SubmitClan = yup.InferType; + +export const PartialClanSchema = yup + .object({ + name: yup + .string() + .matches(/^[\w-]+$/, "No special characters or spaces") + .min(3) + .max(50) + .lowercase(), + members: yup + .array() + .of( + yup.object({ + name: yup.string().min(3).max(50), + pronouns: yup + .array() + .of( + yup.tuple([ + yup + .string() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase(), // she, he, they + yup + .string() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase(), // her, him, them + yup + .string() + .matches(/^[A-z]+$/, "Letters only") + .max(10) + .lowercase() // hers, his, theirs + ]) + ) + .min(1) + }) + ) + .min(1), + description: yup.string().max(10000), + url: yup.string().url(), + color: yup.tuple([ + yup.number().min(0).max(255), + yup.number().min(0).max(255), + yup.number().min(0).max(255) + ]), + policies: yup.object({ + fanart: PolicySchema, + fanartOthers: PolicySchema, + kinning: PolicySchema, + shipping: PolicySchema, + fanfiction: PolicySchema + }), + pfp: yup.string().url(), + bgimage: yup.string().url(), + css: yup.string(), + code: yup.string().max(256, "Too secure!!") + // flairs: yup.array().of(ClientFlairSchema).required(), + }) + .required(); + +export type PartialClan = yup.InferType; diff --git a/src/types/client/troll.ts b/src/types/client/troll.ts index 70dcbab..a04d8dc 100644 --- a/src/types/client/troll.ts +++ b/src/types/client/troll.ts @@ -31,12 +31,14 @@ export const SubmitTrollSchema = yup .string() .required() .matches(/^[A-z-]+$/, "Letters only") - .lowercase(), + .lowercase() + .max(50), yup .string() .required() .matches(/^[A-z-]+$/, "Letters only") .lowercase() + .max(50) ]) .required(), pronouns: yup @@ -137,8 +139,6 @@ export const SubmitTrollSchema = yup shipping: PolicySchema.required(), fanfiction: PolicySchema.required() }) - // owners: yup.array().of(yup.string().required()).required().min(1), - // flairs: yup.array().of(yup.mixed()).required().ensure(), }) .required(); diff --git a/src/types/client/user.ts b/src/types/client/user.ts deleted file mode 100644 index bcb7404..0000000 --- a/src/types/client/user.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as yup from "yup"; -import { TrueSignKeys } from "../assist/extended_zodiac"; -import { PolicySchema } from "../assist/generics"; - -export const SubmitUserSchema = yup - .object({ - name: yup - .string() - .required() - .matches(/^[\w-]+$/, "No special characters or spaces") - .min(3) - .max(50) - .lowercase(), - description: yup.string().max(10000).ensure(), - url: yup.string().notRequired().url(), - trueSign: yup.string().notRequired().oneOf(TrueSignKeys), - pronouns: yup - .array() - .of( - yup - .tuple([ - yup - .string() - .required() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase(), // she, he, they - yup - .string() - .required() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase(), // her, him, them - yup - .string() - .required() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase() // hers, his, theirs - ]) - .required() - ) - .required() - .min(1), - color: yup - .tuple([ - yup.number().min(0).max(255), - yup.number().min(0).max(255), - yup.number().min(0).max(255) - ]) - .notRequired(), - policies: yup - .object({ - fanart: PolicySchema.required(), - fanartOthers: PolicySchema.required(), - kinning: PolicySchema.required(), - shipping: PolicySchema.required(), - fanfiction: PolicySchema.required() - }) - .required(), - pfp: yup.string().notRequired().url(), - bgimage: yup.string().notRequired().url(), - css: yup.string().notRequired(), - code: yup.string().notRequired().max(256, "Too secure!!") - // flairs: yup.array().of(ClientFlairSchema).required(), - }) - .required(); - -export type SubmitUser = yup.InferType; - -export const PartialUserSchema = yup - .object({ - name: yup - .string() - .matches(/^[\w-]+$/, "No special characters or spaces") - .min(3) - .max(50) - .lowercase(), - description: yup.string().max(10000), - url: yup.string().url(), - trueSign: yup - .string() - .nullable() - .transform(v => { - return v === "" ? null : v; - }) - .oneOf(TrueSignKeys), - pronouns: yup - .array() - .of( - yup.tuple([ - yup - .string() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase(), // she, he, they - yup - .string() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase(), // her, him, them - yup - .string() - .matches(/^[A-z]+$/, "Letters only") - .min(1) - .max(10) - .lowercase() // hers, his, theirs - ]) - ) - .min(1), - color: yup.tuple([ - yup.number().min(0).max(255), - yup.number().min(0).max(255), - yup.number().min(0).max(255) - ]), - policies: yup.object({ - fanart: PolicySchema, - fanartOthers: PolicySchema, - kinning: PolicySchema, - shipping: PolicySchema, - fanfiction: PolicySchema - }), - pfp: yup.string().url(), - bgimage: yup.string().url(), - css: yup.string(), - code: yup.string().max(256, "Too secure!!") - // flairs: yup.array().of(ClientFlairSchema).required(), - }) - .required(); - -export type PartialUser = yup.InferType; diff --git a/src/types/dialoglog.ts b/src/types/dialoglog.ts index ba582e7..b0839a3 100644 --- a/src/types/dialoglog.ts +++ b/src/types/dialoglog.ts @@ -1,9 +1,9 @@ import { WithId } from "@/lib/db/crud"; import * as yup from "yup"; import { ObjectIdSchema } from "./assist/mongo"; +import { ClientClanSchema } from "./clan"; import { SubmitDialogSchema } from "./client/dialoglog"; import { ClientTroll, ClientTrollSchema } from "./troll"; -import { ClientUserSchema } from "./user"; export const ServerDialogSchema = SubmitDialogSchema.shape({ owners: yup.array().of(ObjectIdSchema.required()).required().min(1), @@ -23,7 +23,7 @@ export const ServerDialogSchema = SubmitDialogSchema.shape({ export type ServerDialog = WithId>; export const ClientDialogSchema = SubmitDialogSchema.shape({ - owners: yup.array().of(ClientUserSchema.required()).required().min(1), + owners: yup.array().of(ClientClanSchema.required()).required().min(1), characters: yup .array() .of( diff --git a/src/types/troll.ts b/src/types/troll.ts index 605b9ec..b9be576 100644 --- a/src/types/troll.ts +++ b/src/types/troll.ts @@ -2,13 +2,13 @@ import { WithId } from "@/lib/db/crud"; import * as yup from "yup"; import { ClassSchema, TrueSignSchema } from "./assist/extended_zodiac"; import { ObjectIdSchema } from "./assist/mongo"; +import { ClientClanSchema } from "./clan"; import { SubmitTrollSchema } from "./client/troll"; import { ClientFlairSchema } from "./flair"; import { ServerQuirkHolder, ServerQuirkHolderSchema } from "./quirks"; -import { ClientUserSchema } from "./user"; export const ServerTrollSchema = SubmitTrollSchema.shape({ - owners: yup.array().of(ObjectIdSchema.required()).required().min(1), + owner: ObjectIdSchema.required(), flairs: yup.array().of(ObjectIdSchema.required()).required(), quirks: ServerQuirkHolderSchema.required(), updatedDate: yup.date().notRequired() @@ -17,7 +17,7 @@ export const ServerTrollSchema = SubmitTrollSchema.shape({ export type ServerTroll = WithId>; export const ClientTrollSchema = SubmitTrollSchema.shape({ - owners: yup.array().of(ClientUserSchema.required()).required().min(1), + owner: ClientClanSchema.required(), flairs: yup.array().of(ClientFlairSchema.required()).required(), quirks: ServerQuirkHolderSchema.required(), trueSign: TrueSignSchema.notRequired(), diff --git a/src/types/user.ts b/src/types/user.ts deleted file mode 100644 index 512ea9f..0000000 --- a/src/types/user.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { WithId } from "@/lib/db/crud"; -import * as yup from "yup"; -import { TrueSignSchema } from "./assist/extended_zodiac"; -import { ObjectIdSchema } from "./assist/mongo"; -import { SubmitUserSchema } from "./client/user"; -import { ClientFlairSchema } from "./flair"; - -export const ServerUserSchema = SubmitUserSchema.shape({ - flairs: yup.array().of(ObjectIdSchema.required()).required(), - code: yup.string().required(), - updatedDate: yup.date().notRequired() -}); - -export type ServerUser = WithId>; - -export const ClientUserSchema = SubmitUserSchema.shape({ - flairs: yup.array().of(ClientFlairSchema.required()).required(), - trueSign: TrueSignSchema.notRequired(), - updatedDate: yup.number().notRequired() -}); - -export type ClientUser = yup.InferType;