permissions!

This commit is contained in:
MeowcaTheoRange 2023-06-30 20:35:15 -05:00
parent 8cde6799df
commit 6a5a85c773
33 changed files with 683 additions and 666 deletions

View file

@ -2,7 +2,7 @@
"trailingComma": "none",
"tabWidth": 4,
"useTabs": false,
"printWidth": 120,
"printWidth": 80,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",

View file

@ -1,14 +1,15 @@
import { FindCursor, Filter as MDBFilter, ObjectId } from "mongodb";
import { FindCursor, Filter as MDBFilter, ObjectId, Sort } from "mongodb";
import { mainDB } from "./mongodb";
// Functionally identical to the MongoDB type functions, but this is here if you want to extend TrollCall to another NoSQL database.
// That would be hard, though, as some parts of the codebase touch raw MongoDB concepts.
export type WithId<TSchema> = Omit<TSchema, "_id"> & {
_id: ObjectId;
};
export type Document = { [key: string]: any };
export type Filter<TypeSchema> = MDBFilter<Document>;
export type Filter<T> = MDBFilter<Document>;
export async function createOne(collection: string, doc: any) {
const selectedCollection = mainDB.collection(collection);
@ -20,9 +21,13 @@ export async function readOne(collection: string, find: Filter<Document>) {
return await selectedCollection.findOne(find);
} // MongoDB and its finicky type safety
export function readMany(collection: string, find: Filter<Document>) {
export function readMany(
collection: string,
find: Filter<Document>,
sort: Sort
) {
const selectedCollection = mainDB.collection(collection);
return selectedCollection.find(find);
return selectedCollection.find(find).sort(sort);
}
export async function countMany(collection: string, find: any) {
@ -45,7 +50,10 @@ export async function deleteOne(collection: string, find: any) {
return await selectedCollection.findOneAndDelete(find);
}
export async function cursorToArray<T>(cursor: FindCursor<T>, func: (input: any) => any = x => x) {
export async function cursorToArray<T>(
cursor: FindCursor<T>,
func: (input: any) => any = x => x
) {
let array: T[] = [];
while (await cursor.hasNext()) {
array.push(await func(await cursor.next()));

View file

@ -1,7 +1,9 @@
import { ClientFlair, ServerFlair } from "@/types/flair";
import { sanitize } from "../utility/merge";
export async function ServerFlairToClientFlair(serverFlair: ServerFlair): Promise<ClientFlair> {
export async function ServerFlairToClientFlair(
serverFlair: ServerFlair
): Promise<ClientFlair> {
const sanitizedFlair = sanitize(serverFlair);
let clientFlair: ClientFlair = {
...sanitizedFlair

View file

@ -1,23 +1,56 @@
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 { getManyUsers } from "../user";
import { cutArray, sanitize } from "../utility/merge";
import { cutArray, cutObject, sanitize } from "../utility/merge";
import { ServerFlairToClientFlair } from "./flair";
import { ServerUserToClientUser } from "./user";
export async function ServerTrollToClientTroll(serverTroll: ServerTroll): Promise<ClientTroll> {
export async function ServerTrollToClientTroll(
serverTroll: ServerTroll
): Promise<ClientTroll> {
const sanitizedTroll = sanitize(serverTroll);
const owners = await getManyUsers({ _id: { $in: serverTroll.owners } }, ServerUserToClientUser);
const flairs = await getManyFlairs({ _id: { $in: serverTroll.flairs } }, ServerFlairToClientFlair);
const flairs = await getManyFlairs(
{ _id: { $in: serverTroll.flairs } },
ServerFlairToClientFlair
);
let clientTroll: ClientTroll = {
...sanitizedTroll,
trueSign: TrueSign[serverTroll.trueSign],
falseSign: serverTroll.falseSign != null ? TrueSign[serverTroll.falseSign] : null,
falseSign:
serverTroll.falseSign != null
? TrueSign[serverTroll.falseSign]
: null,
class: Class[serverTroll.class],
owners: cutArray(owners),
owners: [],
flairs: cutArray(flairs)
};
return clientTroll;
}
export function SubmitTrollToServerTroll(
submitTroll: Partial<SubmitTroll>
): Omit<Partial<ServerTroll>, "_id"> {
let serverTroll: Omit<Partial<ServerTroll>, "_id"> = {
...submitTroll,
quirks: submitTroll.quirks
? Object.fromEntries(submitTroll.quirks)
: undefined,
owners: [],
flairs: [],
updatedDate: new Date()
};
return serverTroll;
}
export function MergeServerTrolls(
submitTroll: ServerTroll,
merge: Partial<Omit<ServerTroll, "_id">>
): ServerTroll {
let serverTroll: ServerTroll = {
...submitTroll,
...cutObject(merge),
updatedDate: new Date()
};
return serverTroll;
}

View file

@ -6,9 +6,14 @@ import { getManyFlairs } from "../flair";
import { cutArray, cutObject, removeCode, sanitize } from "../utility/merge";
import { ServerFlairToClientFlair } from "./flair";
export async function ServerUserToClientUser(serverUser: ServerUser): Promise<ClientUser> {
export async function ServerUserToClientUser(
serverUser: ServerUser
): Promise<ClientUser> {
const sanitizedUser = removeCode(sanitize(serverUser));
const flairs = await getManyFlairs({ _id: { $in: serverUser.flairs } }, ServerFlairToClientFlair);
const flairs = await getManyFlairs(
{ _id: { $in: serverUser.flairs } },
ServerFlairToClientFlair
);
let clientUser: ClientUser = {
...sanitizedUser,
trueSign: TrueSign[serverUser.trueSign],
@ -18,7 +23,9 @@ export async function ServerUserToClientUser(serverUser: ServerUser): Promise<Cl
return clientUser;
}
export function SubmitUserToServerUser(submitUser: Partial<SubmitUser>): Omit<Partial<ServerUser>, "_id"> {
export function SubmitUserToServerUser(
submitUser: Partial<SubmitUser>
): Omit<Partial<ServerUser>, "_id"> {
let serverUser: Omit<Partial<ServerUser>, "_id"> = {
...submitUser,
flairs: [],
@ -28,7 +35,10 @@ export function SubmitUserToServerUser(submitUser: Partial<SubmitUser>): Omit<Pa
return serverUser;
}
export function MergeServerUsers(submitUser: ServerUser, merge: Partial<Omit<ServerUser, "_id">>): ServerUser {
export function MergeServerUsers(
submitUser: ServerUser,
merge: Partial<Omit<ServerUser, "_id">>
): ServerUser {
let serverUser: ServerUser = {
...submitUser,
...cutObject(merge),

View file

@ -1,13 +1,18 @@
import { ServerFlair } from "@/types/flair";
import { Sort } from "mongodb";
import { Filter, cursorToArray, readMany, readOne } from "../db/crud";
const FlairSort: Sort = { updatedDate: -1, _id: -1 };
/**
* A function that returns one ServerFlairs from the database.
* @param query A partial Find query. Can contain an ID.
* @returns A ServerFlair.
*/
export async function getSingleFlair(query: Filter<ServerFlair>): Promise<ServerFlair | null> {
export async function getSingleFlair(
query: Filter<ServerFlair>
): Promise<ServerFlair | null> {
const flair = (await readOne("flairs", query)) as ServerFlair | null;
return flair;
}
@ -23,6 +28,9 @@ export async function getManyFlairs(
query: Filter<ServerFlair>,
func?: (input: any) => any
): Promise<(ServerFlair | null)[]> {
const flair = (await cursorToArray(readMany("flairs", query), func)) as (ServerFlair | null)[];
const flair = (await cursorToArray(
readMany("flairs", query, FlairSort),
func
)) as (ServerFlair | null)[];
return flair;
}

View file

@ -0,0 +1,27 @@
import { Levels, Permissions } from "@/permissions";
import { ServerUser } from "@/types/user";
export function getLevel(user: ServerUser) {
let highestLevel = "USER";
for (let level of Permissions) {
if (user.flairs.some(oid => level.values.includes(oid.toString())))
highestLevel = level.name;
}
return highestLevel;
}
export function compareLevels(level: string, compareLevel: string) {
return Levels[level] > Levels[compareLevel];
}
export function compareCredentials(
user: ServerUser,
cookies: Partial<{
[key: string]: string;
}>
) {
return (
user.code === cookies.TROLLCALL_CODE &&
user.name === cookies.TROLLCALL_NAME
);
}

View file

@ -1,5 +1,15 @@
import { ServerTroll } from "@/types/troll";
import { Filter, cursorToArray, readMany, readOne } from "../db/crud";
import { Sort } from "mongodb";
import {
Filter,
createOne,
cursorToArray,
readMany,
readOne,
replaceOne
} from "../db/crud";
const TrollSort: Sort = { updatedDate: -1, _id: -1 };
/**
* A function that returns one ServerTrolls from the database.
@ -7,7 +17,9 @@ import { Filter, cursorToArray, readMany, readOne } from "../db/crud";
* @returns A ServerTroll.
*/
export async function getSingleTroll(query: Filter<ServerTroll>): Promise<ServerTroll | null> {
export async function getSingleTroll(
query: Filter<ServerTroll>
): Promise<ServerTroll | null> {
const troll = (await readOne("trolls", query)) as ServerTroll | null;
return troll;
}
@ -19,10 +31,59 @@ export async function getSingleTroll(query: Filter<ServerTroll>): Promise<Server
* @returns An array of ServerTrolls.
*/
export async function getManyTrolls(
export async function getManyTrolls<T>(
query: Filter<ServerTroll>,
func?: (input: any) => any
): Promise<(ServerTroll | null)[]> {
const troll = (await cursorToArray(readMany("trolls", query), func)) as (ServerTroll | null)[];
func?: (input: any) => T
): Promise<(Awaited<T> | null)[]> {
const troll = (await cursorToArray(
readMany("trolls", query, TrollSort),
func
)) as (Awaited<T> | null)[];
return troll;
}
/**
* A function that returns many ServerTrolls 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 ServerTroll returned. Helps reduce loops.
* @returns An array of ServerTrolls.
*/
export async function getManyPagedTrolls<T>(
query: Filter<ServerTroll>,
func?: (input: any) => T,
count: number = 5,
page: number = 0
): Promise<(Awaited<T> | null)[]> {
const find = readMany("trolls", query, TrollSort)
.limit(count)
.skip(page * count);
const user = (await cursorToArray(find, func)) as (Awaited<T> | null)[];
return user;
}
/**
* A function that puts one ServerTroll into the database.
* @param troll A ServerTroll.
* @returns A ServerTroll, or null, depending on if the operation succeeded.
*/
export async function createTroll(
troll: Omit<ServerTroll, "_id">
): Promise<Omit<ServerTroll, "_id"> | null> {
const newTroll = await createOne("trolls", troll);
return newTroll.acknowledged ? troll : null;
}
/**
* A function that changes one database troll with the given params.
* @param troll A ServerTroll.
* @returns A ServerTroll, or null, depending on if the operation succeeded.
*/
export async function changeTroll(
troll: ServerTroll
): Promise<ServerTroll | null> {
const newTroll = await replaceOne("trolls", { _id: troll._id }, troll);
return newTroll.acknowledged ? troll : null;
}

View file

@ -1,5 +1,15 @@
import { ServerUser } from "@/types/user";
import { Filter, createOne, cursorToArray, readMany, readOne, replaceOne } from "../db/crud";
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.
@ -7,7 +17,9 @@ import { Filter, createOne, cursorToArray, readMany, readOne, replaceOne } from
* @returns A ServerUser.
*/
export async function getSingleUser(query: Filter<ServerUser>): Promise<ServerUser | null> {
export async function getSingleUser(
query: Filter<ServerUser>
): Promise<ServerUser | null> {
const user = (await readOne("users", query)) as ServerUser | null;
return user;
}
@ -23,7 +35,30 @@ export async function getManyUsers<T>(
query: Filter<ServerUser>,
func?: (input: any) => T
): Promise<(Awaited<T> | null)[]> {
const user = (await cursorToArray(readMany("users", query), func)) as (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;
}
@ -33,7 +68,9 @@ export async function getManyUsers<T>(
* @returns A ServerUser, or null, depending on if the operation succeeded.
*/
export async function createUser(user: Omit<ServerUser, "_id">): Promise<Omit<ServerUser, "_id"> | null> {
export async function createUser(
user: Omit<ServerUser, "_id">
): Promise<Omit<ServerUser, "_id"> | null> {
const newUser = await createOne("users", user);
return newUser.acknowledged ? user : null;
}

View file

@ -25,13 +25,17 @@ export function cutObject<T extends { [key: string]: any }>(object: T) {
return cut as T;
}
export function sanitize<Type extends WithId<{}>>(serverType: Type): Omit<Type, "_id"> {
export function sanitize<Type extends WithId<{}>>(
serverType: Type
): Omit<Type, "_id"> {
const sanitized: Partial<Type> = serverType;
delete sanitized._id;
return sanitized as Omit<Type, "_id">;
}
export function removeCode<Type extends { code: string }>(serverType: Type): Omit<Type, "code"> {
export function removeCode<Type extends { code: string }>(
serverType: Type
): Omit<Type, "code"> {
const sanitized: Partial<Type> = serverType;
delete sanitized.code;
return sanitized as Omit<Type, "code">;

View file

@ -1,4 +1,3 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import React from "react";

3
src/pages/_error.tsx Normal file
View file

@ -0,0 +1,3 @@
export default function ErrorPage() {
return "404";
}

View file

@ -0,0 +1,21 @@
import { ServerTrollToClientTroll } from "@/lib/trollcall/convert/troll";
import { getManyPagedTrolls } from "@/lib/trollcall/troll";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { method, query } = req;
const page = query.page ? query.page[0] : 0;
if (method === "GET") {
const trolls = await getManyPagedTrolls(
{},
ServerTrollToClientTroll,
5,
page
);
if (trolls == null) return res.status(404).end();
res.json(trolls);
} else return res.status(405).end();
}

View file

@ -0,0 +1,28 @@
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(
req: NextApiRequest,
res: NextApiResponse
) {
const { method, query } = req;
const page = query.page ? query.page[0] : 0;
if (method === "GET") {
const user = await getSingleUser({
name: query.user
});
if (user == null) return res.status(404).end();
const trolls = await getManyPagedTrolls(
{
"owners.0": user._id
},
ServerTrollToClientTroll,
5,
page
);
if (trolls == null) return res.status(404).end();
res.json(trolls);
} else return res.status(405).end();
}

View file

@ -0,0 +1,73 @@
import {
MergeServerTrolls,
ServerTrollToClientTroll,
SubmitTrollToServerTroll
} from "@/lib/trollcall/convert/troll";
import {
compareCredentials,
compareLevels,
getLevel
} from "@/lib/trollcall/perms";
import { changeTroll, getSingleTroll } from "@/lib/trollcall/troll";
import { getSingleUser } from "@/lib/trollcall/user";
import { SubmitTroll } from "@/types/client/troll";
import { PartialUserSchema } from "@/types/client/user";
import { Router } from "express";
import { NextApiRequest, NextApiResponse } from "next";
export const trollRouter = Router();
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { query, cookies, method, body } = req;
if (method === "GET") {
const user = await getSingleUser({
name: query.user
});
if (user == null) return res.status(404).end();
const troll = await getSingleTroll({
"name.0": query.troll,
"owners.0": user._id
});
if (troll == null) return res.status(404).end();
const serverTroll = await ServerTrollToClientTroll(troll);
res.json(serverTroll);
} else if (method === "PUT") {
let validatedTroll: Partial<SubmitTroll>;
try {
validatedTroll = (await PartialUserSchema.validate(
body
)) as Partial<SubmitTroll>;
} catch (err) {
return res.status(400).send(err);
}
const checkUser = await getSingleUser({
name: query.user
});
if (checkUser == null) return res.status(404).end();
if (!compareCredentials(checkUser, cookies)) {
const thisUser = await getSingleUser({
name: query.user
});
if (thisUser == null || !compareCredentials(thisUser, cookies))
return res.status(403).end();
if (!compareLevels(getLevel(thisUser), "MODERATOR"))
return res.status(403).end();
}
const editingTroll = await getSingleTroll({
"name.0": query.troll,
"owners": checkUser._id
});
if (editingTroll == null) return res.status(404).end();
const serverTroll = SubmitTrollToServerTroll(validatedTroll);
const bothTrolls = MergeServerTrolls(editingTroll, serverTroll);
const newTroll = await changeTroll(bothTrolls);
if (newTroll == null) return res.status(503).end();
res.json(newTroll);
} else return res.status(405).end();
}
// 1119731948118605995
// 1089603977131331584

View file

@ -0,0 +1,42 @@
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";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { body, method, cookies } = req;
if (method === "POST") {
let validatedTroll;
try {
validatedTroll = await SubmitTrollSchema.validate(body);
} catch (err) {
return res.status(400).send(err);
}
const checkUser = await getSingleUser({
name: cookies.TROLLCALL_NAME
});
if (checkUser == null) return res.status(404).end();
if (!compareCredentials(checkUser, cookies))
return res.status(403).end();
const checkExistingTroll = await getSingleTroll({
"name.0": validatedTroll.name[0],
"owners": checkUser._id
});
if (checkExistingTroll != null) return res.status(409).end();
// we are sure this object is full, so cast partial
let serverTroll = SubmitTrollToServerTroll(validatedTroll) as Omit<
ServerTroll,
"_id"
>;
serverTroll.owners[0] = checkUser._id;
const newTroll = await createTroll(serverTroll);
if (newTroll == null) return res.status(503).end();
res.json(newTroll);
} else return res.status(405).end();
}

View file

@ -0,0 +1,21 @@
import { ServerUserToClientUser } from "@/lib/trollcall/convert/user";
import { getManyPagedUsers } from "@/lib/trollcall/user";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { method, query } = req;
const page = query.page ? query.page[0] : 0;
if (method === "GET") {
const users = await getManyPagedUsers(
{},
ServerUserToClientUser,
5,
page
);
if (users == null) return res.status(404).end();
res.json(users);
} else return res.status(405).end();
}

View file

@ -1,10 +1,22 @@
import { MergeServerUsers, ServerUserToClientUser, SubmitUserToServerUser } from "@/lib/trollcall/convert/user";
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 { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { body, cookies, query, method } = req;
if (method === "GET") {
const user = await getSingleUser({
@ -16,7 +28,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} else if (method === "PUT") {
let validatedUser: Partial<SubmitUser>;
try {
validatedUser = (await PartialUserSchema.validate(body)) as Partial<SubmitUser>;
validatedUser = (await PartialUserSchema.validate(
body
)) as Partial<SubmitUser>;
} catch (err) {
return res.status(400).send(err);
}
@ -24,16 +38,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
name: query.user
});
if (checkExistingUser == null) return res.status(404).end();
if (checkExistingUser.code !== cookies.TROLLCALL_CODE || checkExistingUser.name !== cookies.TROLLCALL_NAME)
if (!compareCredentials(checkExistingUser, cookies)) {
const thisUser = await getSingleUser({
name: query.user
});
if (thisUser == null || !compareCredentials(thisUser, cookies))
return res.status(403).end();
if (!compareLevels(getLevel(thisUser), "MODERATOR"))
return res.status(403).end();
}
const serverUser = SubmitUserToServerUser(validatedUser);
const bothUsers = MergeServerUsers(checkExistingUser, serverUser);
const newUser = await changeUser(bothUsers);
if (newUser == null) return res.status(503).end();
// Give cookies, redundant style
res.setHeader("Set-Cookie", [
serialize("TROLLCALL_NAME", newUser.name, { path: "/", maxAge: 31540000 }),
serialize("TROLLCALL_NAME", newUser.code, { path: "/", maxAge: 31540000 })
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 return res.status(405).end();
}

View file

@ -1,24 +0,0 @@
import { ServerTrollToClientTroll } from "@/lib/trollcall/convert/troll";
import { getSingleTroll } from "@/lib/trollcall/troll";
import { getSingleUser } from "@/lib/trollcall/user";
import { Router } from "express";
import { NextApiRequest, NextApiResponse } from "next";
export const trollRouter = Router();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { query, method } = req;
if (method === "GET") {
const user = await getSingleUser({
name: query.user
});
if (user == null) return res.status(404).end();
const troll = await getSingleTroll({
"name.0": query.troll,
"owners.0": user._id
});
if (troll == null) return res.status(404).end();
const serverTroll = await ServerTrollToClientTroll(troll);
res.json(serverTroll);
} else return res.status(405).end();
}

View file

@ -5,8 +5,12 @@ import { ServerUser } from "@/types/user";
import { serialize } from "cookie";
import { NextApiRequest, NextApiResponse } from "next";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { body } = req;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { body, method } = req;
if (method === "POST") {
let validatedUser;
try {
validatedUser = await SubmitUserSchema.validate(body);
@ -18,13 +22,27 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
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">;
const serverUser = SubmitUserToServerUser(validatedUser) as Omit<
ServerUser,
"_id"
>;
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_NAME", newUser.code, { path: "/", maxAge: 31540000 })
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();
}

View file

@ -1,115 +1,3 @@
import styles from "@/styles/Home.module.css";
import { Inter } from "next/font/google";
import Head from "next/head";
import Image from "next/image";
import React from "react";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
return (
<>
<Head>
<title>Create Next App</title>
<meta
name="description"
content="Generated by create next app"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<link
rel="icon"
href="/favicon.ico"
/>
</Head>
<main className={`${styles.main} ${inter.className}`}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
<code className={styles.code}>src/pages/index.tsx</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{" "}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className={styles.center}>
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Docs <span>-&gt;</span>
</h2>
<p>Find in-depth information about Next.js features and&nbsp;API.</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Learn <span>-&gt;</span>
</h2>
<p>Learn about Next.js in an interactive course with&nbsp;quizzes!</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Templates <span>-&gt;</span>
</h2>
<p>Discover and deploy boilerplate example Next.js&nbsp;projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2>
Deploy <span>-&gt;</span>
</h2>
<p>Instantly deploy your Next.js site to a shareable URL with&nbsp;Vercel.</p>
</a>
</div>
</main>
</>
);
return "Welcome home!";
}

24
src/permissions.ts Normal file
View file

@ -0,0 +1,24 @@
export const Permissions = [
{
name: "SUPPORTER",
values: ["6487ec00cd90481163238f79", "648b76878de421ccb5937f54"]
},
{
name: "MODERATOR",
values: ["649ef0a9c62f6a4c8eb3f98a"]
},
{
name: "ADMIN",
values: ["6488077200893ada4afdd09c"]
},
{
name: "OWNER",
values: ["6487eb5fcd90481163238f6a"]
}
];
export const Levels: { [key: string]: number } = {
"SUPPORTER": 0,
"MODERATOR": 1,
"ADMIN": 2,
"OWNER": 3
};

View file

@ -1,229 +0,0 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo {
position: relative;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

View file

@ -1,107 +0,0 @@
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}

View file

@ -5,7 +5,7 @@ export type ColorTypes = [number, number, number];
export const ColorSchema = yup.tuple([
yup.number().required().min(0).max(255),
yup.number().required().min(0).max(255),
yup.number().required().min(0).max(255),
yup.number().required().min(0).max(255)
]);
const clamp = (n: number, mi: number, ma: number) =>
@ -32,9 +32,9 @@ export class Color3 {
hex.match(new RegExp(`[0-9a-f]{1,${hex.length / 3}}`, "gi")) ?? [
"0",
"0",
"0",
"0"
]
).map((x) => parseInt(x, 16) / 255);
).map(x => parseInt(x, 16) / 255);
return new Color3(...hexSplit);
}
static fromInt(int: number) {
@ -63,7 +63,7 @@ export class Color3 {
return (
(Array.isArray(value) &&
value.length === 3 &&
value.every((x) => typeof x === "number" && !isNaN(x))) ||
value.every(x => typeof x === "number" && !isNaN(x))) ||
(typeof value === "string" && !isNaN(parseInt(value, 16))) ||
(typeof value === "number" && !isNaN(value))
);

View file

@ -316,7 +316,9 @@ export const SignColorSchema = yup.object({
description: yup.string().required(),
sign: yup.string().required(),
color: ColorSchema.required(),
dates: yup.tuple([yup.string().required(), yup.string().required()]).required()
dates: yup
.tuple([yup.string().required(), yup.string().required()])
.required()
});
export type SignColorType = {

View file

@ -37,7 +37,7 @@ export function ArraySample(array: any[]) {
export function ProperNounCase(string: string) {
return string
.split(" ")
.map((x) => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.join(" ");
}
@ -45,7 +45,9 @@ export function PesterchumNameFormatter(string: string) {
return (
string +
" [" +
string.replace(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "$2$4").toUpperCase() +
string
.replace(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "$2$4")
.toUpperCase() +
"]"
);
}

View file

@ -3,8 +3,17 @@ import { QuirkSchema } from "../quirks";
export const SubmitQuirkHolderSchema = yup
.array()
.of(yup.tuple([yup.string().required().lowercase(), QuirkSchema.required()]).required())
.of(
yup
.tuple([
yup.string().required().lowercase(),
QuirkSchema.required()
])
.required()
.test("has-default", 'Needs "default" Quirk Mode', v => v.some(([k, v]) => k === "default" || k === "Default"));
)
.required()
.test("has-default", 'Needs "default" Quirk Mode', v =>
v.some(([k, v]) => k === "default" || k === "Default")
);
export type SubmitQuirkHolder = yup.InferType<typeof SubmitQuirkHolderSchema>;

View file

@ -79,11 +79,26 @@ export const SubmitTrollSchema = yup
// Personal
preferences: yup
.object({
love: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10),
hate: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10)
love: yup
.array()
.of(yup.string().required().min(5).max(100))
.required()
.min(3)
.max(10),
hate: yup
.array()
.of(yup.string().required().min(5).max(100))
.required()
.min(3)
.max(10)
})
.required(),
facts: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10),
facts: yup
.array()
.of(yup.string().required().min(5).max(100))
.required()
.min(3)
.max(10),
// Hiveswap identity
trueSign: yup.string().required().oneOf(TrueSignKeys),
@ -100,7 +115,10 @@ export const SubmitTrollSchema = yup
username: yup
.string()
.required()
.matches(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "Username must match Pesterchum formatting."),
.matches(
/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/,
"Username must match Pesterchum formatting."
),
textColor: ColorSchema.notRequired(), // default to trueSign color if undefined,
quirks: SubmitQuirkHolderSchema.required(), // DO NOT HANDLE RIGHT NOW.
// Handled! :D
@ -208,7 +226,10 @@ export const PartialTrollSchema = yup
// Trollian
username: yup
.string()
.matches(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "Username must match Pesterchum formatting."),
.matches(
/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/,
"Username must match Pesterchum formatting."
),
textColor: yup.tuple([
yup.number().min(0).max(255),
yup.number().min(0).max(255),

View file

@ -40,7 +40,11 @@ export const PartialUserSchema = yup
return v === "" ? null : v;
})
.oneOf(TrueSignKeys),
color: yup.tuple([yup.number().min(0).max(255), yup.number().min(0).max(255), yup.number().min(0).max(255)]),
color: yup.tuple([
yup.number().min(0).max(255),
yup.number().min(0).max(255),
yup.number().min(0).max(255)
]),
pfp: yup.string().url(),
code: yup.string().max(256, "Too secure!!")
// flairs: yup.array().of(ClientFlairSchema).required(),

View file

@ -70,7 +70,8 @@ export const QuirkReplacementTypes: {
},
case: {
find: null,
replace: 'Change the case of all text. "lower" for lowercase. "upper" for uppercase.'
replace:
'Change the case of all text. "lower" for lowercase. "upper" for uppercase.'
},
case_simple: {
find: "Text to find what you want to change the case of.",