refactor to use repository

This commit is contained in:
Alex Holliday committed 2026-01-08 17:47:26 +00:00
1 parent c42932a7b3
commit 58a77d6aa2
11 files changed
+120 -44

No files matched your search

+6 -1
View File
@@ -756,6 +756,11 @@
"infoBox": {
"description": "Configure where you want to receive notifications when monitors detect issues. Connect email, Slack, Discord, webhooks, and other channels to stay informed.",
"title": "Notification Channels"
},
"table": {
"headers": {
"destination": "Destination"
}
}
},
"profile": {
@@ -894,4 +899,4 @@
}
}
}
}
}
@@ -10,7 +10,7 @@ import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router";
import type { ActionMenuItem } from "@/components/actions-menu";
import type { Header } from "@/components/design-elements/Table";
import type { INotificationChannel } from "@/types/notification-channel";
import type { NotificationChannel } from "@/types/notification-channel";
import { useGet, usePatch, useDelete } from "@/hooks/UseApi";
import type { ApiResponse } from "@/types/api";
import { useTranslation } from "react-i18next";
@@ -21,11 +21,11 @@ const NotificationChannelsPage = () => {
const navigate = useNavigate();
const theme = useTheme();
const [selectedChannel, setSelectedChannel] =
useState<INotificationChannel | null>(null);
useState<NotificationChannel | null>(null);
const open = Boolean(selectedChannel);
const { response, isValidating, error, refetch } = useGet<
ApiResponse<INotificationChannel[]>
ApiResponse<NotificationChannel[]>
>(
"/notification-channels",
{},
@@ -37,15 +37,15 @@ const NotificationChannelsPage = () => {
{ useTeamIdAsKey: true }
);
const { patch, loading: pausing } = usePatch<{}, INotificationChannel>();
const { patch, loading: pausing } = usePatch<{}, NotificationChannel>();
const { deleteFn, loading: isDeleting } =
useDelete<ApiResponse<INotificationChannel>>();
useDelete<ApiResponse<NotificationChannel>>();
const notificationChannels = response?.data || [];
const handleConfirm = async () => {
if (!selectedChannel) return;
const res = await deleteFn(`/notification-channels/${selectedChannel._id}`);
const res = await deleteFn(`/notification-channels/${selectedChannel.id}`);
if (res) {
setSelectedChannel(null);
refetch();
@@ -56,13 +56,13 @@ const NotificationChannelsPage = () => {
setSelectedChannel(null);
};
const getActions = (channel: INotificationChannel): ActionMenuItem[] => {
const getActions = (channel: NotificationChannel): ActionMenuItem[] => {
return [
{
id: 1,
label: t("monitors.common.actions.configure"),
action: () => {
navigate(`/notification-channels/${channel._id}/configure`);
navigate(`/notification-channels/${channel.id}/configure`);
},
closeMenu: true,
},
@@ -73,7 +73,7 @@ const NotificationChannelsPage = () => {
: t("common.buttons.enable"),
action: async () => {
const res = await patch(
`/notification-channels/${channel._id}/active`,
`/notification-channels/${channel.id}/active`,
{}
);
if (res) {
@@ -98,7 +98,7 @@ const NotificationChannelsPage = () => {
};
const getHeaders = () => {
const headers: Header<INotificationChannel>[] = [
const headers: Header<NotificationChannel>[] = [
{
id: "name",
content: t("common.table.headers.name"),
@@ -125,6 +125,17 @@ const NotificationChannelsPage = () => {
);
},
},
{
id: "destination",
content: t("notificationChannels.table.headers.destination"),
render: (row) => {
return (
<Typography>
{row?.config?.url || row?.config?.emailAddress}
</Typography>
);
},
},
{
id: "actions",
content: t("common.table.headers.actions"),
@@ -167,7 +178,7 @@ const NotificationChannelsPage = () => {
headers={headers}
data={notificationChannels}
onRowClick={(row) => {
navigate(`/notification-channels/${row._id}/configure`);
navigate(`/notification-channels/${row.id}/configure`);
}}
/>
@@ -4,18 +4,18 @@ import { useParams } from "react-router";
import { useNavigate } from "react-router";
import { usePatch, useGet } from "@/hooks/UseApi";
import type { ApiResponse } from "@/types/api";
import type { INotificationChannel } from "@/types/notification-channel";
import type { NotificationChannel } from "@/types/notification-channel";
import type { FormValues } from "./NotificationChannelsForm";
const NotificationsChannelConfigPage = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const { response, loading } = useGet<ApiResponse<INotificationChannel>>(
const { response, loading } = useGet<ApiResponse<NotificationChannel>>(
`/notification-channels/${id}`
);
const { patch, loading: updating } = usePatch<
FormValues,
INotificationChannel
NotificationChannel
>();
const notification = response?.data;
@@ -2,11 +2,11 @@ import { NotificationChannelsForm } from "./NotificationChannelsForm";
import { useNavigate } from "react-router";
import { usePost } from "@/hooks/UseApi";
import type { INotificationChannel } from "@/types/notification-channel";
import type { NotificationChannel } from "@/types/notification-channel";
import type { FormValues } from "./NotificationChannelsForm";
const NotificationsChannelCreatePage = () => {
const { post, loading } = usePost<FormValues, INotificationChannel>();
const { post, loading } = usePost<FormValues, NotificationChannel>();
const navigate = useNavigate();
const onSubmit = async (data: FormValues) => {
+5 -5
View File
@@ -1,19 +1,19 @@
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
export type ChannelType = (typeof ChannelTypes)[number];
export interface INotificationChannelConfig {
export interface NotificationChannelConfig {
url?: string; // For webhook, slack, discord
emailAddress?: string; // For email
}
export interface INotificationChannel {
_id: string;
export interface NotificationChannel {
id: string;
orgId: string;
teamId: string;
name: string;
type: ChannelType;
config: INotificationChannelConfig;
isActive: boolean;
config: NotificationChannelConfig;
isActive: boolean | unknown;
createdBy: string;
updatedBy: string;
createdAt: Date;
+4 -2
View File
@@ -46,7 +46,9 @@ class RecoveryController implements IRecoveryController {
if (user) {
try {
const recoveryToken = await this.recoveryService.create(user._id);
const recoveryToken = await this.recoveryService.create(
user._id.toString()
);
// Don't wait for email to be sent
this.emailService
.sendGeneric(user.email, "Password Recovery", {
@@ -77,7 +79,7 @@ class RecoveryController implements IRecoveryController {
recoveryToken.userId.toString(),
password
);
await this.recoveryService.delete(recoveryToken._id);
await this.recoveryService.delete(recoveryToken.id);
const user = await User.findById(recoveryToken.userId);
if (!user) {
+6 -3
View File
@@ -47,6 +47,7 @@ import {
MongoOrgMembershipRepository,
MongoMaintenanceRepository,
MongoNotificationChannelRepository,
MongoRecoveryTokenRepository,
} from "@/repositories/index.js";
export const initServices = async () => {
@@ -62,7 +63,9 @@ export const initServices = async () => {
const orgRepository = new MongoOrgRepository();
const orgMembershipRepository = new MongoOrgMembershipRepository();
const maintenanceRepository = new MongoMaintenanceRepository();
const notificationChannelRepository = new MongoNotificationChannelRepository();
const notificationChannelRepository =
new MongoNotificationChannelRepository();
const recoveryTokenRepository = new MongoRecoveryTokenRepository();
const checkService = new CheckService(checksRepository, monitorRepository);
const inviteService = new InviteService(
@@ -147,11 +150,11 @@ export const initServices = async () => {
);
const queueService = new QueueService(jobQueue);
const teamService = new TeamService(jobQueue, monitorRepository);
const roleService = new RoleService();
const roleService = new RoleService(roleRepository);
const teamMemberService = new TeamMemberService();
const statusPageService = new StatusPageService();
const diagnosticService = new DiagnosticService(jobQueue);
const recoveryService = new RecoveryService();
const recoveryService = new RecoveryService(recoveryTokenRepository);
const services = {
checkService,
inviteService,
@@ -1 +1,12 @@
export interface IRecoveryTokenRepistory {}
import type { RecoveryToken } from "@/types/domain/index.js";
export interface IRecoveryTokenRepistory {
// create
create(userId: string, tokenHash: string): Promise<boolean>;
// single fetch
findByTokenHash(tokenHash: string): Promise<RecoveryToken | null>;
// collection fetch
// update
// delete
deleteByid(tokenId: string): Promise<boolean>;
}
@@ -1,5 +1,38 @@
import { type IRecoveryToken, RecoveryToken } from "@/db/models/index.js";
import type { RecoveryToken as RecoveryTokenEntity } from "@/types/domain/index.js";
import { IRecoveryTokenRepistory } from "@/repositories/index.js";
class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {}
class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {
private toEntity = (doc: IRecoveryToken): RecoveryTokenEntity => {
return {
id: doc._id.toString(),
userId: doc.userId.toString(),
tokenHash: doc.tokenHash,
expiry: doc.expiry,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
};
create = async (userId: string, tokenHash: string) => {
const recoveryToken = await RecoveryToken.create({
userId,
tokenHash,
});
if (!recoveryToken) return false;
return true;
};
findByTokenHash = async (tokenHash: string) => {
const recoveryToken = await RecoveryToken.findOne({ tokenHash });
if (!recoveryToken) return null;
return this.toEntity(recoveryToken);
};
deleteByid = async (tokenId: string) => {
const result = await RecoveryToken.deleteOne({ _id: tokenId });
return result.deletedCount === 1;
};
}
export default MongoRecoveryTokenRepository;
+22 -13
View File
@@ -1,36 +1,45 @@
import crypto from "node:crypto";
import { RecoveryToken, IRecoveryToken } from "@/db/models/index.js";
import ApiError from "@/utils/ApiError.js";
import { Types } from "mongoose";
import { IRecoveryTokenRepistory } from "@/repositories/index.js";
import type { RecoveryToken } from "@/types/domain/index.js";
const SERVICE_NAME = "RecoveryService";
export interface IRecoveryService {
create: (userId: Types.ObjectId) => Promise<string>;
get: (token: string) => Promise<IRecoveryToken>;
create: (userId: string) => Promise<string>;
get: (token: string) => Promise<RecoveryToken>;
delete: (id: string) => Promise<boolean>;
}
class RecoveryService implements IRecoveryService {
public SERVICE_NAME: string;
constructor() {
private recoveryTokenRepository: IRecoveryTokenRepistory;
constructor(recoveryTokenRepository: IRecoveryTokenRepistory) {
this.SERVICE_NAME = SERVICE_NAME;
this.recoveryTokenRepository = recoveryTokenRepository;
}
create = async (userId: Types.ObjectId) => {
create = async (userId: string) => {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
await RecoveryToken.create({
const didCreate = await this.recoveryTokenRepository.create(
userId,
tokenHash,
});
tokenHash
);
if (!didCreate) {
throw new ApiError("Failed to create recovery token", 500);
}
return token;
};
get = async (token: string) => {
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const recoveryToken = await RecoveryToken.findOne({ tokenHash });
const recoveryToken = await this.recoveryTokenRepository.findByTokenHash(
tokenHash
);
if (!recoveryToken) {
throw new ApiError("Recovery token not found", 404);
}
@@ -43,11 +52,11 @@ class RecoveryService implements IRecoveryService {
};
delete = async (id: string) => {
const result = await RecoveryToken.deleteOne({ _id: id });
if (!result.deletedCount) {
const didDelete = await this.recoveryTokenRepository.deleteByid(id);
if (!didDelete) {
throw new ApiError("Recovery token not found", 404);
}
return result.deletedCount === 1;
return didDelete;
};
}
+4 -2
View File
@@ -1,5 +1,5 @@
import { Role, IRole } from "@/db/models/index.js";
import type { IJobQueue } from "../infrastructure/JobQueue.js";
import type { IRoleRepository } from "@/repositories/index.js";
import ApiError from "@/utils/ApiError.js";
const SERVICE_NAME = "RoleService";
@@ -11,9 +11,11 @@ export interface IRoleService {
class RoleService implements IRoleService {
public SERVICE_NAME: string;
private roleRepository: IRoleRepository;
constructor() {
constructor(roleRepository: IRoleRepository) {
this.SERVICE_NAME = SERVICE_NAME;
this.roleRepository = roleRepository;
}
getAll = async (orgId: string, type: string) => {