Merge branch 'feat-ip-tracking' into 'main'

Track IP addresses for alternate account discovery

See merge request sc07/canvas!15
This commit is contained in:
Grant 2024-07-14 16:02:32 +00:00
commit 112f72b4d9
6 changed files with 127 additions and 0 deletions

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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
*

View File

@ -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");

View File

@ -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