initial commit
This commit is contained in:
commit
6f5a39c212
47 changed files with 3601 additions and 0 deletions
46
server/.gitignore
vendored
Normal file
46
server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
**/*.trace
|
||||
**/*.zip
|
||||
**/*.tar.gz
|
||||
**/*.tgz
|
||||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
|
||||
static/
|
||||
|
||||
node_modules
|
||||
.env
|
||||
BIN
server/bun.lockb
Executable file
BIN
server/bun.lockb
Executable file
Binary file not shown.
25
server/package.json
Normal file
25
server/package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "server",
|
||||
"version": "1.0.50",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/cors": "^1.0.2",
|
||||
"@elysiajs/eden": "^1.0.13",
|
||||
"@elysiajs/static": "^1.0.3",
|
||||
"@elysiajs/swagger": "^1.0.5",
|
||||
"@lucia-auth/adapter-prisma": "^4.0.1",
|
||||
"@prisma/client": "5.13.0",
|
||||
"elysia": "^1.0.16",
|
||||
"elysia-rate-limit": "^4.0.0",
|
||||
"logestic": "^1.1.1",
|
||||
"lucia": "^3.2.0",
|
||||
"prisma": "^5.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"module": "src/index.js"
|
||||
}
|
||||
43
server/prisma/schema.prisma
Normal file
43
server/prisma/schema.prisma
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Upload {
|
||||
id String @id
|
||||
expiresAt DateTime
|
||||
type String
|
||||
uploadedBy String
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
username String
|
||||
password String
|
||||
staff Boolean @default(false)
|
||||
|
||||
status String @default("")
|
||||
passkeys Json[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model Punishment {
|
||||
id String @id
|
||||
type String // "mute", "ban"
|
||||
reason String
|
||||
punishedUserId String
|
||||
staffId String
|
||||
time Int
|
||||
at DateTime
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
userId String
|
||||
expiresAt DateTime
|
||||
user User @relation(references: [id], fields: [userId], onDelete: Cascade)
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
38
server/src/db.ts
Normal file
38
server/src/db.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Lucia, TimeSpan } from "lucia";
|
||||
|
||||
export const client = new PrismaClient();
|
||||
|
||||
const adapter = new PrismaAdapter(client.session, client.user);
|
||||
export const lucia = new Lucia(adapter, {
|
||||
sessionExpiresIn: new TimeSpan(2, "w"), // 2 weeks
|
||||
|
||||
sessionCookie: {
|
||||
attributes: {
|
||||
secure: process.env.ENV === "PRODUCTION", // set `Secure` flag in HTTPS
|
||||
},
|
||||
},
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
username: attributes.username,
|
||||
password: attributes.password,
|
||||
staff: attributes.staff,
|
||||
status: attributes.status
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
declare module "lucia" {
|
||||
interface Register {
|
||||
Lucia: typeof lucia;
|
||||
DatabaseUserAttributes: DatabaseUserAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseUserAttributes {
|
||||
username: string;
|
||||
password: string;
|
||||
status: string;
|
||||
staff: boolean;
|
||||
}
|
||||
85
server/src/index.ts
Normal file
85
server/src/index.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
What if I never make it back from this damn panic attack?
|
||||
GTB sliding in a Hellcat bumping From First to Last
|
||||
When I die they're gonna make a park bench saying "This where he sat"
|
||||
Me and Yung Sherman going rehab, this shit is very sad
|
||||
Me and Yung Sherm in Venice Beach, man, this shit is very rad
|
||||
Me and Yung Sherman at the gym working out and getting tanned
|
||||
I never will see you again and I hope you understand
|
||||
I'm crashing down some like a wave over castles made of sand
|
||||
*/
|
||||
|
||||
// sad.ovh development
|
||||
|
||||
import cors from "@elysiajs/cors";
|
||||
import Elysia from "elysia";
|
||||
import auth from "./routes/auth";
|
||||
import ws from "./routes/ws";
|
||||
import profile from "./routes/profile";
|
||||
import { Logestic } from "logestic";
|
||||
import swagger from "@elysiajs/swagger";
|
||||
import staticPlugin from "@elysiajs/static";
|
||||
import { client } from "./db";
|
||||
import staff from "./routes/staff";
|
||||
import { mkdir } from "fs/promises";
|
||||
|
||||
try {
|
||||
await mkdir("static/pfps", { recursive: true });
|
||||
} catch {}
|
||||
try {
|
||||
await mkdir("static/images", { recursive: true });
|
||||
} catch {}
|
||||
|
||||
|
||||
const app = new Elysia()
|
||||
.use(Logestic.preset("common"))
|
||||
.use(
|
||||
swagger({
|
||||
documentation: {
|
||||
tags: [
|
||||
{
|
||||
name: "Auth",
|
||||
description: "All authenication routes have this tag.",
|
||||
},
|
||||
{ name: "Profile", description: "All profile routes have this tag." },
|
||||
{
|
||||
name: "Websocket",
|
||||
description: "All websocket routes have this tag.",
|
||||
},
|
||||
{ name: "Staff", description: "All staff routes have this tag." },
|
||||
{
|
||||
name: "Authenication not required",
|
||||
description: "Authenication isn't required",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
.use(
|
||||
cors({
|
||||
origin: (context) => {
|
||||
return /(http(|s):\/\/|)(sad\.ovh|127\.0\.0\.1|localhost)(:\d{1,4}|)(\/|)/gm.test(
|
||||
context.url
|
||||
); // TODO fix this
|
||||
},
|
||||
allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept",
|
||||
})
|
||||
)
|
||||
.use(
|
||||
staticPlugin({
|
||||
prefix: "",
|
||||
assets: "static",
|
||||
noCache: !(process.env.ENV === "PRODUCTION"),
|
||||
})
|
||||
)
|
||||
.use(auth)
|
||||
.use(ws)
|
||||
.use(profile)
|
||||
.use(staff)
|
||||
.listen(+process.env.PORT!);
|
||||
|
||||
export type App = typeof app;
|
||||
|
||||
console.log(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
74
server/src/lib.ts
Normal file
74
server/src/lib.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { client } from "./db";
|
||||
|
||||
import { unlink, mkdir, exists } from "fs/promises";
|
||||
|
||||
export async function setDeleteTimeout(
|
||||
entry: string | { id: string; expiresAt: Date; type: string }
|
||||
) {
|
||||
let upload: { id: string; expiresAt: Date; type: string };
|
||||
if (typeof entry == "string") {
|
||||
const x = await client.upload.findFirst({
|
||||
where: {
|
||||
id: entry,
|
||||
},
|
||||
});
|
||||
if (!x) {
|
||||
throw new Error(
|
||||
"A delete timeout was set for the " + entry + ", which doesn't exist!"
|
||||
);
|
||||
}
|
||||
upload = x;
|
||||
} else {
|
||||
upload = entry;
|
||||
}
|
||||
if (!(await exists("static/images/" + upload.id))) {
|
||||
console.log(
|
||||
"File with upload ID",
|
||||
upload.id,
|
||||
"was already deleted from FS, deleting it from DB."
|
||||
);
|
||||
await client.upload.delete({
|
||||
where: {
|
||||
id: upload.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = upload.expiresAt.getTime() - new Date().getTime();
|
||||
|
||||
if (diff < 0) {
|
||||
console.log(
|
||||
"File with upload ID",
|
||||
upload.id,
|
||||
"expired, which is being deleted."
|
||||
);
|
||||
|
||||
await unlink("static/images/" + upload.id);
|
||||
await client.upload.delete({
|
||||
where: {
|
||||
id: upload.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log("File ID", upload.id, "expiring in", diff + "ms.");
|
||||
setTimeout(async () => {
|
||||
console.log(
|
||||
"File with upload ID",
|
||||
upload.id,
|
||||
"reached expiration time, and it is being deleted"
|
||||
);
|
||||
|
||||
await unlink("static/images/" + upload.id);
|
||||
await client.upload.delete({
|
||||
where: {
|
||||
id: upload.id,
|
||||
},
|
||||
});
|
||||
}, diff);
|
||||
}
|
||||
|
||||
(await client.upload.findMany()).forEach(async (z) => {
|
||||
await setDeleteTimeout(z);
|
||||
});
|
||||
253
server/src/routes/auth.ts
Normal file
253
server/src/routes/auth.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { generateIdFromEntropySize, User } from "lucia";
|
||||
import { client, lucia } from "../db";
|
||||
import session, { ipGenerator } from "../session";
|
||||
import { rateLimit } from "elysia-rate-limit";
|
||||
|
||||
async function checkRecaptcha(
|
||||
recaptchaToken: string,
|
||||
action: "login" | "register"
|
||||
) {
|
||||
const recaptchaTest = await fetch(
|
||||
"https://www.google.com/recaptcha/api/siteverify?secret=" +
|
||||
process.env.RECAPTCHA_SECRET +
|
||||
"&response=" +
|
||||
encodeURIComponent(recaptchaToken),
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
const recaptchaJson = await recaptchaTest.json();
|
||||
|
||||
if (!recaptchaJson.success) {
|
||||
return new Response("Recaptcha failed.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (recaptchaJson.action != action) {
|
||||
return new Response("Wrong action.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
recaptchaJson.hostname == "localhost" &&
|
||||
process.env.ENV == "production"
|
||||
) {
|
||||
return new Response(
|
||||
"Incorrect captcha, this is a production environment!",
|
||||
{
|
||||
status: 400,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (recaptchaJson.score < 0.7) {
|
||||
return new Response("Suspicious request!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
export default new Elysia({
|
||||
prefix: "/api/auth",
|
||||
})
|
||||
.use(session)
|
||||
.get(
|
||||
"/userInfo",
|
||||
(context) => {
|
||||
if (!context.user) {
|
||||
return new Response(null, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
username: context.user.username,
|
||||
};
|
||||
},
|
||||
{
|
||||
response: t.MaybeEmpty(
|
||||
t.Object({
|
||||
username: t.String(),
|
||||
})
|
||||
),
|
||||
detail: {
|
||||
tags: ["Auth"],
|
||||
description: "Returns the username of the currently signed in user.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.group("", (a) =>
|
||||
a
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "local",
|
||||
duration: 1 * 24 * 60 * 1000, // 1 day
|
||||
max: 1,
|
||||
generator: ipGenerator(),
|
||||
})
|
||||
)
|
||||
.post(
|
||||
"/register",
|
||||
async (context) => {
|
||||
context;
|
||||
if (context.user as User) {
|
||||
return new Response("You're already signed in!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
username: context.body.username,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return new Response("User already exists!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.body.username.length > 30) {
|
||||
return new Response("Username greater than 30!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (context.body.username.length < 3) {
|
||||
return new Response("Username smaller than 3!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-_!.]*$/gm.test(context.body.username)) {
|
||||
return new Response("Username is incorrectly formatted!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (context.body.password.length < 5) {
|
||||
return new Response("Password smaller than 5!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const recaptcha = await checkRecaptcha(
|
||||
context.body.recaptcha,
|
||||
"register"
|
||||
);
|
||||
if (recaptcha) return recaptcha;
|
||||
|
||||
const hashedPassword = await Bun.password.hash(context.body.password);
|
||||
const id = generateIdFromEntropySize(10);
|
||||
|
||||
await client.user.create({
|
||||
data: {
|
||||
id,
|
||||
username: context.body.username,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
password: t.String(),
|
||||
username: t.String(),
|
||||
recaptcha: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Auth", "Authenication not required"],
|
||||
description:
|
||||
"Register endpoint. Requires a re-captcha token. This re-captcha token has to from our website.",
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
.get(
|
||||
"/logout",
|
||||
async (context) => {
|
||||
if (!context.session) {
|
||||
return new Response("You're not signed in.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
await lucia.invalidateSession(context.session?.id);
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
context.cookie[sessionCookie.name].set(sessionCookie.attributes);
|
||||
context.cookie[sessionCookie.name].value = sessionCookie.value;
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ["Auth"],
|
||||
description: "Logs out of the currently authenicated request.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.group("", (a) =>
|
||||
a
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "local",
|
||||
duration: 60 * 1000, // 1 minute
|
||||
max: 5, // 5 attempts
|
||||
generator: ipGenerator(),
|
||||
})
|
||||
)
|
||||
.post(
|
||||
"/login",
|
||||
|
||||
async (context) => {
|
||||
if (context.user) {
|
||||
return new Response("You're already signed in!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
username: context.body.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return new Response("There is no user with this username.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!(await Bun.password.verify(context.body.password, user.password))
|
||||
) {
|
||||
return new Response("Incorrect password.", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const recaptcha = await checkRecaptcha(
|
||||
context.body.recaptcha,
|
||||
"login"
|
||||
);
|
||||
if (recaptcha) return recaptcha;
|
||||
|
||||
const session = await lucia.createSession(user.id, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
context.cookie[sessionCookie.name].set(sessionCookie.attributes);
|
||||
context.cookie[sessionCookie.name].value = sessionCookie.value;
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
password: t.String(),
|
||||
username: t.String(),
|
||||
recaptcha: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Auth", "Authenication not required"],
|
||||
description:
|
||||
"Authenicates with password and username. Requires captcha, properly created.",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
145
server/src/routes/profile.ts
Normal file
145
server/src/routes/profile.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import session from "../session";
|
||||
import { client } from "../db";
|
||||
import { generateIdFromEntropySize } from "lucia";
|
||||
import { setDeleteTimeout } from "../lib";
|
||||
import { updateRooms } from "./ws";
|
||||
|
||||
export default new Elysia({
|
||||
prefix: "/api/profile",
|
||||
})
|
||||
.use(session)
|
||||
|
||||
.post(
|
||||
"/setStatus",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (context.body.status.length > 40) {
|
||||
return new Response("Status greater than 40!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (context.body.status.length < 1) {
|
||||
return new Response("Status smaller than 1!", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
await client.user.update({
|
||||
where: {
|
||||
id: context.user.id,
|
||||
},
|
||||
data: {
|
||||
status: context.body.status,
|
||||
},
|
||||
});
|
||||
updateRooms(context.user.id, "status", context.body.status);
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
status: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Profile"],
|
||||
description: "Sets the status of the user. Can be seen in the client.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/setProfilePicture",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.body.file.size > 2e7) {
|
||||
return new Response("PFP too big", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (!context.body.file.type.startsWith("image/")) {
|
||||
return new Response("PFP not image", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const response = new Response(context.body.file.stream());
|
||||
await Bun.write("static/pfps/" + context.user.id, response);
|
||||
|
||||
updateRooms(context.user.id, "pfp");
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
file: t.File(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Profile"],
|
||||
description:
|
||||
"Sets a profile picture. Max size 20MB, only images (mimetype image/).",
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/uploadImage",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.body.file.size > 1e7) {
|
||||
return new Response("Image too big", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (!context.body.file.type.startsWith("image/")) {
|
||||
return new Response("File not image", {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const uploadId = generateIdFromEntropySize(10);
|
||||
const expiresAt = new Date(Date.now() + 8.64e7);
|
||||
|
||||
await client.upload.create({
|
||||
data: {
|
||||
id: uploadId,
|
||||
expiresAt,
|
||||
type: context.body.file.type,
|
||||
uploadedBy: context.user.id,
|
||||
},
|
||||
});
|
||||
const response = new Response(context.body.file.stream());
|
||||
await Bun.write("static/images/" + uploadId, response);
|
||||
|
||||
await setDeleteTimeout({
|
||||
id: uploadId,
|
||||
expiresAt,
|
||||
type: context.body.file.type,
|
||||
});
|
||||
return {
|
||||
uploadId: uploadId,
|
||||
};
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
file: t.File(),
|
||||
}),
|
||||
response: t.MaybeEmpty(
|
||||
t.Object({
|
||||
uploadId: t.String(),
|
||||
})
|
||||
),
|
||||
detail: {
|
||||
tags: ["Profile"],
|
||||
description:
|
||||
"Uploads a file. Most commonly used for chat messages. These will only stay for 24 hours.",
|
||||
},
|
||||
}
|
||||
);
|
||||
277
server/src/routes/staff.ts
Normal file
277
server/src/routes/staff.ts
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
import Elysia, { t } from "elysia";
|
||||
import session from "../session";
|
||||
import { generateIdFromEntropySize } from "lucia";
|
||||
import { client } from "../db";
|
||||
import { disconnectID, findID } from "./ws";
|
||||
|
||||
export default new Elysia({
|
||||
prefix: "/api/staff",
|
||||
})
|
||||
.use(session)
|
||||
.post(
|
||||
"/invalidatePunishment",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (!context.user.staff) {
|
||||
return new Response("You are not staff.", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const punishment = await client.punishment.findFirst({
|
||||
where: {
|
||||
id: context.body.punishmentId,
|
||||
},
|
||||
});
|
||||
if (!punishment) {
|
||||
return new Response("No punishment found!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
await client.punishment.delete({
|
||||
where: {
|
||||
id: context.body.punishmentId,
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
punishmentId: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Staff"],
|
||||
description:
|
||||
"Invalidates a request. Invalidate currently means delete, but it might in the future mean that the punishment is invalid.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/punishments",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (!context.user.staff) {
|
||||
return new Response("You are not staff.", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
id: context.body.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return new Response("User does not exist!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const punishments = await client.punishment.findMany({
|
||||
where: {
|
||||
punishedUserId: user.id,
|
||||
},
|
||||
});
|
||||
return punishments.map((z) => {
|
||||
return {
|
||||
id: z.id,
|
||||
type: z.type,
|
||||
reason: z.reason,
|
||||
staffId: z.staffId,
|
||||
time: z.time,
|
||||
at: z.at,
|
||||
};
|
||||
});
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
userId: t.String(),
|
||||
}),
|
||||
response: t.MaybeEmpty(
|
||||
t.Array(
|
||||
t.Object({
|
||||
id: t.String(),
|
||||
type: t.String(),
|
||||
reason: t.String(),
|
||||
staffId: t.String(),
|
||||
time: t.Number(),
|
||||
at: t.Date(),
|
||||
})
|
||||
)
|
||||
),
|
||||
detail: {
|
||||
tags: ["Staff"],
|
||||
description:
|
||||
"View an user's punishments. Will show all punishments by user, and who banned them and for what reason. Can be invalidated.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.guard(
|
||||
{
|
||||
body: t.Object({
|
||||
id: t.String(),
|
||||
reason: t.String(),
|
||||
ms: t.Number(),
|
||||
isPermanent: t.MaybeEmpty(t.Boolean()),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Staff"],
|
||||
description:
|
||||
"Ban and Mute. These either ban, or mute a person. Mutes prohibit the user from chatting, and bans prohibit the user from joining.",
|
||||
},
|
||||
},
|
||||
(a) =>
|
||||
a
|
||||
.post("/ban", async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (!context.user.staff) {
|
||||
return new Response("You are not staff.", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
id: context.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return new Response("User does not exist!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (user.staff) {
|
||||
return new Response("Cannot ban staff!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
disconnectID(user.id);
|
||||
|
||||
await client.punishment.create({
|
||||
data: {
|
||||
type: "ban",
|
||||
id: generateIdFromEntropySize(10),
|
||||
reason: context.body.reason,
|
||||
punishedUserId: user.id,
|
||||
staffId: context.user.id,
|
||||
at: new Date(),
|
||||
time: context.body.isPermanent ? -1 : context.body.ms,
|
||||
},
|
||||
});
|
||||
})
|
||||
.post("/mute", async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (!context.user.staff) {
|
||||
return new Response("You are not staff.", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
id: context.body.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return new Response("User does not exist!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (user.staff) {
|
||||
return new Response("Cannot mute staff!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
await client.punishment.create({
|
||||
data: {
|
||||
type: "mute",
|
||||
id: generateIdFromEntropySize(10),
|
||||
reason: context.body.reason,
|
||||
punishedUserId: user.id,
|
||||
staffId: context.user.id,
|
||||
at: new Date(),
|
||||
time: context.body.isPermanent ? -1 : context.body.ms,
|
||||
},
|
||||
});
|
||||
})
|
||||
)
|
||||
.post(
|
||||
"/isUserOnline",
|
||||
async (context) => {
|
||||
if (!context.user) {
|
||||
return new Response("Not authenicated", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (!context.user.staff) {
|
||||
return new Response("You are not staff.", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await client.user.findFirst({
|
||||
where: {
|
||||
id: context.body.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return new Response("User does not exist!", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const participiants = findID(user.id);
|
||||
|
||||
if (participiants.length == 0) {
|
||||
return {
|
||||
isOnline: false,
|
||||
name: user.username,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isOnline: true,
|
||||
rooms: participiants.map((z) => z.room),
|
||||
name: user.username,
|
||||
};
|
||||
},
|
||||
{
|
||||
response: t.MaybeEmpty(
|
||||
t.Object({
|
||||
isOnline: t.Boolean(),
|
||||
rooms: t.MaybeEmpty(t.Array(t.String())),
|
||||
name: t.String(),
|
||||
})
|
||||
),
|
||||
body: t.Object({
|
||||
userId: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
tags: ["Staff"],
|
||||
description:
|
||||
"Returns username from ID, and checks if user is online. If is online, then also returns all rooms they're in.",
|
||||
},
|
||||
}
|
||||
);
|
||||
87
server/src/routes/ws.ts
Normal file
87
server/src/routes/ws.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
The Pulitzer Prize winner is definitely spiralin'
|
||||
I got your fucking lines tapped, I swear that I'm dialed in
|
||||
First, I was a rat, so where's the proof of the trial then?
|
||||
Where's the paperwork or the cabinet it's filed in?
|
||||
1090 Jake woulda took all the walls down
|
||||
The streets woulda had me hidin' out in a small town
|
||||
My Montreal connects stand up, now fall down
|
||||
The ones that you're getting your stories from, they all clowns
|
||||
I am a war general, sеasoned in preparation
|
||||
My jacket is covеred in medals, honor and decoration
|
||||
*/
|
||||
|
||||
// This file was developed independently from the rest of the project,
|
||||
// as it's way more complicated than.. let's say the index.ts of the frontend.
|
||||
|
||||
// sad.ovh developmeant
|
||||
|
||||
import Elysia, {
|
||||
InputSchema,
|
||||
MergeSchema,
|
||||
TSchema,
|
||||
UnwrapRoute,
|
||||
t,
|
||||
} from "elysia";
|
||||
import session, { ipGenerator } from "../session";
|
||||
import { rateLimit } from "elysia-rate-limit";
|
||||
import { ElysiaWSType, Server } from "./ws/Server";
|
||||
import { Participiant } from "./ws/Participiant";
|
||||
|
||||
const server = new Server();
|
||||
|
||||
export function updateRooms(
|
||||
userId: string,
|
||||
type: "pfp" | "username" | "status",
|
||||
data: string = ""
|
||||
) {
|
||||
server.rooms.forEach((z) => {
|
||||
for (const [_, participiant] of z.participiants) {
|
||||
if (participiant.user.id == userId) {
|
||||
participiant.broadcast({
|
||||
type: "updateUser",
|
||||
updateType: type,
|
||||
id: participiant.user.id,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
export function disconnectID(id: string) {
|
||||
for (const room of server.rooms) {
|
||||
for (const [_, part] of room.participiants) {
|
||||
if (part.user.id == id) {
|
||||
part.closeClients();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function findID(id: string) {
|
||||
const parts: Participiant[] = [];
|
||||
|
||||
for (const room of server.rooms) {
|
||||
for (const [_, part] of room.participiants) {
|
||||
if (part.user.id == id) {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
export default new Elysia()
|
||||
.use(session)
|
||||
.use(
|
||||
rateLimit({
|
||||
scoping: "local",
|
||||
duration: 1000, // 1 second
|
||||
max: 2,
|
||||
generator: ipGenerator(),
|
||||
})
|
||||
)
|
||||
.ws("/api/ws", {
|
||||
open: (a) => server.open(a as unknown as ElysiaWSType),
|
||||
message: (a, b) => server.message(a as unknown as ElysiaWSType, b),
|
||||
close: (a) => server.close(a as unknown as ElysiaWSType),
|
||||
});
|
||||
26
server/src/routes/ws/Client.ts
Normal file
26
server/src/routes/ws/Client.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Participiant } from "./Participiant";
|
||||
import type { ElysiaWSType } from "./Server";
|
||||
|
||||
export class Client {
|
||||
private ws: ElysiaWSType;
|
||||
userId: string;
|
||||
room?: string;
|
||||
id: `${string}-${string}-${string}-${string}-${string}`;
|
||||
|
||||
constructor(ws: ElysiaWSType) {
|
||||
this.ws = ws;
|
||||
this.id = crypto.randomUUID();
|
||||
this.userId = this.ws.data.user!.id;
|
||||
}
|
||||
|
||||
makeParticipiant(): Participiant {
|
||||
return new Participiant(this.ws.data.user!);
|
||||
}
|
||||
send(obj: Record<any, any>) {
|
||||
this.ws.send(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
27
server/src/routes/ws/Participiant.ts
Normal file
27
server/src/routes/ws/Participiant.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { User } from "lucia";
|
||||
import { Client } from "./Client";
|
||||
|
||||
export class Participiant {
|
||||
clients: Client[] = [];
|
||||
room?: string;
|
||||
user: User;
|
||||
|
||||
constructor(user: User) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
closeClients() {
|
||||
this.clients.forEach((z) => {
|
||||
z.close();
|
||||
});
|
||||
}
|
||||
addClient(client: Client) {
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
broadcast(obj: Record<any, any>) {
|
||||
this.clients.forEach((z) => {
|
||||
z.send(obj);
|
||||
});
|
||||
}
|
||||
}
|
||||
155
server/src/routes/ws/Room.ts
Normal file
155
server/src/routes/ws/Room.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { client } from "../../db";
|
||||
import { Client } from "./Client";
|
||||
import { Participiant } from "./Participiant";
|
||||
|
||||
import {exists} from "fs/promises";
|
||||
|
||||
export class Room {
|
||||
participiants: Map<string, Participiant> = new Map();
|
||||
name: string;
|
||||
hidden: boolean;
|
||||
messageHistory: {
|
||||
userID: string;
|
||||
content: string;
|
||||
images?: string[] | undefined;
|
||||
username: string;
|
||||
time: number;
|
||||
}[] = [];
|
||||
|
||||
timeout?: number;
|
||||
|
||||
constructor(name: string, hidden: boolean) {
|
||||
this.name = name;
|
||||
this.hidden = hidden;
|
||||
}
|
||||
|
||||
async addMessage(
|
||||
message: { message: string; images: string[] },
|
||||
part: Participiant
|
||||
) {
|
||||
if (message.message.length > 200) return;
|
||||
if (message.message.length < 1) return;
|
||||
let validImages: string[] = [];
|
||||
|
||||
for (const image of message.images) {
|
||||
if (!/^[a-z0-9]{1,16}$/gm.test(image)) continue;
|
||||
if (await exists("static/images/" + image)) {
|
||||
validImages.push(image);
|
||||
}
|
||||
}
|
||||
|
||||
const punishments = await client.punishment.findMany({
|
||||
where: {
|
||||
punishedUserId: part.user.id,
|
||||
type: "mute",
|
||||
},
|
||||
});
|
||||
|
||||
for (const punishment of punishments) {
|
||||
if (punishment.time == -1) punishment.time = Infinity;
|
||||
const diff =
|
||||
punishment.at.getTime() + punishment.time - new Date().getTime();
|
||||
|
||||
if (diff > 0) {
|
||||
// punishment is valid
|
||||
const punishedBy = await client.user.findFirst({
|
||||
where: {
|
||||
id: punishment.staffId,
|
||||
},
|
||||
});
|
||||
|
||||
part.broadcast({
|
||||
type: "notification",
|
||||
message: `You've been muted by ${punishedBy?.username}. There are ${diff}ms left in your mute. You were muted at ${punishment.at}. The reason you were muted is "${punishment.reason}"`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const msg = {
|
||||
content: message.message,
|
||||
images: validImages,
|
||||
time: Date.now(),
|
||||
userID: part.user.id,
|
||||
username: part.user.username,
|
||||
};
|
||||
|
||||
for (const part of this.participiants) {
|
||||
part[1].broadcast({
|
||||
type: "message",
|
||||
message: msg,
|
||||
});
|
||||
}
|
||||
|
||||
this.messageHistory.push(msg);
|
||||
}
|
||||
addClient(client: Client) {
|
||||
let part = this.participiants.get(client.userId);
|
||||
if (part) {
|
||||
part.room = this.name;
|
||||
part.addClient(client);
|
||||
} else {
|
||||
part = client.makeParticipiant();
|
||||
part.room = this.name;
|
||||
part.addClient(client);
|
||||
this.participiants.set(part.user.id, part);
|
||||
}
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
client.send({
|
||||
type: "notification",
|
||||
message: `You saved ${this.name}! Thank you!`,
|
||||
});
|
||||
}
|
||||
client.send({
|
||||
type: "history",
|
||||
messages: this.messageHistory,
|
||||
});
|
||||
client.send({
|
||||
type: "room",
|
||||
room: this.name,
|
||||
});
|
||||
|
||||
this.updatePeople();
|
||||
}
|
||||
|
||||
removeClient(client: Client, timeoutFunction: () => void) {
|
||||
let part = this.participiants.get(client.userId);
|
||||
if (!part) return;
|
||||
part.clients = part.clients.filter((z) => z.id !== client.id);
|
||||
if (part.clients.length == 0) {
|
||||
this.participiants.delete(client.userId);
|
||||
}
|
||||
if (this.participiants.size == 0) {
|
||||
client.send({
|
||||
type: "notification",
|
||||
message:
|
||||
this.name +
|
||||
" has 0 users left! If nobody joins in 15 seconds, the room's history will be deleted.",
|
||||
});
|
||||
|
||||
this.timeout = setTimeout(timeoutFunction, 15000) as unknown as number;
|
||||
}
|
||||
this.updatePeople();
|
||||
return part.clients.length == 0;
|
||||
}
|
||||
sendPeople(z: Client) {
|
||||
z.send({
|
||||
type: "people",
|
||||
people: [...this.participiants.values()].map((z) => {
|
||||
return { username: z.user?.username, id: z.user?.id, status: z.user?.status };
|
||||
}),
|
||||
});
|
||||
}
|
||||
updatePeople() {
|
||||
this.participiants.forEach((z) => {
|
||||
z.broadcast({
|
||||
type: "people",
|
||||
people: [...this.participiants.values()].map((z) => {
|
||||
return { username: z.user?.username, id: z.user?.id, status: z.user?.status };
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
160
server/src/routes/ws/Server.ts
Normal file
160
server/src/routes/ws/Server.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { ServerWebSocket } from "bun";
|
||||
import type { TSchema, MergeSchema, UnwrapRoute, InputSchema } from "elysia";
|
||||
import type { TypeCheck } from "elysia/dist/type-system";
|
||||
import type { ElysiaWS } from "elysia/dist/ws";
|
||||
import type { User, Session } from "lucia";
|
||||
import { client } from "../../db";
|
||||
import { Room } from "./Room";
|
||||
import { Client } from "./Client";
|
||||
|
||||
export type ElysiaWSType = ElysiaWS<
|
||||
ServerWebSocket<{
|
||||
validator?: TypeCheck<TSchema> | undefined;
|
||||
}>,
|
||||
MergeSchema<UnwrapRoute<InputSchema<never>, {}>, {}> & {
|
||||
params: Record<never, string>;
|
||||
},
|
||||
{
|
||||
decorator: {};
|
||||
store: {};
|
||||
derive: {};
|
||||
resolve: {
|
||||
client: Client | null;
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
};
|
||||
} & {
|
||||
derive: {};
|
||||
resolve: {};
|
||||
}
|
||||
>;
|
||||
export class Server {
|
||||
clients: Client[] = [];
|
||||
rooms: Room[] = [];
|
||||
|
||||
async open(ws: ElysiaWSType) {
|
||||
if (!ws.data.user) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const punishments = await client.punishment.findMany({
|
||||
where: {
|
||||
punishedUserId: ws.data.user.id,
|
||||
type: "ban",
|
||||
},
|
||||
});
|
||||
|
||||
for (const punishment of punishments) {
|
||||
if (punishment.time == -1) punishment.time = Infinity;
|
||||
const diff =
|
||||
punishment.at.getTime() + punishment.time - new Date().getTime();
|
||||
|
||||
if (diff > 0) {
|
||||
// punishment is valid
|
||||
const punishedBy = await client.user.findFirst({
|
||||
where: {
|
||||
id: punishment.staffId,
|
||||
},
|
||||
});
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "notification",
|
||||
message: `You've been banned by ${punishedBy?.username}. There are ${diff}ms left in your ban. You were banned at ${punishment.at}. The reason you were banned is "${punishment.reason}"`,
|
||||
})
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "notification",
|
||||
message: `Your ID: ${punishment.punishedUserId}, punishment ID: ${punishment.id}`,
|
||||
})
|
||||
);
|
||||
|
||||
ws.close();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ccl = new Client(ws);
|
||||
this.clients.push(ccl);
|
||||
ws.data.client = ccl;
|
||||
ccl.send({
|
||||
type: "hello",
|
||||
staff: ws.data.user.staff,
|
||||
});
|
||||
}
|
||||
|
||||
message(ws: ElysiaWSType, message: any) {
|
||||
if (!ws.data.client) return;
|
||||
const client = ws.data.client;
|
||||
|
||||
if (message.type == "room") {
|
||||
if (
|
||||
!message.room ||
|
||||
typeof message.room != "string" ||
|
||||
message.room.length == 0 ||
|
||||
message.room.length > 20
|
||||
)
|
||||
message.room = "lobby";
|
||||
|
||||
if (client.room) {
|
||||
if (client.room == message.room) return;
|
||||
const prevRoom = this.rooms.find((z) => z.name == client.room);
|
||||
if (prevRoom)
|
||||
prevRoom.removeClient(client, () => {
|
||||
this.rooms = this.rooms.filter((z) => z.name !== prevRoom.name);
|
||||
this.updateRooms();
|
||||
});
|
||||
}
|
||||
let room = this.rooms.find((z) => z.name == message.room);
|
||||
if (!room) {
|
||||
room = new Room(message.room, !!message.hidden);
|
||||
this.rooms.push(room);
|
||||
}
|
||||
|
||||
room.addClient(client);
|
||||
|
||||
client.room = message.room;
|
||||
|
||||
this.updateRooms();
|
||||
}
|
||||
if (message.type == "message") {
|
||||
const room = this.rooms.find((z) => z.name == client.room);
|
||||
if (!room) return;
|
||||
|
||||
room.addMessage(message, room.participiants.get(client.userId)!);
|
||||
}
|
||||
}
|
||||
|
||||
close(ws: ElysiaWSType) {
|
||||
if (!ws.data.client) return;
|
||||
this.clients = this.clients.filter((z) => z.id !== ws.data.client?.id);
|
||||
const room = this.rooms.find((z) => z.name == ws.data.client?.room);
|
||||
if (room) {
|
||||
if (room.removeClient(ws.data.client!, () => {
|
||||
this.rooms = this.rooms.filter((z) => z.name !== room.name);
|
||||
this.updateRooms();
|
||||
})) {
|
||||
this.updateRooms();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sendRooms(client: Client) {
|
||||
client.send({
|
||||
type: "rooms",
|
||||
rooms: this.rooms
|
||||
.filter((z) => !z.hidden)
|
||||
.map((g) => {
|
||||
return { name: g.name, count: g.participiants.size };
|
||||
}),
|
||||
});
|
||||
}
|
||||
updateRooms() {
|
||||
this.clients.forEach((z) => this.sendRooms(z));
|
||||
}
|
||||
}
|
||||
|
||||
88
server/src/session.ts
Normal file
88
server/src/session.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import Elysia from "elysia";
|
||||
import { User, Session, verifyRequestOrigin } from "lucia";
|
||||
import { lucia } from "./db";
|
||||
import type { Generator } from "elysia-rate-limit";
|
||||
|
||||
export const ipGenerator = () => {
|
||||
let gen: Generator<any>;
|
||||
|
||||
if (process.env.DETECT_IP == "fwf") {
|
||||
gen = (req, server) => {
|
||||
let fwf = req.headers.get("x-forwarded-for")!;
|
||||
if (!fwf) {
|
||||
console.log(
|
||||
"!!! x-forwarded-for missing on request, while DETECT_IP is set to fwf! falling back to server IP !!!"
|
||||
);
|
||||
fwf = server?.requestIP(req)?.address!;
|
||||
}
|
||||
|
||||
return fwf;
|
||||
};
|
||||
} else {
|
||||
gen = (req, server) => {
|
||||
return server?.requestIP(req)?.address!;
|
||||
};
|
||||
}
|
||||
return gen;
|
||||
};
|
||||
|
||||
export default new Elysia({
|
||||
name: "session",
|
||||
}).derive(
|
||||
{ as: "global" },
|
||||
async (
|
||||
context
|
||||
): Promise<{
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
}> => {
|
||||
// CSRF check
|
||||
if (context.request.method !== "GET") {
|
||||
const originHeader = context.request.headers.get("Origin");
|
||||
// NOTE: You may need to use `X-Forwarded-Host` instead
|
||||
const hostHeader = context.request.headers.get("Host");
|
||||
if (
|
||||
!originHeader ||
|
||||
!hostHeader ||
|
||||
!verifyRequestOrigin(originHeader, [hostHeader, "localhost:5173"])
|
||||
) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// use headers instead of Cookie API to prevent type coercion
|
||||
const cookieHeader = context.request.headers.get("Cookie") ?? "";
|
||||
const sessionId = lucia.readSessionCookie(cookieHeader);
|
||||
if (!sessionId) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { session, user } = await lucia.validateSession(sessionId);
|
||||
if (session && session.fresh) {
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
context.cookie[sessionCookie.name].set({
|
||||
value: sessionCookie.value,
|
||||
...sessionCookie.attributes,
|
||||
});
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
context.cookie[sessionCookie.name].set({
|
||||
value: sessionCookie.value,
|
||||
...sessionCookie.attributes,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
session,
|
||||
};
|
||||
}
|
||||
);
|
||||
103
server/tsconfig.json
Normal file
103
server/tsconfig.json
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
"noErrorTruncation": true,
|
||||
/* Language and Environment */
|
||||
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "ES2022", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue