feat: add notification test rpc action

This commit is contained in:
moonrailgun 2023-09-28 01:04:17 +08:00
parent 324edc2d88
commit a3eb5a14eb
11 changed files with 155 additions and 77 deletions

View File

@ -41,6 +41,7 @@
"maxmind": "^4.3.11",
"morgan": "^1.10.0",
"nanoid": "^3.3.6",
"nodemailer": "^6.9.5",
"openbadge": "^1.0.4",
"passport": "^0.6.0",
"passport-jwt": "^4.0.1",
@ -68,6 +69,7 @@
"@types/lodash-es": "^4.17.9",
"@types/morgan": "^1.9.5",
"@types/node": "^18.17.12",
"@types/nodemailer": "^6.4.11",
"@types/passport": "^1.0.12",
"@types/passport-jwt": "^3.0.9",
"@types/react": "^18.2.21",

View File

@ -85,6 +85,9 @@ dependencies:
nanoid:
specifier: ^3.3.6
version: 3.3.6
nodemailer:
specifier: ^6.9.5
version: 6.9.5
openbadge:
specifier: ^1.0.4
version: 1.0.4
@ -162,6 +165,9 @@ devDependencies:
'@types/node':
specifier: ^18.17.12
version: 18.17.12
'@types/nodemailer':
specifier: ^6.4.11
version: 6.4.11
'@types/passport':
specifier: ^1.0.12
version: 1.0.12
@ -2029,6 +2035,12 @@ packages:
/@types/node@18.17.12:
resolution: {integrity: sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==}
/@types/nodemailer@6.4.11:
resolution: {integrity: sha512-Ld2c0frwpGT4VseuoeboCXQ7UJIkK3X7Lx/4YsZEiUHtHsthWAOCYtf6PAiLhMtfwV0cWJRabLBS3+LD8x6Nrw==}
dependencies:
'@types/node': 18.17.12
dev: true
/@types/passport-jwt@3.0.9:
resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==}
dependencies:
@ -4159,6 +4171,11 @@ packages:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true
/nodemailer@6.9.5:
resolution: {integrity: sha512-/dmdWo62XjumuLc5+AYQZeiRj+PRR8y8qKtFCOyuOl1k/hckZd8durUUHs/ucKx6/8kN+wFxqKJlQ/LK/qR5FA==}
engines: {node: '>=6.0.0'}
dev: false
/nodemon@2.0.22:
resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==}
engines: {node: '>=8.10.0'}

View File

@ -1,7 +1,8 @@
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../../server/trpc';
import { httpBatchLink } from '@trpc/client';
import { httpBatchLink, TRPCClientErrorLike } from '@trpc/client';
import { getJWT } from './auth';
import { message } from 'antd';
export const trpc = createTRPCReact<AppRouter>();
@ -17,3 +18,23 @@ export const trpcClient = trpc.createClient({
}),
],
});
/**
* @usage
* trpc.<action>.useMutation({
* onSuccess: defaultSuccessHandler,
* });
*/
export function defaultSuccessHandler(data: any) {
message.success('Operate Success');
}
/**
* @usage
* trpc.<action>.useMutation({
* onError: defaultErrorHandler,
* });
*/
export function defaultErrorHandler(error: TRPCClientErrorLike<any>) {
message.error(error.message);
}

View File

