Switch from "Users" to "Clans"

This commit is contained in:
MeowcaTheoRange 2023-08-07 18:29:30 -05:00
parent b955477231
commit 28c27fb172
28 changed files with 581 additions and 500 deletions

View file

@ -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

12
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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, {});

View file

@ -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<ClientClan | null> {
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;
}

87
src/lib/trollcall/clan.ts Normal file
View file

@ -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<ServerClan>
): Promise<ServerClan | null> {
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<T>(
query: Filter<ServerClan>,
func?: (input: any) => T
): Promise<(Awaited<T> | null)[]> {
const clan = (await cursorToArray(
readMany("clans", query, ClanSort),
func
)) as (Awaited<T> | 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<T>(
query: Filter<ServerClan>,
func?: (input: any) => T,
count: number = 5,
page: number = 0
): Promise<(Awaited<T> | null)[]> {
const find = readMany("clans", query, ClanSort)
.limit(count)
.skip(page * count);
const clan = (await cursorToArray(find, func)) as (Awaited<T> | 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<ServerClan, "_id">
): Promise<Omit<ServerClan, "_id"> | 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<ServerClan | null> {
const newClan = await replaceOne("clans", { _id: clan._id }, clan);
return newClan.acknowledged ? clan : null;
}

View file

@ -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<Partial<ClientClan>> {
const sanitizedClan = removeCode(sanitize(serverClan));
let clientClan: Partial<ClientClan> = {
...sanitizedClan,
flairs: undefined,
updatedDate: serverClan.updatedDate?.getTime()
};
return clientClan;
}
export function SubmitClanToServerClan(
submitClan: Partial<SubmitClan>
): Omit<Partial<ServerClan>, "_id"> {
let serverClan: Omit<Partial<ServerClan>, "_id"> = {
...submitClan,
flairs: undefined,
code: submitClan.code || undefined,
updatedDate: new Date()
};
return serverClan;
}
export function MergeServerClans(
submitClan: ServerClan,
merge: Partial<Omit<ServerClan, "_id">>
): ServerClan {
let serverClan: ServerClan = {
...submitClan,
...cutObject(merge),
updatedDate: new Date()
};
return serverClan;
}

View file

@ -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<ClientTroll> {
): Promise<Partial<ClientTroll>> {
const sanitizedTroll = sanitize(serverTroll);
const flairs = await getManyFlairs(
{ _id: { $in: serverTroll.flairs } },
ServerFlairToClientFlair
);
let clientTroll: ClientTroll = {
let clientTroll: Partial<ClientTroll> = {
...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()
};

View file

@ -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<ClientUser> {
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<SubmitUser>
): Omit<Partial<ServerUser>, "_id"> {
let serverUser: Omit<Partial<ServerUser>, "_id"> = {
...submitUser,
flairs: undefined,
code: submitUser.code || "",
updatedDate: new Date()
};
return serverUser;
}
export function MergeServerUsers(
submitUser: ServerUser,
merge: Partial<Omit<ServerUser, "_id">>
): ServerUser {
let serverUser: ServerUser = {
...submitUser,
...cutObject(merge),
updatedDate: new Date()
};
return serverUser;
}

View file

@ -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<ServerUser> & { flairs: ObjectId[] }) {
let highestLevel = "USER";
export function getLevel(clan: Partial<ServerClan> & { 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
);
}

View file

@ -58,8 +58,8 @@ export async function getManyPagedTrolls<T>(
const find = readMany("trolls", query, TrollSort)
.limit(count)
.skip(page * count);
const user = (await cursorToArray(find, func)) as (Awaited<T> | null)[];
return user;
const clan = (await cursorToArray(find, func)) as (Awaited<T> | null)[];
return clan;
}
/**

View file

@ -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<ServerUser>
): Promise<ServerUser | null> {
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<T>(
query: Filter<ServerUser>,
func?: (input: any) => T
): Promise<(Awaited<T> | null)[]> {
const user = (await cursorToArray(
readMany("users", query, UserSort),
func
)) as (Awaited<T> | 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<T>(
query: Filter<ServerUser>,
func?: (input: any) => T,
count: number = 5,
page: number = 0
): Promise<(Awaited<T> | null)[]> {
const find = readMany("users", query, UserSort)
.limit(count)
.skip(page * count);
const user = (await cursorToArray(find, func)) as (Awaited<T> | 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<ServerUser, "_id">
): Promise<Omit<ServerUser, "_id"> | 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<ServerUser | null> {
const newUser = await replaceOne("users", { _id: user._id }, user);
return newUser.acknowledged ? user : null;
}

View file

@ -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();
}

View file

@ -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<SubmitClan>;
try {
validatedClan = (await PartialClanSchema.validate(body, {
stripUnknown: true
})) as Partial<SubmitClan>;
} 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();
}

View file

@ -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();
}

View file

@ -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,

View file

@ -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<SubmitTroll>;
@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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<SubmitUser>;
try {
validatedUser = (await PartialUserSchema.validate(body, {
stripUnknown: true
})) as Partial<SubmitUser>;
} 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();
}

View file

@ -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();
}

20
src/types/clan.ts Normal file
View file

@ -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<yup.InferType<typeof ServerClanSchema>>;
export const ClientClanSchema = SubmitClanSchema.shape({
flairs: yup.array().of(ClientFlairSchema.required()).required(),
updatedDate: yup.number().notRequired()
});
export type ClientClan = yup.InferType<typeof ClientClanSchema>;

139
src/types/client/clan.ts Normal file
View file

@ -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<typeof SubmitClanSchema>;
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<typeof PartialClanSchema>;

View file

@ -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();

View file

@ -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<typeof SubmitUserSchema>;
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<typeof PartialUserSchema>;

View file

@ -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<yup.InferType<typeof ServerDialogSchema>>;
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(

View file

@ -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<yup.InferType<typeof ServerTrollSchema>>;
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(),

View file

@ -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<yup.InferType<typeof ServerUserSchema>>;
export const ClientUserSchema = SubmitUserSchema.shape({
flairs: yup.array().of(ClientFlairSchema.required()).required(),
trueSign: TrueSignSchema.notRequired(),
updatedDate: yup.number().notRequired()
});
export type ClientUser = yup.InferType<typeof ClientUserSchema>;