Files
Minikura/apps/backend/src/index.ts
2025-01-02 14:48:19 +07:00

419 lines
11 KiB
TypeScript

import { dotenvLoad } from "dotenv-mono";
const dotenv = dotenvLoad();
import { Elysia, error, t } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { prisma, ServerType } from "@minikura/db";
import { ServerService } from "./services/server";
import { UserService } from "./services/user";
import argon2 from "argon2";
import { SessionService } from "./services/session";
enum ReturnError {
INVALID_USERNAME_OR_PASSWORD = "INVALID_USERNAME_OR_PASSWORD",
MISSING_TOKEN = "MISSING_TOKEN",
REVOKED_TOKEN = "REVOKED_TOKEN",
EXPIRED_TOKEN = "EXPIRED_TOKEN",
INVALID_TOKEN = "INVALID_TOKEN",
SERVER_NAME_IN_USE = "SERVER_NAME_IN_USE",
SERVER_NOT_FOUND = "SERVER_NOT_FOUND",
}
const bootstrap = async () => {
const users = await prisma.user.findMany();
if (users.length !== 0) {
return;
}
await prisma.user.create({
data: {
username: "admin",
password: await argon2.hash("admin"),
},
});
console.log("Default user created");
};
const app = new Elysia()
.use(swagger())
.ws("/ws", {
body: t.Object({
message: t.String(),
}),
// query: t.Object({
// id: t.String(),
// }),
message(ws, { message }) {
const { id } = ws.data.query;
ws.send({
id,
message,
time: Date.now(),
});
},
})
.post(
"/login",
async ({ body, cookie: { session_token } }) => {
const user = await UserService.getUserByUsername(body.username);
if (!user) {
// Fake hash to prevent timing attacks
const FAKE_HASH =
"$argon2id$v=19$m=65536,t=3,p=4$PMqZArEBmfkHPbPv7Z50KQ$miV6tIIXU6w2WWOGLbWrdan8TGMTUsS1A1hy9RrpS9Y";
await argon2.verify(FAKE_HASH, "fake");
return error("Unauthorized", {
success: false,
message: ReturnError.INVALID_USERNAME_OR_PASSWORD,
});
}
const valid = await argon2.verify(user.password, body.password);
if (!valid) {
return error("Unauthorized", {
success: false,
message: ReturnError.INVALID_USERNAME_OR_PASSWORD,
});
}
const session = await SessionService.create(user.id);
session_token.httpOnly = true;
session_token.value = session.token;
return {
success: true,
};
},
{
body: t.Object({
username: t.String({ minLength: 1 }),
password: t.String({ minLength: 1 }),
}),
}
)
.derive(async ({ headers, cookie: { session_token } }) => {
const auth = session_token.value;
const token = headers.authorization?.split(" ")[1];
if (!auth && !token)
return error("Unauthorized", {
success: false,
message: ReturnError.MISSING_TOKEN,
});
if (auth) {
const session = await SessionService.validate(auth);
if (session.status === SessionService.SESSION_STATUS.REVOKED) {
return error("Unauthorized", {
success: false,
message: ReturnError.REVOKED_TOKEN,
});
}
if (session.status === SessionService.SESSION_STATUS.EXPIRED) {
return error("Unauthorized", {
success: false,
message: ReturnError.EXPIRED_TOKEN,
});
}
if (
session.status === SessionService.SESSION_STATUS.INVALID ||
session.status !== SessionService.SESSION_STATUS.VALID ||
!session.session
) {
return error("Unauthorized", {
success: false,
message: ReturnError.INVALID_TOKEN,
});
}
return {
server: null,
session: session.session,
};
}
if (token) {
const session = await SessionService.validateApiKey(token);
if (session.status === SessionService.SESSION_STATUS.INVALID) {
return error("Unauthorized", {
success: false,
message: ReturnError.INVALID_TOKEN,
});
}
if (
session.status === SessionService.SESSION_STATUS.VALID &&
session.server
) {
return {
session: null,
server: session.server,
};
}
}
// Should never reach here
return error("Unauthorized", {
success: false,
message: ReturnError.INVALID_TOKEN,
});
})
.post("/logout", async ({ session, cookie: { session_token } }) => {
if (!session) return { success: true };
await SessionService.revoke(session.token);
session_token.remove();
return {
success: true,
};
})
.get("/servers", async ({ session }) => {
return await ServerService.getAllServers(!session);
})
.get("/servers/:id", async ({ session, params: { id } }) => {
return await ServerService.getServerById(id, !session);
})
.post(
"/servers",
async ({ body, error }) => {
// Must be a-z, A-Z, 0-9, and -_ only
if (!/^[a-zA-Z0-9-_]+$/.test(body.name)) {
return error("Bad Request", "Name must be a-z, A-Z, 0-9, and -_ only");
}
const _server = await ServerService.getServerByName(body.name);
if (_server) {
return error("Conflict", {
success: false,
message: ReturnError.SERVER_NAME_IN_USE,
});
}
const server = await ServerService.createServer({
name: body.name,
description: body.description,
address: body.address,
port: body.port,
type: body.type,
join_priority: body.join_priority,
});
return {
success: true,
data: {
server,
},
};
},
{
body: t.Object({
name: t.String({ minLength: 1 }),
description: t.Nullable(t.String({ minLength: 1 })),
address: t.String({ minLength: 1 }),
port: t.Integer({ minimum: 1, maximum: 65535 }),
type: t.Enum(ServerType),
join_priority: t.Nullable(t.Integer({ minimum: 0 })),
}),
}
)
.patch(
"/servers/:id",
async ({ session, params: { id }, body }) => {
const server = await ServerService.getServerById(id);
if (!server) {
return error("Not Found", {
success: false,
message: ReturnError.SERVER_NOT_FOUND,
});
}
if (body.name) {
const _server = await ServerService.getServerByName(body.name);
if (_server && _server.id !== id) {
return error("Conflict", {
success: false,
message: ReturnError.SERVER_NAME_IN_USE,
});
}
}
const data = {
name: body.name,
description: body.description,
address: body.address,
port: body.port,
join_priority: body.join_priority,
};
await prisma.server.update({
where: { id },
data,
});
const newServer = await ServerService.getServerById(id, !session);
return {
success: true,
data: {
server: newServer,
},
};
},
{
body: t.Object({
name: t.Optional(t.String({ minLength: 1 })),
description: t.Optional(t.Nullable(t.String({ minLength: 1 }))),
address: t.Optional(t.String({ minLength: 1 })),
port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })),
join_priority: t.Optional(t.Integer({ minimum: 0 })),
}),
}
)
.delete("/servers/:id", async ({ params: { id } }) => {
const server = await ServerService.getServerById(id);
if (!server) {
return error("Not Found", {
success: false,
message: ReturnError.SERVER_NOT_FOUND,
});
}
await prisma.server.delete({
where: { id },
});
return {
success: true,
};
})
.get("/reverse_proxy_servers", async ({ session }) => {
return await ServerService.getAllReverseProxyServers(!session);
})
.post(
"/reverse_proxy_servers",
async ({ body, error }) => {
// Must be a-z, A-Z, 0-9, and -_ only
if (!/^[a-zA-Z0-9-_]+$/.test(body.name)) {
return error("Bad Request", "Name must be a-z, A-Z, 0-9, and -_ only");
}
const _server = await ServerService.getReverseProxyServerByName(
body.name
);
if (_server) {
return error("Conflict", {
success: false,
message: ReturnError.SERVER_NAME_IN_USE,
});
}
const server = await ServerService.createReverseProxyServer({
name: body.name,
address: body.address,
port: body.port,
description: body.description,
});
return {
success: true,
data: {
server,
},
};
},
{
body: t.Object({
name: t.String({ minLength: 1 }),
description: t.Nullable(t.String({ minLength: 1 })),
address: t.String({ minLength: 1 }),
port: t.Integer({ minimum: 1, maximum: 65535 }),
}),
}
)
.patch(
"/reverse_proxy_servers/:id",
async ({ session, params: { id }, body }) => {
const server = await prisma.reverseProxyServer.findUnique({
where: { id },
});
if (!server) {
return error("Not Found", {
success: false,
message: ReturnError.SERVER_NOT_FOUND,
});
}
if (body.name) {
const _server = await ServerService.getServerByName(body.name);
if (_server && _server.id !== id) {
return error("Conflict", {
success: false,
message: ReturnError.SERVER_NAME_IN_USE,
});
}
}
const data = {
name: body.name,
description: body.description,
address: body.address,
port: body.port,
};
await prisma.reverseProxyServer.update({
where: { id },
data,
});
const newServer = await ServerService.getReverseProxyServerById(
id,
!session
);
return {
success: true,
data: {
server: newServer,
},
};
},
{
body: t.Object({
name: t.Optional(t.String({ minLength: 1 })),
description: t.Optional(t.Nullable(t.String({ minLength: 1 }))),
address: t.Optional(t.String({ minLength: 1 })),
port: t.Optional(t.Integer({ minimum: 1, maximum: 65535 })),
}),
}
)
.delete("/reverse_proxy_servers/:id", async ({ params: { id } }) => {
const server = await prisma.reverseProxyServer.findUnique({
where: { id },
});
if (!server) {
return error("Not Found", {
success: false,
message: ReturnError.SERVER_NOT_FOUND,
});
}
await prisma.reverseProxyServer.delete({
where: { id },
});
return {
success: true,
};
})
.listen(3000, async () => {
console.log("Server is running on port 3000");
bootstrap();
});
export type App = typeof app;