refactor to use repository
This commit is contained in:
1 parent
c42932a7b3
commit
58a77d6aa2
11 files changed
+120
-44
No files matched your search
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user