rename core-server
This commit is contained in:
19
apps/core-server/.d.ts
vendored
Normal file
19
apps/core-server/.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
uid: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
declare module "cookie-parser";
|
||||
|
||||
import type { User } from "@repo/db";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: User | null;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
apps/core-server/.dockerignore
Normal file
6
apps/core-server/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
nodemon.json
|
||||
.env
|
||||
.env.example
|
||||
7
apps/core-server/.env.example
Normal file
7
apps/core-server/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
DISCORD_SERVER_PORT=3005
|
||||
DISCORD_GUILD_ID=1077269395019141140
|
||||
DISCORD_OAUTH_CLIENT_ID=930384053344034846
|
||||
DISCORD_OAUTH_SECRET=96aSvmIePqFTbGc54mad0QsZfDnYwhl1
|
||||
DISCORD_BOT_TOKEN=OTMwMzg0MDUzMzQ0MDM0ODQ2.G7zIy-._hE3dTbtUv6sd7nIP2PUn3d8s-2MFk0x3nYMg8
|
||||
DISCORD_REDIRECT_URL=https://hub.premiumag.de/api/discord-redirect
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.com/oauth2/authorize?client_id=930384053344034846&response_type=code&redirect_uri=https%3A%2F%2Fhub.premiumag.de%2Fapi%2Fdiscord-redirect&scope=identify+guilds+email
|
||||
50
apps/core-server/Dockerfile
Normal file
50
apps/core-server/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
ENV PNPM_HOME="/usr/local/pnpm"
|
||||
ENV PATH="${PNPM_HOME}:${PATH}"
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
RUN pnpm add -g turbo@^2.5
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN ls -lh
|
||||
|
||||
RUN turbo prune core-server --docker
|
||||
|
||||
FROM base AS installer
|
||||
RUN apk update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY --from=builder /usr/app/out/json/ .
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project
|
||||
COPY --from=builder /usr/app/out/full/ .
|
||||
|
||||
RUN turbo run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /usr/app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer --chown=nextjs:nodejs /usr/app/ ./
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 3003
|
||||
|
||||
CMD ["pnpm", "--dir", "apps/core-server", "run", "start"]
|
||||
16
apps/core-server/index.ts
Normal file
16
apps/core-server/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import router from "routes/router";
|
||||
import cors from "cors";
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(router);
|
||||
|
||||
server.listen(process.env.DISCORD_SERVER_PORT, () => {
|
||||
console.log(`Server running on port ${process.env.DISCORD_SERVER_PORT}`);
|
||||
});
|
||||
19
apps/core-server/modules/discord.ts
Normal file
19
apps/core-server/modules/discord.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Client, GatewayIntentBits } from "discord.js";
|
||||
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
|
||||
});
|
||||
|
||||
const token = process.env.DISCORD_BOT_TOKEN;
|
||||
|
||||
if (!token) {
|
||||
throw new Error("DISCORD_BOT_TOKEN environment variable is not set.");
|
||||
}
|
||||
|
||||
client.login(token);
|
||||
|
||||
client.on("ready", () => {
|
||||
console.log(`Logged in as ${client.user?.tag}`);
|
||||
});
|
||||
|
||||
export default client;
|
||||
36
apps/core-server/modules/prometheus.ts
Normal file
36
apps/core-server/modules/prometheus.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { prisma } from "@repo/db";
|
||||
import promClient from "prom-client";
|
||||
|
||||
export const promRegister = new promClient.Registry();
|
||||
promClient.collectDefaultMetrics({ register: promRegister });
|
||||
|
||||
export const connectedPilots = new promClient.Gauge({
|
||||
name: "connected_pilots",
|
||||
help: "Counts connected pilots",
|
||||
registers: [promRegister],
|
||||
collect: async () => {
|
||||
const count = await prisma.connectedAircraft.count({
|
||||
where: {
|
||||
logoutTime: null,
|
||||
},
|
||||
});
|
||||
connectedPilots.set(count);
|
||||
},
|
||||
});
|
||||
|
||||
export const connectedDispatcher = new promClient.Gauge({
|
||||
name: "connected_dispatcher",
|
||||
help: "Counts connected dispatchers",
|
||||
registers: [promRegister],
|
||||
collect: async () => {
|
||||
const count = await prisma.connectedDispatcher.count({
|
||||
where: {
|
||||
logoutTime: null,
|
||||
},
|
||||
});
|
||||
connectedDispatcher.set(count);
|
||||
},
|
||||
});
|
||||
|
||||
promRegister.registerMetric(connectedPilots);
|
||||
promRegister.registerMetric(connectedDispatcher);
|
||||
5
apps/core-server/nodemon.json
Normal file
5
apps/core-server/nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["."],
|
||||
"ext": "ts",
|
||||
"exec": "tsx index.ts"
|
||||
}
|
||||
35
apps/core-server/package.json
Normal file
35
apps/core-server/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "core-server",
|
||||
"exports": {
|
||||
"helpers": "./helper"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "nodemon --signal SIGINT",
|
||||
"start": "tsx index.ts --transpile-only",
|
||||
"build": "tsc"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"devDependencies": {
|
||||
"@repo/db": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"concurrently": "^9.1.2",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.1.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"prom-client": "^15.1.3",
|
||||
"react": "^19.1.0",
|
||||
"tsx": "^4.19.4"
|
||||
}
|
||||
}
|
||||
56
apps/core-server/routes/helper.ts
Normal file
56
apps/core-server/routes/helper.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { DISCORD_ROLES, Event, getPublicUser, Participant, prisma } from "@repo/db";
|
||||
import { Router } from "express";
|
||||
import { changeMemberRoles, getMember } from "routes/member";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||
if (!participant) return false;
|
||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
router.post("/set-standard-name", async (req, res) => {
|
||||
const { memberId, userId } = req.body;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
console.log(`Setting standard name for user ${userId} (${user?.publicId}) to member ${memberId}`);
|
||||
if (!user) {
|
||||
res.status(404).json({ error: "User not found" });
|
||||
return;
|
||||
}
|
||||
const participant = await prisma.participant.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
include: {
|
||||
Event: true,
|
||||
},
|
||||
});
|
||||
|
||||
participant.forEach(async (p) => {
|
||||
if (!p.Event.discordRoleId) return;
|
||||
if (eventCompleted(p.Event, p)) {
|
||||
await changeMemberRoles(memberId, [p.Event.discordRoleId], "remove");
|
||||
} else {
|
||||
await changeMemberRoles(memberId, [p.Event.discordRoleId], "add");
|
||||
}
|
||||
});
|
||||
|
||||
const publicUser = getPublicUser(user);
|
||||
const member = await getMember(memberId);
|
||||
|
||||
await member.setNickname(`${publicUser.fullName} - ${user.publicId}`);
|
||||
const isPilot = user.permissions.includes("PILOT");
|
||||
const isDispatcher = user.permissions.includes("DISPO");
|
||||
|
||||
await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove");
|
||||
await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove");
|
||||
});
|
||||
|
||||
export default router;
|
||||
78
apps/core-server/routes/member.ts
Normal file
78
apps/core-server/routes/member.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import client from "modules/discord";
|
||||
|
||||
const GUILD_ID = process.env.DISCORD_GUILD_ID;
|
||||
if (!GUILD_ID) {
|
||||
throw new Error("DISCORD_GUILD_ID environment variable is not set.");
|
||||
}
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
export const getMember = async (memberId: string) => {
|
||||
const guild = client.guilds.cache.get(GUILD_ID);
|
||||
if (!guild) throw new Error("Guild not found");
|
||||
try {
|
||||
return guild.members.cache.get(memberId) ?? (await guild.members.fetch(memberId));
|
||||
} catch (error) {
|
||||
console.error("Error fetching member:", error);
|
||||
throw new Error("Member not found");
|
||||
}
|
||||
};
|
||||
|
||||
router.post("/rename", async (req: Request, res: Response) => {
|
||||
const { newName, memberId } = req.body;
|
||||
if (typeof newName !== "string" || !memberId) {
|
||||
res.status(400).json({ error: "Invalid or missing newName or memberId" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const member = await getMember(memberId);
|
||||
await member.setNickname(newName);
|
||||
console.log(`Member ${member.id} renamed to ${newName}`);
|
||||
res.status(200).json({ message: "Member renamed successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error renaming member:", error);
|
||||
res.status(500).json({ error: "Failed to rename member" });
|
||||
}
|
||||
});
|
||||
|
||||
export const changeMemberRoles = async (
|
||||
memberId: string,
|
||||
roleIds: string[],
|
||||
action: "add" | "remove",
|
||||
) => {
|
||||
const member = await getMember(memberId);
|
||||
|
||||
const currentRoleIds = member.roles.cache.map((role) => role.id);
|
||||
const filteredRoleIds =
|
||||
action === "add"
|
||||
? roleIds.filter((id: string) => !currentRoleIds.includes(id))
|
||||
: roleIds.filter((id: string) => currentRoleIds.includes(id));
|
||||
|
||||
if (filteredRoleIds.length === 0) {
|
||||
return { message: `No roles to ${action}` };
|
||||
}
|
||||
|
||||
await member.roles[action](filteredRoleIds);
|
||||
return { message: `Roles ${action}ed successfully` };
|
||||
};
|
||||
|
||||
const handleRoleChange = (action: "add" | "remove") => async (req: Request, res: Response) => {
|
||||
const { roleIds, memberId } = req.body;
|
||||
if (!Array.isArray(roleIds) || !memberId) {
|
||||
res.status(400).json({ error: "Invalid or missing roleIds or memberId" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await changeMemberRoles(memberId, roleIds, action);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error(`Error ${action}ing roles:`, error);
|
||||
res.status(500).json({ error: `Failed to ${action} roles` });
|
||||
}
|
||||
};
|
||||
|
||||
router.post("/add-role", handleRoleChange("add"));
|
||||
router.post("/remove-role", handleRoleChange("remove"));
|
||||
|
||||
export default router;
|
||||
9
apps/core-server/routes/metrics.ts
Normal file
9
apps/core-server/routes/metrics.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Router } from "express";
|
||||
import { promRegister } from "modules/prometheus";
|
||||
|
||||
export const metricsRouter: Router = Router();
|
||||
|
||||
metricsRouter.get("/", async (req, res) => {
|
||||
res.setHeader("Content-Type", promRegister.contentType);
|
||||
res.end(await promRegister.metrics());
|
||||
});
|
||||
72
apps/core-server/routes/report.ts
Normal file
72
apps/core-server/routes/report.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { prisma } from "@repo/db";
|
||||
import { Embed, EmbedBuilder } from "discord.js";
|
||||
import { Router } from "express";
|
||||
import client from "modules/discord";
|
||||
|
||||
if (!process.env.DISCORD_REPORT_CHANNEL)
|
||||
throw new Error("DISCORD_REPORT_CHANNEL environment variable is not set.");
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.post("/admin-embed", async (req, res) => {
|
||||
try {
|
||||
const { reportId } = req.body;
|
||||
if (!reportId) {
|
||||
res.status(400).json({ error: "reportId is required" });
|
||||
return;
|
||||
}
|
||||
const report = await prisma.report.findUnique({
|
||||
where: {
|
||||
id: Number(reportId),
|
||||
},
|
||||
include: {
|
||||
Reported: true,
|
||||
Sender: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!report) {
|
||||
res.status(404).json({ error: "Report not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`Report #${report.id}`)
|
||||
.setURL(`${process.env.NEXT_PUBLIC_HUB_URL}/admin/report/${report.id}`)
|
||||
.setDescription(report.text)
|
||||
.addFields(
|
||||
{
|
||||
name: "gemeldeter Nutzer",
|
||||
value: `${report.Reported?.firstname} ${report.Reported?.lastname} (${report.Reported?.publicId})`,
|
||||
inline: true,
|
||||
},
|
||||
{ name: "angemeldet als", value: report.reportedUserRole, inline: true },
|
||||
{
|
||||
name: "gemeldet von",
|
||||
value: `${report.Sender?.firstname} ${report.Sender?.lastname} (${report.Sender?.publicId})`,
|
||||
},
|
||||
)
|
||||
.setFooter({
|
||||
text: "Bitte reagiere mit 🫡, wenn du den Report bearbeitet hast, oder mit ✅, wenn er abgeschlossen ist.",
|
||||
})
|
||||
.setTimestamp(new Date(report.timestamp))
|
||||
.setColor("DarkRed");
|
||||
|
||||
const reportsChannel = await client.channels.fetch(process.env.DISCORD_REPORT_CHANNEL!);
|
||||
if (!reportsChannel || !reportsChannel.isSendable()) {
|
||||
res.status(500).json({ error: "Reports channel not found or is not a text channel" });
|
||||
return;
|
||||
}
|
||||
const message = await reportsChannel.send({ embeds: [embed] });
|
||||
message.react("🫡").catch(console.error);
|
||||
message.react("✅").catch(console.error);
|
||||
res.json({
|
||||
message: "Report embed sent to Discord channel",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error sending report embed:", error);
|
||||
res.status(500).json({ error: "Failed to send report embed" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
14
apps/core-server/routes/router.ts
Normal file
14
apps/core-server/routes/router.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import memberRouter from "./member";
|
||||
import helperRouter from "./helper";
|
||||
import reportRouter from "./report";
|
||||
import { metricsRouter } from "routes/metrics";
|
||||
|
||||
const router: Router = Router();
|
||||
|
||||
router.use("/member", memberRouter);
|
||||
router.use("/helper", helperRouter);
|
||||
router.use("/report", reportRouter);
|
||||
router.use("/metrics", metricsRouter);
|
||||
|
||||
export default router;
|
||||
11
apps/core-server/tsconfig.json
Normal file
11
apps/core-server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"allowImportingTsExtensions": false,
|
||||
"baseUrl": ".",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["**/*.ts", "./index.ts", "**/*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user