Audit Log
This commit is contained in:
parent
5f57679bb3
commit
caf19b247d
|
@ -9,6 +9,7 @@ import {
|
|||
faCog,
|
||||
faHashtag,
|
||||
faHome,
|
||||
faList,
|
||||
faServer,
|
||||
faShieldHalved,
|
||||
faSquare,
|
||||
|
@ -54,6 +55,12 @@ export const SidebarWrapper = () => {
|
|||
isActive={pathname === "/"}
|
||||
href="/"
|
||||
/>
|
||||
<SidebarItem
|
||||
title="Audit Log"
|
||||
icon={<FontAwesomeIcon icon={faList} />}
|
||||
isActive={pathname === "/audit"}
|
||||
href="/audit"
|
||||
/>
|
||||
<CollapseItems
|
||||
icon={<FontAwesomeIcon icon={faChartBar} />}
|
||||
title="Stats"
|
||||
|
|
|
@ -9,6 +9,7 @@ import { HomePage } from "./pages/Home/page.tsx";
|
|||
import { AccountsPage } from "./pages/Accounts/Accounts/page.tsx";
|
||||
import { ServiceSettingsPage } from "./pages/Service/settings.tsx";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { AuditLog } from "./pages/AuditLog/auditlog.tsx";
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
|
@ -28,6 +29,10 @@ const router = createBrowserRouter(
|
|||
path: "/service/settings",
|
||||
element: <ServiceSettingsPage />,
|
||||
},
|
||||
{
|
||||
path: "/audit",
|
||||
element: <AuditLog />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { api, handleError } from "../../lib/utils";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
type AuditLogAction = "BAN_CREATE" | "BAN_UPDATE" | "BAN_DELETE";
|
||||
|
||||
type AuditLog = {
|
||||
id: number;
|
||||
userId: string;
|
||||
action: AuditLogAction;
|
||||
reason?: string;
|
||||
comment?: string;
|
||||
|
||||
banId?: number;
|
||||
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export const AuditLog = () => {
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api<{ auditLogs: AuditLog[] }>("/api/admin/audit", "GET").then(
|
||||
({ status, data }) => {
|
||||
if (status === 200) {
|
||||
if (data.success) {
|
||||
setAuditLogs(data.auditLogs);
|
||||
} else {
|
||||
handleError(status, data);
|
||||
}
|
||||
} else {
|
||||
handleError(status, data);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="text-l font-semibold">Audit Log</h4>
|
||||
<div className="relative">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableColumn>ID</TableColumn>
|
||||
<TableColumn>User ID</TableColumn>
|
||||
<TableColumn>Action</TableColumn>
|
||||
<TableColumn>Reason</TableColumn>
|
||||
<TableColumn>Comment</TableColumn>
|
||||
<TableColumn>Created At / Updated At</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{auditLogs.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell>{log.id}</TableCell>
|
||||
<TableCell>{log.userId}</TableCell>
|
||||
<TableCell>{log.action}</TableCell>
|
||||
<TableCell>{log.reason}</TableCell>
|
||||
<TableCell>{log.comment}</TableCell>
|
||||
<TableCell>
|
||||
{log.createdAt} / {log.updatedAt}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -20,6 +20,7 @@ Table User {
|
|||
pixels Pixel [not null]
|
||||
FactionMember FactionMember [not null]
|
||||
Ban Ban
|
||||
AuditLog AuditLog [not null]
|
||||
}
|
||||
|
||||
Table Instance {
|
||||
|
@ -109,8 +110,30 @@ Table Ban {
|
|||
privateNote String
|
||||
publicNote String
|
||||
expiresAt DateTime [not null]
|
||||
createdAt DateTime [default: `now()`, not null]
|
||||
updatedAt DateTime
|
||||
user User
|
||||
instance Instance
|
||||
AuditLog AuditLog [not null]
|
||||
}
|
||||
|
||||
Table AuditLog {
|
||||
id Int [pk, increment]
|
||||
userId String
|
||||
action AuditLogAction [not null]
|
||||
reason String
|
||||
comment String
|
||||
banId Int
|
||||
createdAt DateTime [default: `now()`, not null]
|
||||
updatedAt DateTime
|
||||
user User
|
||||
ban Ban
|
||||
}
|
||||
|
||||
Enum AuditLogAction {
|
||||
BAN_CREATE
|
||||
BAN_UPDATE
|
||||
BAN_DELETE
|
||||
}
|
||||
|
||||
Ref: Pixel.userId > User.sub
|
||||
|
@ -130,3 +153,7 @@ Ref: FactionSetting.factionId > Faction.id
|
|||
Ref: Ban.userId - User.sub
|
||||
|
||||
Ref: Ban.instanceId - Instance.id
|
||||
|
||||
Ref: AuditLog.userId > User.sub
|
||||
|
||||
Ref: AuditLog.banId > Ban.id
|
|
@ -0,0 +1,22 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AuditLogAction" AS ENUM ('BAN_CREATE', 'BAN_UPDATE', 'BAN_DELETE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuditLog" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" TEXT,
|
||||
"action" "AuditLogAction" NOT NULL,
|
||||
"reason" TEXT,
|
||||
"comment" TEXT,
|
||||
"banId" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("sub") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_banId_fkey" FOREIGN KEY ("banId") REFERENCES "Ban"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -0,0 +1,4 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Ban" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "deletedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "updatedAt" TIMESTAMP(3);
|
|
@ -35,6 +35,7 @@ model User {
|
|||
pixels Pixel[]
|
||||
FactionMember FactionMember[]
|
||||
Ban Ban?
|
||||
AuditLog AuditLog[]
|
||||
}
|
||||
|
||||
model Instance {
|
||||
|
@ -139,8 +140,32 @@ model Ban {
|
|||
publicNote String?
|
||||
expiresAt DateTime
|
||||
|
||||
// TODO: link audit log
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime?
|
||||
|
||||
user User? @relation(fields: [userId], references: [sub])
|
||||
instance Instance? @relation(fields: [instanceId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [sub])
|
||||
instance Instance? @relation(fields: [instanceId], references: [id])
|
||||
AuditLog AuditLog[]
|
||||
}
|
||||
|
||||
enum AuditLogAction {
|
||||
BAN_CREATE
|
||||
BAN_UPDATE
|
||||
BAN_DELETE
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id Int @id @default(autoincrement())
|
||||
userId String?
|
||||
action AuditLogAction
|
||||
reason String?
|
||||
comment String? // service comment
|
||||
|
||||
banId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime?
|
||||
|
||||
user User? @relation(fields: [userId], references: [sub])
|
||||
ban Ban? @relation(fields: [banId], references: [id])
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Router } from "express";
|
||||
import { User, UserNotFound } from "../models/User";
|
||||
import { User, UserNotBanned, UserNotFound } from "../models/User";
|
||||
import Canvas from "../lib/Canvas";
|
||||
import { getLogger } from "../lib/Logger";
|
||||
import { RateLimiter } from "../lib/RateLimiter";
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
InstanceNotBanned,
|
||||
InstanceNotFound,
|
||||
} from "../models/Instance";
|
||||
import { AuditLog } from "../models/AuditLog";
|
||||
|
||||
const app = Router();
|
||||
const Logger = getLogger("HTTP/ADMIN");
|
||||
|
@ -206,6 +207,15 @@ app.put("/canvas/fill", async (req, res) => {
|
|||
res.json({ success: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Create or ban a user
|
||||
*
|
||||
* @header X-Audit
|
||||
* @param :sub User sub claim
|
||||
* @body expiresAt string! ISO date time string
|
||||
* @body publicNote string?
|
||||
* @body privateNote string?
|
||||
*/
|
||||
app.put("/user/:sub/ban", async (req, res) => {
|
||||
let user: User;
|
||||
let expires: Date;
|
||||
|
@ -270,23 +280,8 @@ app.put("/user/:sub/ban", async (req, res) => {
|
|||
privateNote = req.body.privateNote;
|
||||
}
|
||||
|
||||
const existingBan = user.ban;
|
||||
|
||||
const ban = await prisma.ban.upsert({
|
||||
where: { userId: user.sub },
|
||||
create: {
|
||||
userId: user.sub,
|
||||
expiresAt: expires,
|
||||
publicNote,
|
||||
privateNote,
|
||||
},
|
||||
update: {
|
||||
expiresAt: expires,
|
||||
publicNote,
|
||||
privateNote,
|
||||
},
|
||||
});
|
||||
await user.update(true);
|
||||
const existingBan = user.getBan();
|
||||
const ban = await user.ban(expires, publicNote, privateNote);
|
||||
|
||||
let shouldNotifyUser = false;
|
||||
|
||||
|
@ -312,11 +307,27 @@ app.put("/user/:sub/ban", async (req, res) => {
|
|||
|
||||
user.updateStanding();
|
||||
|
||||
// todo: audit log
|
||||
const adminUser = (await User.fromAuthSession(req.session.user!))!;
|
||||
const audit = await AuditLog.Factory(adminUser.sub)
|
||||
.doing(existingBan ? "BAN_UPDATE" : "BAN_CREATE")
|
||||
.reason(req.header("X-Audit") || null)
|
||||
.withComment(
|
||||
existingBan
|
||||
? `Updated ban on ${user.sub}`
|
||||
: `Created a ban for ${user.sub}`
|
||||
)
|
||||
.withBan(ban)
|
||||
.create();
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({ success: true, audit });
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a user ban
|
||||
*
|
||||
* @header X-Audit
|
||||
* @param :sub User sub
|
||||
*/
|
||||
app.delete("/user/:sub/ban", async (req, res) => {
|
||||
// delete ban ("unban")
|
||||
|
||||
|
@ -334,18 +345,20 @@ app.delete("/user/:sub/ban", async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!user.ban?.id) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "User is not banned",
|
||||
});
|
||||
try {
|
||||
await user.unban();
|
||||
} catch (e) {
|
||||
if (e instanceof UserNotBanned) {
|
||||
res.status(404).json({ success: false, error: "User is not banned" });
|
||||
} else {
|
||||
Logger.error(
|
||||
`/instance/${req.params.sub}/ban Error ` + (e as any)?.message
|
||||
);
|
||||
res.status(500).json({ success: false, error: "Internal error" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.ban.delete({
|
||||
where: { id: user.ban.id },
|
||||
});
|
||||
|
||||
user.notify({
|
||||
is: "modal",
|
||||
action: "moderation",
|
||||
|
@ -357,9 +370,14 @@ app.delete("/user/:sub/ban", async (req, res) => {
|
|||
await user.update(true);
|
||||
user.updateStanding();
|
||||
|
||||
// todo: audit log
|
||||
const adminUser = (await User.fromAuthSession(req.session.user!))!;
|
||||
const audit = await AuditLog.Factory(adminUser.sub)
|
||||
.doing("BAN_DELETE")
|
||||
.reason(req.header("X-Audit") || null)
|
||||
.withComment(`Deleted ban for ${user.sub}`)
|
||||
.create();
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({ success: true, audit });
|
||||
});
|
||||
|
||||
app.get("/instance/:domain/ban", async (req, res) => {
|
||||
|
@ -392,6 +410,15 @@ app.get("/instance/:domain/ban", async (req, res) => {
|
|||
res.json({ success: true, ban });
|
||||
});
|
||||
|
||||
/**
|
||||
* Create or update a ban for an instance (and subdomains)
|
||||
*
|
||||
* @header X-Audit
|
||||
* @param :domain Domain for the instance
|
||||
* @body expiresAt string! ISO date time string
|
||||
* @body publicNote string?
|
||||
* @body privateNote string?
|
||||
*/
|
||||
app.put("/instance/:domain/ban", async (req, res) => {
|
||||
// ban domain & subdomains
|
||||
|
||||
|
@ -460,15 +487,34 @@ app.put("/instance/:domain/ban", async (req, res) => {
|
|||
privateNote = req.body.privateNote;
|
||||
}
|
||||
|
||||
await instance.ban(expires, publicNote, privateNote);
|
||||
const hasExistingBan = await instance.getBan();
|
||||
|
||||
// todo: audit log
|
||||
const user = (await User.fromAuthSession(req.session.user!))!;
|
||||
const ban = await instance.ban(expires, publicNote, privateNote);
|
||||
const audit = await AuditLog.Factory(user.sub)
|
||||
.doing(hasExistingBan ? "BAN_UPDATE" : "BAN_CREATE")
|
||||
.reason(req.header("X-Audit") || null)
|
||||
.withComment(
|
||||
hasExistingBan
|
||||
? `Updated ban for ${instance.hostname}`
|
||||
: `Created a ban for ${instance.hostname}`
|
||||
)
|
||||
.withBan(ban)
|
||||
.create();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
ban,
|
||||
audit,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete an instance ban
|
||||
*
|
||||
* @header X-Audit
|
||||
* @param :domain The instance domain
|
||||
*/
|
||||
app.delete("/instance/:domain/ban", async (req, res) => {
|
||||
// unban domain & subdomains
|
||||
|
||||
|
@ -488,8 +534,9 @@ app.delete("/instance/:domain/ban", async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
let ban;
|
||||
try {
|
||||
await instance.unban();
|
||||
ban = await instance.unban();
|
||||
} catch (e) {
|
||||
if (e instanceof InstanceNotBanned) {
|
||||
res.status(404).json({ success: false, error: "instance not banned" });
|
||||
|
@ -502,9 +549,104 @@ app.delete("/instance/:domain/ban", async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// todo: audit log
|
||||
const user = (await User.fromAuthSession(req.session.user!))!;
|
||||
const audit = await AuditLog.Factory(user.sub)
|
||||
.doing("BAN_DELETE")
|
||||
.reason(req.header("X-Audit") || null)
|
||||
.withComment(`Deleted ban for ${instance.hostname}`)
|
||||
.create();
|
||||
|
||||
res.json({ success: true });
|
||||
res.json({ success: true, audit });
|
||||
});
|
||||
|
||||
/**
|
||||
* Get all audit logs
|
||||
*
|
||||
* TODO: pagination
|
||||
*/
|
||||
app.get("/audit", async (req, res) => {
|
||||
const auditLogs = await prisma.auditLog.findMany({
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true, auditLogs });
|
||||
});
|
||||
|
||||
/**
|
||||
* Get audit log entry by ID
|
||||
*
|
||||
* @param :id Audit log ID
|
||||
*/
|
||||
app.get("/audit/:id", async (req, res) => {
|
||||
let id = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "id is not a number" });
|
||||
}
|
||||
|
||||
const auditLog = await prisma.auditLog.findFirst({ where: { id } });
|
||||
|
||||
if (!auditLog) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, error: "Audit log not found" });
|
||||
}
|
||||
|
||||
res.json({ success: true, auditLog });
|
||||
});
|
||||
|
||||
/**
|
||||
* Update audit log reason
|
||||
*
|
||||
* @param :id Audit log id
|
||||
* @body reason string|null
|
||||
*/
|
||||
app.put("/audit/:id/reason", async (req, res) => {
|
||||
let id = parseInt(req.params.id);
|
||||
let reason: string;
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "id is not a number" });
|
||||
}
|
||||
|
||||
if (typeof req.body.reason !== "string" && req.body.reason !== null) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, error: "reason is not a string or null" });
|
||||
}
|
||||
|
||||
reason = req.body.reason;
|
||||
|
||||
const auditLog = await prisma.auditLog.findFirst({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!auditLog) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, error: "audit log is not found" });
|
||||
}
|
||||
|
||||
const newAudit = await prisma.auditLog.update({
|
||||
where: { id },
|
||||
data: {
|
||||
reason,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
auditLog: newAudit,
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -173,22 +173,23 @@ export class SocketServer {
|
|||
if (socket.request.session.user) {
|
||||
// inform the client of their session if it exists
|
||||
socket.emit("user", socket.request.session.user);
|
||||
|
||||
socket.emit(
|
||||
"standing",
|
||||
user?.ban
|
||||
? {
|
||||
banned: true,
|
||||
until: user.ban.expires.toISOString(),
|
||||
reason: user.ban.publicNote || undefined,
|
||||
}
|
||||
: { banned: false }
|
||||
);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
socket.emit("availablePixels", user.pixelStack);
|
||||
socket.emit("pixelLastPlaced", user.lastPixelTime.getTime());
|
||||
|
||||
const ban = user.getBan();
|
||||
socket.emit(
|
||||
"standing",
|
||||
ban
|
||||
? {
|
||||
banned: true,
|
||||
until: ban.expires.toISOString(),
|
||||
reason: ban.publicNote || undefined,
|
||||
}
|
||||
: { banned: false }
|
||||
);
|
||||
}
|
||||
|
||||
socket.emit("config", getClientConfig());
|
||||
|
@ -261,7 +262,7 @@ export class SocketServer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (user.ban && user.ban.expires > new Date()) {
|
||||
if ((user.getBan()?.expires || 0) > new Date()) {
|
||||
ack({ success: false, error: "banned" });
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -14,3 +14,8 @@ export const createEnum = <T extends string>(values: T[]): { [k in T]: k } => {
|
|||
|
||||
return ret;
|
||||
};
|
||||
|
||||
export type ConditionalPromise<
|
||||
T,
|
||||
UsePromise extends boolean = false,
|
||||
> = UsePromise extends true ? Promise<T> : UsePromise extends false ? T : never;
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { AuditLog as AuditLogDB, Ban, User } from "@prisma/client";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
export class AuditLog {
|
||||
static Factory(user: User | string | null) {
|
||||
return new AuditLogFactory(user);
|
||||
}
|
||||
|
||||
static async createEmpty(
|
||||
user: User,
|
||||
action: AuditLogDB["action"],
|
||||
reason?: string
|
||||
) {
|
||||
return await prisma.auditLog.create({
|
||||
data: {
|
||||
userId: user.sub,
|
||||
action,
|
||||
reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AuditLogFactory {
|
||||
/**
|
||||
* User who committed the action
|
||||
*
|
||||
* If null; the system did the action
|
||||
*
|
||||
* @nullable
|
||||
*/
|
||||
private _userId: string | null;
|
||||
/**
|
||||
* @required
|
||||
*/
|
||||
private _action?: AuditLogDB["action"];
|
||||
private _reason: string | null = null;
|
||||
private _comment: string | null = null;
|
||||
|
||||
/**
|
||||
* Associated ban, if applicable
|
||||
*/
|
||||
private _ban?: Ban;
|
||||
|
||||
constructor(user: User | string | null) {
|
||||
if (typeof user === "string" || user === null) {
|
||||
this._userId = user;
|
||||
} else {
|
||||
this._userId = user.sub;
|
||||
}
|
||||
}
|
||||
|
||||
doing(action: AuditLogDB["action"]) {
|
||||
this._action = action;
|
||||
return this;
|
||||
}
|
||||
|
||||
reason(reason: string | null) {
|
||||
this._reason = reason;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add comment from the service
|
||||
* @param comment
|
||||
* @returns
|
||||
*/
|
||||
withComment(comment: string | null) {
|
||||
this._comment = comment;
|
||||
return this;
|
||||
}
|
||||
|
||||
withBan(ban: Ban) {
|
||||
this._ban = ban;
|
||||
return this;
|
||||
}
|
||||
|
||||
async create() {
|
||||
if (!this._action) {
|
||||
throw new Error("Missing action");
|
||||
}
|
||||
|
||||
return await prisma.auditLog.create({
|
||||
data: {
|
||||
action: this._action,
|
||||
userId: this._userId || null,
|
||||
reason: this._reason,
|
||||
comment: this._comment,
|
||||
|
||||
banId: this._ban?.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,6 +14,10 @@ export class Instance {
|
|||
this.instance = data;
|
||||
}
|
||||
|
||||
get hostname() {
|
||||
return this.instance.hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Instance instance
|
||||
*
|
||||
|
@ -91,10 +95,10 @@ export class Instance {
|
|||
publicNote: string | null | undefined,
|
||||
privateNote: string | null | undefined
|
||||
) {
|
||||
const subdomains = await Instance.getRegisteredSubdomains(
|
||||
/*const subdomains = await Instance.getRegisteredSubdomains(
|
||||
this.instance.hostname
|
||||
);
|
||||
const existing = await this.getBan();
|
||||
const existing = await this.getBan();*/
|
||||
const ban = await prisma.ban.upsert({
|
||||
where: {
|
||||
instanceId: this.instance.id,
|
||||
|
@ -112,6 +116,8 @@ export class Instance {
|
|||
privateNote,
|
||||
},
|
||||
});
|
||||
|
||||
return ban;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -126,11 +132,11 @@ export class Instance {
|
|||
|
||||
if (!existing) throw new InstanceNotBanned();
|
||||
|
||||
await prisma.ban.delete({
|
||||
where: {
|
||||
id: existing.id,
|
||||
},
|
||||
const ban = await prisma.ban.delete({
|
||||
where: { id: existing.id },
|
||||
});
|
||||
|
||||
return ban;
|
||||
}
|
||||
|
||||
static async fromDomain(hostname: string): Promise<Instance> {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from "@sc07-canvas/lib/src/net";
|
||||
import { Ban, User as UserDB } from "@prisma/client";
|
||||
import { Instance } from "./Instance";
|
||||
import { ConditionalPromise } from "../lib/utils";
|
||||
|
||||
const Logger = getLogger();
|
||||
|
||||
|
@ -40,7 +41,7 @@ export class User {
|
|||
pixelStack: number;
|
||||
authSession?: AuthSession;
|
||||
undoExpires?: Date;
|
||||
ban?: IUserBan;
|
||||
private _ban?: IUserBan;
|
||||
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
|
@ -90,7 +91,7 @@ export class User {
|
|||
|
||||
private async updateBanFromUserData(userData: UserDB & { Ban: Ban | null }) {
|
||||
if (userData.Ban) {
|
||||
this.ban = {
|
||||
this._ban = {
|
||||
id: userData.Ban.id,
|
||||
expires: userData.Ban.expiresAt,
|
||||
publicNote: userData.Ban.publicNote,
|
||||
|
@ -173,12 +174,14 @@ export class User {
|
|||
* Sends packet to all user's sockets with current standing information
|
||||
*/
|
||||
updateStanding() {
|
||||
if (this.ban) {
|
||||
const ban = this.getBan();
|
||||
|
||||
if (ban) {
|
||||
for (const socket of this.sockets) {
|
||||
socket.emit("standing", {
|
||||
banned: true,
|
||||
until: this.ban.expires.toISOString(),
|
||||
reason: this.ban.publicNote || undefined,
|
||||
until: ban.expires.toISOString(),
|
||||
reason: ban.publicNote || undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
@ -188,6 +191,83 @@ export class User {
|
|||
}
|
||||
}
|
||||
|
||||
getBan<DoUpdate extends boolean = false>(
|
||||
update: DoUpdate = false as DoUpdate
|
||||
): ConditionalPromise<typeof this._ban, DoUpdate> {
|
||||
if (update) {
|
||||
return new Promise(async (res) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
sub: this.sub,
|
||||
},
|
||||
include: {
|
||||
Ban: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user?.Ban) {
|
||||
return res(undefined);
|
||||
}
|
||||
|
||||
this._ban = {
|
||||
type: "user",
|
||||
id: user.Ban.id,
|
||||
expires: user.Ban.expiresAt,
|
||||
publicNote: user.Ban.publicNote,
|
||||
};
|
||||
|
||||
res(this._ban);
|
||||
}) as any;
|
||||
} else {
|
||||
return this._ban as any;
|
||||
}
|
||||
}
|
||||
|
||||
async ban(
|
||||
expires: Date,
|
||||
publicNote: string | null | undefined,
|
||||
privateNote: string | null | undefined
|
||||
) {
|
||||
const ban = await prisma.ban.upsert({
|
||||
where: {
|
||||
userId: this.sub,
|
||||
},
|
||||
create: {
|
||||
userId: this.sub,
|
||||
expiresAt: expires,
|
||||
publicNote,
|
||||
privateNote,
|
||||
},
|
||||
update: {
|
||||
userId: this.sub,
|
||||
expiresAt: expires,
|
||||
publicNote,
|
||||
privateNote,
|
||||
},
|
||||
});
|
||||
|
||||
this._ban = {
|
||||
id: ban.id,
|
||||
type: "user",
|
||||
expires,
|
||||
publicNote: publicNote || null,
|
||||
};
|
||||
|
||||
return ban;
|
||||
}
|
||||
|
||||
async unban() {
|
||||
const existing = await this.getBan(true);
|
||||
|
||||
if (!existing) throw new UserNotBanned();
|
||||
|
||||
const ban = await prisma.ban.delete({
|
||||
where: { id: existing.id },
|
||||
});
|
||||
|
||||
return ban;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all sockets for this user of a message
|
||||
* @param alert
|
||||
|
@ -249,3 +329,10 @@ export class UserNotFound extends Error {
|
|||
this.name = "UserNotFound";
|
||||
}
|
||||
}
|
||||
|
||||
export class UserNotBanned extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "UserNotBanned";
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue