Skip to content

Commit 58a77d6

Browse files
committed
refactor to use repository
1 parent c42932a commit 58a77d6

File tree

11 files changed

+120
-44
lines changed

11 files changed

+120
-44
lines changed

client/src/locales/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@
756756
"infoBox": {
757757
"description": "Configure where you want to receive notifications when monitors detect issues. Connect email, Slack, Discord, webhooks, and other channels to stay informed.",
758758
"title": "Notification Channels"
759+
},
760+
"table": {
761+
"headers": {
762+
"destination": "Destination"
763+
}
759764
}
760765
},
761766
"profile": {
@@ -894,4 +899,4 @@
894899
}
895900
}
896901
}
897-
}
902+
}

client/src/pages/notification-channels/NotificationChannels.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useTheme } from "@mui/material/styles";
1010
import { useNavigate } from "react-router";
1111
import type { ActionMenuItem } from "@/components/actions-menu";
1212
import type { Header } from "@/components/design-elements/Table";
13-
import type { INotificationChannel } from "@/types/notification-channel";
13+
import type { NotificationChannel } from "@/types/notification-channel";
1414
import { useGet, usePatch, useDelete } from "@/hooks/UseApi";
1515
import type { ApiResponse } from "@/types/api";
1616
import { useTranslation } from "react-i18next";
@@ -21,11 +21,11 @@ const NotificationChannelsPage = () => {
2121
const navigate = useNavigate();
2222
const theme = useTheme();
2323
const [selectedChannel, setSelectedChannel] =
24-
useState<INotificationChannel | null>(null);
24+
useState<NotificationChannel | null>(null);
2525
const open = Boolean(selectedChannel);
2626

2727
const { response, isValidating, error, refetch } = useGet<
28-
ApiResponse<INotificationChannel[]>
28+
ApiResponse<NotificationChannel[]>
2929
>(
3030
"/notification-channels",
3131
{},
@@ -37,15 +37,15 @@ const NotificationChannelsPage = () => {
3737
{ useTeamIdAsKey: true }
3838
);
3939

40-
const { patch, loading: pausing } = usePatch<{}, INotificationChannel>();
40+
const { patch, loading: pausing } = usePatch<{}, NotificationChannel>();
4141
const { deleteFn, loading: isDeleting } =
42-
useDelete<ApiResponse<INotificationChannel>>();
42+
useDelete<ApiResponse<NotificationChannel>>();
4343

4444
const notificationChannels = response?.data || [];
4545

4646
const handleConfirm = async () => {
4747
if (!selectedChannel) return;
48-
const res = await deleteFn(`/notification-channels/${selectedChannel._id}`);
48+
const res = await deleteFn(`/notification-channels/${selectedChannel.id}`);
4949
if (res) {
5050
setSelectedChannel(null);
5151
refetch();
@@ -56,13 +56,13 @@ const NotificationChannelsPage = () => {
5656
setSelectedChannel(null);
5757
};
5858

59-
const getActions = (channel: INotificationChannel): ActionMenuItem[] => {
59+
const getActions = (channel: NotificationChannel): ActionMenuItem[] => {
6060
return [
6161
{
6262
id: 1,
6363
label: t("monitors.common.actions.configure"),
6464
action: () => {
65-
navigate(`/notification-channels/${channel._id}/configure`);
65+
navigate(`/notification-channels/${channel.id}/configure`);
6666
},
6767
closeMenu: true,
6868
},
@@ -73,7 +73,7 @@ const NotificationChannelsPage = () => {
7373
: t("common.buttons.enable"),
7474
action: async () => {
7575
const res = await patch(
76-
`/notification-channels/${channel._id}/active`,
76+
`/notification-channels/${channel.id}/active`,
7777
{}
7878
);
7979
if (res) {
@@ -98,7 +98,7 @@ const NotificationChannelsPage = () => {
9898
};
9999

100100
const getHeaders = () => {
101-
const headers: Header<INotificationChannel>[] = [
101+
const headers: Header<NotificationChannel>[] = [
102102
{
103103
id: "name",
104104
content: t("common.table.headers.name"),
@@ -125,6 +125,17 @@ const NotificationChannelsPage = () => {
125125
);
126126
},
127127
},
128+
{
129+
id: "destination",
130+
content: t("notificationChannels.table.headers.destination"),
131+
render: (row) => {
132+
return (
133+
<Typography>
134+
{row?.config?.url || row?.config?.emailAddress}
135+
</Typography>
136+
);
137+
},
138+
},
128139
{
129140
id: "actions",
130141
content: t("common.table.headers.actions"),
@@ -167,7 +178,7 @@ const NotificationChannelsPage = () => {
167178
headers={headers}
168179
data={notificationChannels}
169180
onRowClick={(row) => {
170-
navigate(`/notification-channels/${row._id}/configure`);
181+
navigate(`/notification-channels/${row.id}/configure`);
171182
}}
172183
/>
173184

client/src/pages/notification-channels/NotificationChannelsConfig.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ import { useParams } from "react-router";
44
import { useNavigate } from "react-router";
55
import { usePatch, useGet } from "@/hooks/UseApi";
66
import type { ApiResponse } from "@/types/api";
7-
import type { INotificationChannel } from "@/types/notification-channel";
7+
import type { NotificationChannel } from "@/types/notification-channel";
88
import type { FormValues } from "./NotificationChannelsForm";
99

1010
const NotificationsChannelConfigPage = () => {
1111
const navigate = useNavigate();
1212
const { id } = useParams<{ id: string }>();
13-
const { response, loading } = useGet<ApiResponse<INotificationChannel>>(
13+
const { response, loading } = useGet<ApiResponse<NotificationChannel>>(
1414
`/notification-channels/${id}`
1515
);
1616
const { patch, loading: updating } = usePatch<
1717
FormValues,
18-
INotificationChannel
18+
NotificationChannel
1919
>();
2020
const notification = response?.data;
2121

client/src/pages/notification-channels/NotificationChannelsCreate.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { NotificationChannelsForm } from "./NotificationChannelsForm";
22

33
import { useNavigate } from "react-router";
44
import { usePost } from "@/hooks/UseApi";
5-
import type { INotificationChannel } from "@/types/notification-channel";
5+
import type { NotificationChannel } from "@/types/notification-channel";
66
import type { FormValues } from "./NotificationChannelsForm";
77

88
const NotificationsChannelCreatePage = () => {
9-
const { post, loading } = usePost<FormValues, INotificationChannel>();
9+
const { post, loading } = usePost<FormValues, NotificationChannel>();
1010
const navigate = useNavigate();
1111

1212
const onSubmit = async (data: FormValues) => {

client/src/types/notification-channel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
22
export type ChannelType = (typeof ChannelTypes)[number];
33

4-
export interface INotificationChannelConfig {
4+
export interface NotificationChannelConfig {
55
url?: string; // For webhook, slack, discord
66
emailAddress?: string; // For email
77
}
88

9-
export interface INotificationChannel {
10-
_id: string;
9+
export interface NotificationChannel {
10+
id: string;
1111
orgId: string;
1212
teamId: string;
1313
name: string;
1414
type: ChannelType;
15-
config: INotificationChannelConfig;
16-
isActive: boolean;
15+
config: NotificationChannelConfig;
16+
isActive: boolean | unknown;
1717
createdBy: string;
1818
updatedBy: string;
1919
createdAt: Date;

server/src/controllers/RecoveryController.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ class RecoveryController implements IRecoveryController {
4646

4747
if (user) {
4848
try {
49-
const recoveryToken = await this.recoveryService.create(user._id);
49+
const recoveryToken = await this.recoveryService.create(
50+
user._id.toString()
51+
);
5052
// Don't wait for email to be sent
5153
this.emailService
5254
.sendGeneric(user.email, "Password Recovery", {
@@ -77,7 +79,7 @@ class RecoveryController implements IRecoveryController {
7779
recoveryToken.userId.toString(),
7880
password
7981
);
80-
await this.recoveryService.delete(recoveryToken._id);
82+
await this.recoveryService.delete(recoveryToken.id);
8183

8284
const user = await User.findById(recoveryToken.userId);
8385
if (!user) {

server/src/init/services.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
MongoOrgMembershipRepository,
4848
MongoMaintenanceRepository,
4949
MongoNotificationChannelRepository,
50+
MongoRecoveryTokenRepository,
5051
} from "@/repositories/index.js";
5152

5253
export const initServices = async () => {
@@ -62,7 +63,9 @@ export const initServices = async () => {
6263
const orgRepository = new MongoOrgRepository();
6364
const orgMembershipRepository = new MongoOrgMembershipRepository();
6465
const maintenanceRepository = new MongoMaintenanceRepository();
65-
const notificationChannelRepository = new MongoNotificationChannelRepository();
66+
const notificationChannelRepository =
67+
new MongoNotificationChannelRepository();
68+
const recoveryTokenRepository = new MongoRecoveryTokenRepository();
6669

6770
const checkService = new CheckService(checksRepository, monitorRepository);
6871
const inviteService = new InviteService(
@@ -147,11 +150,11 @@ export const initServices = async () => {
147150
);
148151
const queueService = new QueueService(jobQueue);
149152
const teamService = new TeamService(jobQueue, monitorRepository);
150-
const roleService = new RoleService();
153+
const roleService = new RoleService(roleRepository);
151154
const teamMemberService = new TeamMemberService();
152155
const statusPageService = new StatusPageService();
153156
const diagnosticService = new DiagnosticService(jobQueue);
154-
const recoveryService = new RecoveryService();
157+
const recoveryService = new RecoveryService(recoveryTokenRepository);
155158
const services = {
156159
checkService,
157160
inviteService,
Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,12 @@
1-
export interface IRecoveryTokenRepistory {}
1+
import type { RecoveryToken } from "@/types/domain/index.js";
2+
3+
export interface IRecoveryTokenRepistory {
4+
// create
5+
create(userId: string, tokenHash: string): Promise<boolean>;
6+
// single fetch
7+
findByTokenHash(tokenHash: string): Promise<RecoveryToken | null>;
8+
// collection fetch
9+
// update
10+
// delete
11+
deleteByid(tokenId: string): Promise<boolean>;
12+
}
Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
1+
import { type IRecoveryToken, RecoveryToken } from "@/db/models/index.js";
2+
import type { RecoveryToken as RecoveryTokenEntity } from "@/types/domain/index.js";
3+
14
import { IRecoveryTokenRepistory } from "@/repositories/index.js";
25

3-
class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {}
6+
class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {
7+
private toEntity = (doc: IRecoveryToken): RecoveryTokenEntity => {
8+
return {
9+
id: doc._id.toString(),
10+
userId: doc.userId.toString(),
11+
tokenHash: doc.tokenHash,
12+
expiry: doc.expiry,
13+
createdAt: doc.createdAt,
14+
updatedAt: doc.updatedAt,
15+
};
16+
};
17+
create = async (userId: string, tokenHash: string) => {
18+
const recoveryToken = await RecoveryToken.create({
19+
userId,
20+
tokenHash,
21+
});
22+
if (!recoveryToken) return false;
23+
return true;
24+
};
25+
26+
findByTokenHash = async (tokenHash: string) => {
27+
const recoveryToken = await RecoveryToken.findOne({ tokenHash });
28+
if (!recoveryToken) return null;
29+
return this.toEntity(recoveryToken);
30+
};
31+
32+
deleteByid = async (tokenId: string) => {
33+
const result = await RecoveryToken.deleteOne({ _id: tokenId });
34+
return result.deletedCount === 1;
35+
};
36+
}
437

538
export default MongoRecoveryTokenRepository;

server/src/services/business/RecoveryService.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
11
import crypto from "node:crypto";
2-
import { RecoveryToken, IRecoveryToken } from "@/db/models/index.js";
32
import ApiError from "@/utils/ApiError.js";
4-
import { Types } from "mongoose";
3+
import { IRecoveryTokenRepistory } from "@/repositories/index.js";
4+
import type { RecoveryToken } from "@/types/domain/index.js";
55
const SERVICE_NAME = "RecoveryService";
66

77
export interface IRecoveryService {
8-
create: (userId: Types.ObjectId) => Promise<string>;
9-
get: (token: string) => Promise<IRecoveryToken>;
8+
create: (userId: string) => Promise<string>;
9+
get: (token: string) => Promise<RecoveryToken>;
1010
delete: (id: string) => Promise<boolean>;
1111
}
1212

1313
class RecoveryService implements IRecoveryService {
1414
public SERVICE_NAME: string;
15-
constructor() {
15+
16+
private recoveryTokenRepository: IRecoveryTokenRepistory;
17+
constructor(recoveryTokenRepository: IRecoveryTokenRepistory) {
1618
this.SERVICE_NAME = SERVICE_NAME;
19+
this.recoveryTokenRepository = recoveryTokenRepository;
1720
}
1821

19-
create = async (userId: Types.ObjectId) => {
22+
create = async (userId: string) => {
2023
const token = crypto.randomBytes(32).toString("hex");
2124
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
2225

23-
await RecoveryToken.create({
26+
const didCreate = await this.recoveryTokenRepository.create(
2427
userId,
25-
tokenHash,
26-
});
28+
tokenHash
29+
);
30+
if (!didCreate) {
31+
throw new ApiError("Failed to create recovery token", 500);
32+
}
33+
2734
return token;
2835
};
2936

3037
get = async (token: string) => {
3138
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
3239

33-
const recoveryToken = await RecoveryToken.findOne({ tokenHash });
40+
const recoveryToken = await this.recoveryTokenRepository.findByTokenHash(
41+
tokenHash
42+
);
3443
if (!recoveryToken) {
3544
throw new ApiError("Recovery token not found", 404);
3645
}
@@ -43,11 +52,11 @@ class RecoveryService implements IRecoveryService {
4352
};
4453

4554
delete = async (id: string) => {
46-
const result = await RecoveryToken.deleteOne({ _id: id });
47-
if (!result.deletedCount) {
55+
const didDelete = await this.recoveryTokenRepository.deleteByid(id);
56+
if (!didDelete) {
4857
throw new ApiError("Recovery token not found", 404);
4958
}
50-
return result.deletedCount === 1;
59+
return didDelete;
5160
};
5261
}
5362

0 commit comments

Comments
 (0)