diff --git a/packages/server/prisma/dbml/schema.dbml b/packages/server/prisma/dbml/schema.dbml index aa417a3..ef43b1e 100644 --- a/packages/server/prisma/dbml/schema.dbml +++ b/packages/server/prisma/dbml/schema.dbml @@ -21,6 +21,7 @@ Table User { FactionMember FactionMember [not null] Ban Ban AuditLog AuditLog [not null] + IPAddress IPAddress [not null] } Table Instance { @@ -32,6 +33,18 @@ Table Instance { Ban Ban } +Table IPAddress { + ip String [not null] + userSub String [not null] + lastUsedAt DateTime [not null] + createdAt DateTime [default: `now()`, not null] + user User [not null] + + indexes { + (ip, userSub) [pk] + } +} + Table PaletteColor { id Int [pk, increment] name String [not null] @@ -145,6 +158,8 @@ Enum AuditLogAction { USER_UNADMIN } +Ref: IPAddress.userSub > User.sub + Ref: Pixel.userId > User.sub Ref: FactionMember.sub > User.sub diff --git a/packages/server/prisma/migrations/20240714153412_add_ip_address_table/migration.sql b/packages/server/prisma/migrations/20240714153412_add_ip_address_table/migration.sql new file mode 100644 index 0000000..3fa061f --- /dev/null +++ b/packages/server/prisma/migrations/20240714153412_add_ip_address_table/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "IPAddress" ( + "ip" TEXT NOT NULL, + "userSub" TEXT NOT NULL, + "lastUsedAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IPAddress_pkey" PRIMARY KEY ("ip","userSub") +); + +-- AddForeignKey +ALTER TABLE "IPAddress" ADD CONSTRAINT "IPAddress_userSub_fkey" FOREIGN KEY ("userSub") REFERENCES "User"("sub") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/server/prisma/schema.prisma b/packages/server/prisma/schema.prisma index 9df5391..d65bc42 100644 --- a/packages/server/prisma/schema.prisma +++ b/packages/server/prisma/schema.prisma @@ -36,6 +36,7 @@ model User { FactionMember FactionMember[] Ban Ban? AuditLog AuditLog[] + IPAddress IPAddress[] } model Instance { @@ -47,6 +48,18 @@ model Instance { Ban Ban? } +model IPAddress { + ip String + userSub String + + lastUsedAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userSub], references: [sub]) + + @@id([ip, userSub]) +} + model PaletteColor { id Int @id @default(autoincrement()) name String diff --git a/packages/server/src/api/admin.ts b/packages/server/src/api/admin.ts index 26f4af5..5287c6e 100644 --- a/packages/server/src/api/admin.ts +++ b/packages/server/src/api/admin.ts @@ -417,6 +417,61 @@ app.put("/canvas/fill", async (req, res) => { res.json({ success: true, auditLog }); }); +/** + * Get ip address info + * + * @query address IP address + */ +app.get("/ip", async (req, res) => { + if (typeof req.query.address !== "string") { + return res.status(400).json({ success: false, error: "missing ?address=" }); + } + + const ip: string = req.query.address; + + const results = await prisma.iPAddress.findMany({ + select: { + userSub: true, + createdAt: true, + lastUsedAt: true, + }, + where: { + ip, + }, + }); + + res.json({ success: true, results }); +}); + +/** + * Get all of a user's IP addresses + * + * @param :sub User ID + */ +app.get("/user/:sub/ips", async (req, res) => { + let user: User; + + try { + user = await User.fromSub(req.params.sub); + } catch (e) { + if (e instanceof UserNotFound) { + res.status(404).json({ success: false, error: "User not found" }); + } else { + Logger.error(`/user/${req.params.sub}/ips Error ` + (e as any)?.message); + res.status(500).json({ success: false, error: "Internal error" }); + } + return; + } + + const ips = await prisma.iPAddress.findMany({ + where: { + userSub: user.sub, + }, + }); + + res.json({ success: true, ips }); +}); + /** * Create or ban a user * diff --git a/packages/server/src/lib/SocketServer.ts b/packages/server/src/lib/SocketServer.ts index c9ab344..70b9c08 100644 --- a/packages/server/src/lib/SocketServer.ts +++ b/packages/server/src/lib/SocketServer.ts @@ -164,6 +164,17 @@ export class SocketServer { ); user?.sockets.add(socket); + + let ip = socket.handshake.address; + if (process.env.NODE_ENV === "production") { + if (typeof socket.handshake.headers["x-forwarded-for"] === "string") { + ip = socket.handshake.headers["x-forwarded-for"]; + } else { + ip = socket.handshake.headers["x-forwarded-for"]?.[0] || ip; + } + } + user?.trackIP(ip); + Logger.debug("handleConnection " + user?.sockets.size); socket.emit("clearCanvasChunks"); diff --git a/packages/server/src/models/User.ts b/packages/server/src/models/User.ts index a8b34cf..4faf41e 100644 --- a/packages/server/src/models/User.ts +++ b/packages/server/src/models/User.ts @@ -317,6 +317,27 @@ export class User { } } + async trackIP(ip: string) { + await prisma.iPAddress.upsert({ + where: { + ip_userSub: { + ip, + userSub: this.sub, + }, + }, + create: { + ip, + userSub: this.sub, + lastUsedAt: new Date(), + }, + update: { + ip, + userSub: this.sub, + lastUsedAt: new Date(), + }, + }); + } + /** * Determine if this user data is stale and should be updated * @see User#update