@ -3,12 +3,19 @@ import {
Form,
FormProps,
Input,
message,
Modal,
ModalProps,
Select,
} from 'antd';
import React, { useMemo } from 'react';
import {
defaultErrorHandler,
defaultSuccessHandler,
trpc,
} from '../../../api/trpc';
import { useEvent } from '../../../hooks/useEvent';
import { useCurrentWorkspaceId } from '../../../store/user';
import { notificationStrategies } from './strategies';
export interface NotificationFormValues {
@ -32,6 +39,12 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
React.memo((props) => {
const [form] = Form.useForm();
const typeValue = Form.useWatch('type', form);
const currentWorkspaceId = useCurrentWorkspaceId()!;
const testMutation = trpc.notification.test.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const formEl = useMemo(() => {
const strategy = notificationStrategies.find((s) => s.name === typeValue);
@ -63,7 +76,12 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
const values = form.getFieldsValue();
const { name, type, payload } = values;
console.log('TODO', { name, type, payload });
await testMutation.mutateAsync({
workspaceId: currentWorkspaceId,
name,
type,
payload,
});
});
return (
@ -76,7 +94,9 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
onCancel={props.onCancel}
footer={
<div>
<Button onClick={handleTest}>Test</Button>
<Button loading={testMutation.isLoading} onClick={handleTest}>
Test
</Button>
<Button type="primary" onClick={handleSave}>
Save
</Button>

View File

@ -1,27 +0,0 @@
import { prisma } from './_client';
export function getWorkspaceNotifications(workspaceId: string) {
return prisma.notification.findMany({
where: {
workspaceId,
},
});
}
export async function createWorkspaceNotification(
workspaceId: string,
name: string,
type: string,
payload: Record<string, any>
) {
const notification = await prisma.notification.create({
data: {
workspaceId,
name,
type,
payload,
},
});
return notification;
}

View File

@ -0,0 +1,15 @@
import { Notification } from '@prisma/client';
import { notificationProviders } from './provider';
export async function sendNotification(
notification: Pick<Notification, 'name' | 'type' | 'payload'>,
message: string
) {
const type = notification.type;
if (!notificationProviders[type]) {
throw new Error('Not match type:' + type);
}
await notificationProviders[type].send(notification, message);
}

View File

@ -0,0 +1,6 @@
import { smtp } from './smtp';
import type { NotificationProvider } from './type';
export const notificationProviders: Record<string, NotificationProvider> = {
smtp,
};

View File

@ -0,0 +1,50 @@
import { NotificationProvider } from './type';
import nodemailer from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
interface SMTPPayload {
hostname: string;
port: number;
security: boolean;
ignoreTLS: boolean;
username: string;
password: string;
from: string;
to: string;
cc?: string;
bcc?: string;
}
// Fork from https://github.com/louislam/uptime-kuma/blob/HEAD/server/notification-providers/smtp.js
export const smtp: NotificationProvider = {
send: async (notification, message) => {
const payload = notification.payload as unknown as SMTPPayload;
const config: SMTPTransport.Options = {
host: payload.hostname,
port: payload.port,
secure: payload.security,
ignoreTLS: payload.ignoreTLS,
};
if (payload.username || payload.password) {
config.auth = {
user: payload.username,
pass: payload.password,
};
}
const subject = message;
const bodyTextContent = message;
const transporter = nodemailer.createTransport(config);
await transporter.sendMail({
from: payload.from,
to: payload.to,
cc: payload.cc,
bcc: payload.bcc,
subject: subject,
text: bodyTextContent,
});
},
};

View File

@ -0,0 +1,8 @@
import { Notification } from '@prisma/client';
export interface NotificationProvider {
send: (
notification: Pick<Notification, 'name' | 'type' | 'payload'>,
message: string
) => Promise<void>;
}

View File

@ -1,47 +0,0 @@
import { Router } from 'express';
import { auth } from '../middleware/auth';
import { body, param, validate } from '../middleware/validate';
import { workspacePermission } from '../middleware/workspace';
import {
createWorkspaceNotification,
getWorkspaceNotifications,
} from '../model/notification';
import { ROLES } from '../utils/const';
export const notificationRouter = Router();
notificationRouter.get(
'/list',
validate(param('workspaceId').isUUID()),
auth(),
workspacePermission(),
async (req, res) => {
const list = await getWorkspaceNotifications(req.params.workspaceId);
res.json({ list });
}
);
notificationRouter.post(
'/create',
validate(
param('workspaceId').isUUID(),
body('name').isString(),
body('type').isString(),
body('payload').isObject()
),
auth(),
workspacePermission([ROLES.owner]),
async (req, res) => {
const workspaceId = req.params.workspaceId;
const { name, type, payload } = req.body;
const notification = await createWorkspaceNotification(
workspaceId,
name,
type,
payload
);
res.json({ notification });
}
);

View File

@ -1,6 +1,7 @@
import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc';
import { z } from 'zod';
import { prisma } from '../../model/_client';
import { sendNotification } from '../../model/notification';
export const notificationRouter = router({
getAll: workspaceProcedure.query(({ input }) => {
@ -12,6 +13,18 @@ export const notificationRouter = router({
},
});
}),
test: workspaceProcedure
.input(
z.object({
id: z.string().optional(),
name: z.string(),
type: z.string(),
payload: z.object({}).passthrough(),
})
)
.mutation(async ({ input }) => {
await sendNotification(input, `${input.name} + Notification Testing`);
}),
upsert: workspaceOwnerProcedure
.input(
z.object({