Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 167 additions & 94 deletions prisma/seed.ts

Large diffs are not rendered by default.

156 changes: 145 additions & 11 deletions src/resolvers/alert.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
import { GraphQLError } from "graphql";
import type { Context } from "../context.js";
import type { AlertStatus } from "../generated/prisma/client.js";
import type { InputJsonValue } from "../generated/prisma/internal/prismaNamespace.js";
import { requireRole } from "../utils/auth-guard.js";

interface CreateAlertInput {
title: string;
description: string;
severity: number;
status?: AlertStatus;
sourceId?: string;
primaryEventId?: string;
eventIds?: string[];
locationIds?: string[];
metadata?: Record<string, unknown>;
}

interface UpdateAlertInput {
title?: string;
description?: string;
severity?: number;
status?: AlertStatus;
sourceId?: string;
primaryEventId?: string;
eventIds?: string[];
locationIds?: string[];
metadata?: Record<string, unknown>;
}

export const alertResolvers = {
Query: {
Expand All @@ -8,42 +35,149 @@ export const alertResolvers = {
where: args.status ? { status: args.status } : undefined,
});
},
alert: (_parent: unknown, args: { id: number }, { prisma }: Context) => {
alert: (_parent: unknown, args: { id: string }, { prisma }: Context) => {
return prisma.alert.findUnique({ where: { id: args.id } });
},
},
Mutation: {
createAlert: async (
_parent: unknown,
args: { input: CreateAlertInput },
context: Context,
) => {
const user = requireRole(context, ["admin", "analyst"]);
const { input } = args;

const alert = await context.prisma.alert.create({
data: {
title: input.title,
description: input.description,
severity: input.severity,
status: input.status ?? "draft",
sourceId: input.sourceId,
primaryEventId: input.primaryEventId,
createdById: user.id,
metadata: input.metadata ? (input.metadata as InputJsonValue) : undefined,
events: input.eventIds?.length
? { connect: input.eventIds.map((id) => ({ id })) }
: undefined,
},
});

if (input.locationIds?.length) {
await context.prisma.alertLocation.createMany({
data: input.locationIds.map((locationId) => ({
alertId: alert.id,
locationId,
})),
});
}

return alert;
},

updateAlert: async (
_parent: unknown,
args: { id: string; input: UpdateAlertInput },
context: Context,
) => {
requireRole(context, ["admin", "analyst"]);
const { id, input } = args;

const existing = await context.prisma.alert.findUnique({ where: { id } });
if (!existing) {
throw new GraphQLError("Alert not found", {
extensions: { code: "NOT_FOUND" },
});
}

if (input.eventIds !== undefined) {
await context.prisma.alert.update({
where: { id },
data: { events: { set: input.eventIds.map((eid) => ({ id: eid })) } },
});
}

if (input.locationIds !== undefined) {
await context.prisma.alertLocation.deleteMany({ where: { alertId: id } });
if (input.locationIds.length) {
await context.prisma.alertLocation.createMany({
data: input.locationIds.map((locationId) => ({
alertId: id,
locationId,
})),
});
}
}

return context.prisma.alert.update({
where: { id },
data: {
title: input.title ?? undefined,
description: input.description ?? undefined,
severity: input.severity ?? undefined,
status: input.status ?? undefined,
sourceId: input.sourceId,
primaryEventId: input.primaryEventId,
metadata: input.metadata as InputJsonValue | undefined,
},
});
},

deleteAlert: async (
_parent: unknown,
args: { id: string },
context: Context,
) => {
requireRole(context, ["admin"]);

const existing = await context.prisma.alert.findUnique({
where: { id: args.id },
});
if (!existing) {
throw new GraphQLError("Alert not found", {
extensions: { code: "NOT_FOUND" },
});
}

await context.prisma.alert.delete({ where: { id: args.id } });
return true;
},
},
Alert: {
source: (parent: { sourceId: number | null }, _args: unknown, { prisma }: Context) => {
source: (parent: { sourceId: string | null }, _args: unknown, { prisma }: Context) => {
if (!parent.sourceId) return null;
return prisma.dataSource.findUnique({ where: { id: parent.sourceId } });
},
createdBy: (parent: { createdById: string | null }, _args: unknown, { prisma }: Context) => {
if (!parent.createdById) return null;
return prisma.user.findUnique({ where: { id: parent.createdById } });
},
primaryDetection: (
parent: { primaryDetectionId: number | null },
primaryEvent: (
parent: { primaryEventId: string | null },
_args: unknown,
{ prisma }: Context,
) => {
if (!parent.primaryDetectionId) return null;
return prisma.detection.findUnique({ where: { id: parent.primaryDetectionId } });
if (!parent.primaryEventId) return null;
return prisma.event.findUnique({ where: { id: parent.primaryEventId } });
},
detections: (parent: { id: number }, _args: unknown, { prisma }: Context) => {
return prisma.detection.findMany({ where: { alertId: parent.id } });
events: (parent: { id: string }, _args: unknown, { prisma }: Context) => {
return prisma.alert
.findUnique({ where: { id: parent.id } })
.events();
},
locations: (parent: { id: number }, _args: unknown, { prisma }: Context) => {
locations: (parent: { id: string }, _args: unknown, { prisma }: Context) => {
return prisma.alertLocation.findMany({ where: { alertId: parent.id } });
},
feedback: (parent: { id: number }, _args: unknown, { prisma }: Context) => {
feedback: (parent: { id: string }, _args: unknown, { prisma }: Context) => {
return prisma.userAlert.findMany({ where: { alertId: parent.id } });
},
},
UserAlert: {
user: (parent: { userId: string }, _args: unknown, { prisma }: Context) => {
return prisma.user.findUnique({ where: { id: parent.userId } });
},
alert: (parent: { alertId: number }, _args: unknown, { prisma }: Context) => {
alert: (parent: { alertId: string }, _args: unknown, { prisma }: Context) => {
return prisma.alert.findUnique({ where: { id: parent.alertId } });
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/resolvers/apiKey.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const apiKeyResolvers = {

revokeApiKey: async (
_parent: unknown,
args: { id: number },
args: { id: string },
context: Context,
) => {
const user = requireAuth(context);
Expand Down
62 changes: 62 additions & 0 deletions src/resolvers/auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { GraphQLError } from "graphql";
import { randomBytes } from "crypto";
import type { Context } from "../context.js";
import { requireAuth } from "../utils/auth-guard.js";

export const authResolvers = {
Query: {
Expand All @@ -7,4 +10,63 @@ export const authResolvers = {
return user;
},
},
Mutation: {
requestEmailVerification: async (
_parent: unknown,
_args: unknown,
context: Context,
) => {
const user = requireAuth(context);

if (user.emailVerified) {
throw new GraphQLError("Email is already verified", {
extensions: { code: "BAD_USER_INPUT" },
});
}

const token = randomBytes(32).toString("hex");
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours

await context.prisma.verification.create({
data: {
identifier: user.email,
value: token,
expiresAt,
},
});

// TODO: Send verification email via messaging provider
return true;
},

verifyEmail: async (
_parent: unknown,
args: { token: string },
context: Context,
) => {
const verification = await context.prisma.verification.findFirst({
where: {
value: args.token,
expiresAt: { gt: new Date() },
},
});

if (!verification) {
throw new GraphQLError("Invalid or expired verification token", {
extensions: { code: "BAD_USER_INPUT" },
});
}

await context.prisma.user.updateMany({
where: { email: verification.identifier },
data: { emailVerified: true },
});

await context.prisma.verification.delete({
where: { id: verification.id },
});

return true;
},
},
};
91 changes: 88 additions & 3 deletions src/resolvers/dataSource.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,104 @@
import { GraphQLError } from "graphql";
import type { Context } from "../context.js";
import { requireRole } from "../utils/auth-guard.js";

interface CreateDataSourceInput {
name: string;
type: string;
isActive?: boolean;
baseUrl?: string;
infoUrl?: string;
}

interface UpdateDataSourceInput {
name?: string;
type?: string;
isActive?: boolean;
baseUrl?: string;
infoUrl?: string;
}

export const dataSourceResolvers = {
Query: {
dataSources: (_parent: unknown, _args: unknown, { prisma }: Context) => {
return prisma.dataSource.findMany();
},
dataSource: (_parent: unknown, args: { id: number }, { prisma }: Context) => {
dataSource: (_parent: unknown, args: { id: string }, { prisma }: Context) => {
return prisma.dataSource.findUnique({ where: { id: args.id } });
},
},
Mutation: {
createDataSource: async (
_parent: unknown,
args: { input: CreateDataSourceInput },
context: Context,
) => {
requireRole(context, ["admin"]);
const { input } = args;

return context.prisma.dataSource.create({
data: {
name: input.name,
type: input.type,
isActive: input.isActive ?? true,
baseUrl: input.baseUrl,
infoUrl: input.infoUrl,
},
});
},

updateDataSource: async (
_parent: unknown,
args: { id: string; input: UpdateDataSourceInput },
context: Context,
) => {
requireRole(context, ["admin"]);
const { id, input } = args;

const existing = await context.prisma.dataSource.findUnique({ where: { id } });
if (!existing) {
throw new GraphQLError("DataSource not found", {
extensions: { code: "NOT_FOUND" },
});
}

return context.prisma.dataSource.update({
where: { id },
data: {
name: input.name ?? undefined,
type: input.type ?? undefined,
isActive: input.isActive ?? undefined,
baseUrl: input.baseUrl,
infoUrl: input.infoUrl,
},
});
},

deleteDataSource: async (
_parent: unknown,
args: { id: string },
context: Context,
) => {
requireRole(context, ["admin"]);

const existing = await context.prisma.dataSource.findUnique({
where: { id: args.id },
});
if (!existing) {
throw new GraphQLError("DataSource not found", {
extensions: { code: "NOT_FOUND" },
});
}

await context.prisma.dataSource.delete({ where: { id: args.id } });
return true;
},
},
DataSource: {
detections: (parent: { id: number }, _args: unknown, { prisma }: Context) => {
detections: (parent: { id: string }, _args: unknown, { prisma }: Context) => {
return prisma.detection.findMany({ where: { sourceId: parent.id } });
},
alerts: (parent: { id: number }, _args: unknown, { prisma }: Context) => {
alerts: (parent: { id: string }, _args: unknown, { prisma }: Context) => {
return prisma.alert.findMany({ where: { sourceId: parent.id } });
},
},
Expand Down
Loading
Loading