feat: 完成举报拉黑和审核处置闭环
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard.js";
|
||||
import { CurrentUser } from "../auth/current-user.decorator.js";
|
||||
import type { AccessClaims } from "../auth/token.service.js";
|
||||
import { StateChangingOriginGuard } from "../conversation/state-changing-origin.guard.js";
|
||||
import {
|
||||
NotificationQueryDto,
|
||||
PushPreferenceDto,
|
||||
ReadNotificationsDto,
|
||||
} from "../safety/dto.js";
|
||||
import { NotificationService } from "./notification.service.js";
|
||||
@Controller()
|
||||
@UseGuards(AuthGuard)
|
||||
export class NotificationController {
|
||||
constructor(
|
||||
@Inject(NotificationService)
|
||||
private readonly notifications: NotificationService,
|
||||
) {}
|
||||
@Get("notifications") list(
|
||||
@CurrentUser() u: AccessClaims,
|
||||
@Query() q: NotificationQueryDto,
|
||||
) {
|
||||
return this.notifications.list(u.sub, q.cursor, q.limit);
|
||||
}
|
||||
@UseGuards(StateChangingOriginGuard) @Post("notifications/read") read(
|
||||
@CurrentUser() u: AccessClaims,
|
||||
@Body() d: ReadNotificationsDto,
|
||||
) {
|
||||
return this.notifications.read(u.sub, d.ids);
|
||||
}
|
||||
@Get("settings/push") pref(@CurrentUser() u: AccessClaims) {
|
||||
return this.notifications.preference(u.sub);
|
||||
}
|
||||
@UseGuards(StateChangingOriginGuard) @Put("settings/push") update(
|
||||
@CurrentUser() u: AccessClaims,
|
||||
@Body() d: PushPreferenceDto,
|
||||
) {
|
||||
return this.notifications.updatePreference(u.sub, d.inAppEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[Object, NotificationQueryDto],
|
||||
NotificationController.prototype,
|
||||
"list",
|
||||
);
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[Object, ReadNotificationsDto],
|
||||
NotificationController.prototype,
|
||||
"read",
|
||||
);
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[Object],
|
||||
NotificationController.prototype,
|
||||
"pref",
|
||||
);
|
||||
Reflect.defineMetadata(
|
||||
"design:paramtypes",
|
||||
[Object, PushPreferenceDto],
|
||||
NotificationController.prototype,
|
||||
"update",
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AuthModule } from "../auth/auth.module.js";
|
||||
import { DatabaseModule } from "../database/database.module.js";
|
||||
import { StateChangingOriginGuard } from "../conversation/state-changing-origin.guard.js";
|
||||
import { NotificationController } from "./notification.controller.js";
|
||||
import { NotificationService } from "./notification.service.js";
|
||||
@Module({
|
||||
imports: [DatabaseModule, AuthModule],
|
||||
controllers: [NotificationController],
|
||||
providers: [NotificationService, StateChangingOriginGuard],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { HttpStatus, Inject, Injectable } from "@nestjs/common";
|
||||
import { ErrorCode } from "@drift/contracts";
|
||||
import { PrismaService } from "../database/prisma.service.js";
|
||||
import { DomainException } from "../common/domain.exception.js";
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
|
||||
async list(accountId: string, cursor: string | undefined, limit: number) {
|
||||
let decoded: { t: string; id: string } | undefined;
|
||||
try {
|
||||
if (cursor) {
|
||||
const value: unknown = JSON.parse(
|
||||
Buffer.from(cursor, "base64url").toString("utf8"),
|
||||
);
|
||||
if (
|
||||
typeof value !== "object" ||
|
||||
value === null ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).sort().join(",") !== "id,t" ||
|
||||
!("t" in value) ||
|
||||
typeof value.t !== "string" ||
|
||||
!("id" in value) ||
|
||||
typeof value.id !== "string" ||
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
value.id,
|
||||
) ||
|
||||
!Number.isFinite(Date.parse(value.t)) ||
|
||||
new Date(value.t).toISOString() !== value.t
|
||||
)
|
||||
throw new Error("Invalid cursor");
|
||||
decoded = { t: value.t, id: value.id };
|
||||
}
|
||||
} catch {
|
||||
throw new DomainException(
|
||||
ErrorCode.VALIDATION_ERROR,
|
||||
"Invalid cursor",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const date = decoded ? new Date(decoded.t) : undefined;
|
||||
const rows = await this.prisma.notification.findMany({
|
||||
where: {
|
||||
accountId,
|
||||
...(decoded && date
|
||||
? {
|
||||
OR: [
|
||||
{ createdAt: { lt: date } },
|
||||
{ createdAt: date, id: { lt: decoded.id } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
|
||||
take: limit + 1,
|
||||
});
|
||||
const page = rows.slice(0, limit);
|
||||
return {
|
||||
items: page.map((x) => ({
|
||||
id: x.id,
|
||||
type: x.type,
|
||||
payload: x.payload,
|
||||
status: x.status,
|
||||
readAt: x.readAt,
|
||||
createdAt: x.createdAt,
|
||||
})),
|
||||
nextCursor:
|
||||
rows.length > limit && page.length
|
||||
? Buffer.from(
|
||||
JSON.stringify({
|
||||
t: page.at(-1)!.createdAt.toISOString(),
|
||||
id: page.at(-1)!.id,
|
||||
}),
|
||||
).toString("base64url")
|
||||
: null,
|
||||
};
|
||||
}
|
||||
async read(accountId: string, ids: string[]) {
|
||||
const owned = await this.prisma.notification.count({
|
||||
where: { accountId, id: { in: ids } },
|
||||
});
|
||||
if (owned !== new Set(ids).size)
|
||||
throw new DomainException(
|
||||
ErrorCode.NOT_FOUND,
|
||||
"Not found",
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
await this.prisma.notification.updateMany({
|
||||
where: { accountId, id: { in: ids } },
|
||||
data: { status: "READ", readAt: new Date() },
|
||||
});
|
||||
return { read: owned };
|
||||
}
|
||||
async preference(accountId: string) {
|
||||
const row = await this.prisma.pushPreference.findUnique({
|
||||
where: { accountId },
|
||||
});
|
||||
return { inAppEnabled: row?.inAppEnabled ?? true };
|
||||
}
|
||||
async updatePreference(accountId: string, inAppEnabled: boolean) {
|
||||
return this.prisma.pushPreference.upsert({
|
||||
where: { accountId },
|
||||
create: { accountId, inAppEnabled },
|
||||
update: { inAppEnabled },
|
||||
select: { inAppEnabled: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user