feat: add notification test rpc action
This commit is contained in:
parent
324edc2d88
commit
a3eb5a14eb
@ -41,6 +41,7 @@
|
|||||||
"maxmind": "^4.3.11",
|
"maxmind": "^4.3.11",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
|
"nodemailer": "^6.9.5",
|
||||||
"openbadge": "^1.0.4",
|
"openbadge": "^1.0.4",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@ -68,6 +69,7 @@
|
|||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"@types/morgan": "^1.9.5",
|
"@types/morgan": "^1.9.5",
|
||||||
"@types/node": "^18.17.12",
|
"@types/node": "^18.17.12",
|
||||||
|
"@types/nodemailer": "^6.4.11",
|
||||||
"@types/passport": "^1.0.12",
|
"@types/passport": "^1.0.12",
|
||||||
"@types/passport-jwt": "^3.0.9",
|
"@types/passport-jwt": "^3.0.9",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
|
@ -85,6 +85,9 @@ dependencies:
|
|||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^3.3.6
|
specifier: ^3.3.6
|
||||||
version: 3.3.6
|
version: 3.3.6
|
||||||
|
nodemailer:
|
||||||
|
specifier: ^6.9.5
|
||||||
|
version: 6.9.5
|
||||||
openbadge:
|
openbadge:
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
@ -162,6 +165,9 @@ devDependencies:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^18.17.12
|
specifier: ^18.17.12
|
||||||
version: 18.17.12
|
version: 18.17.12
|
||||||
|
'@types/nodemailer':
|
||||||
|
specifier: ^6.4.11
|
||||||
|
version: 6.4.11
|
||||||
'@types/passport':
|
'@types/passport':
|
||||||
specifier: ^1.0.12
|
specifier: ^1.0.12
|
||||||
version: 1.0.12
|
version: 1.0.12
|
||||||
@ -2029,6 +2035,12 @@ packages:
|
|||||||
/@types/node@18.17.12:
|
/@types/node@18.17.12:
|
||||||
resolution: {integrity: sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==}
|
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:
|
/@types/passport-jwt@3.0.9:
|
||||||
resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==}
|
resolution: {integrity: sha512-5XJt+79emfgpuBvBQusUPylFIVtW1QVAAkTRwCbRJAmxUjmLtIqUU6V1ovpnHPu6Qut3mR5Juc+s7kd06roNTg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4159,6 +4171,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
||||||
dev: true
|
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:
|
/nodemon@2.0.22:
|
||||||
resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==}
|
resolution: {integrity: sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createTRPCReact } from '@trpc/react-query';
|
import { createTRPCReact } from '@trpc/react-query';
|
||||||
import type { AppRouter } from '../../server/trpc';
|
import type { AppRouter } from '../../server/trpc';
|
||||||
import { httpBatchLink } from '@trpc/client';
|
import { httpBatchLink, TRPCClientErrorLike } from '@trpc/client';
|
||||||
import { getJWT } from './auth';
|
import { getJWT } from './auth';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
export const trpc = createTRPCReact<AppRouter>();
|
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);
|
||||||
|
}
|
||||||
|
@ -3,12 +3,19 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
FormProps,
|
FormProps,
|
||||||
Input,
|
Input,
|
||||||
|
message,
|
||||||
Modal,
|
Modal,
|
||||||
ModalProps,
|
ModalProps,
|
||||||
Select,
|
Select,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
defaultErrorHandler,
|
||||||
|
defaultSuccessHandler,
|
||||||
|
trpc,
|
||||||
|
} from '../../../api/trpc';
|
||||||
import { useEvent } from '../../../hooks/useEvent';
|
import { useEvent } from '../../../hooks/useEvent';
|
||||||
|
import { useCurrentWorkspaceId } from '../../../store/user';
|
||||||
import { notificationStrategies } from './strategies';
|
import { notificationStrategies } from './strategies';
|
||||||
|
|
||||||
export interface NotificationFormValues {
|
export interface NotificationFormValues {
|
||||||
@ -32,6 +39,12 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
|
|||||||
React.memo((props) => {
|
React.memo((props) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const typeValue = Form.useWatch('type', form);
|
const typeValue = Form.useWatch('type', form);
|
||||||
|
const currentWorkspaceId = useCurrentWorkspaceId()!;
|
||||||
|
|
||||||
|
const testMutation = trpc.notification.test.useMutation({
|
||||||
|
onSuccess: defaultSuccessHandler,
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
|
||||||
const formEl = useMemo(() => {
|
const formEl = useMemo(() => {
|
||||||
const strategy = notificationStrategies.find((s) => s.name === typeValue);
|
const strategy = notificationStrategies.find((s) => s.name === typeValue);
|
||||||
@ -63,7 +76,12 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
|
|||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
const { name, type, payload } = values;
|
const { name, type, payload } = values;
|
||||||
|
|
||||||
console.log('TODO', { name, type, payload });
|
await testMutation.mutateAsync({
|
||||||
|
workspaceId: currentWorkspaceId,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -76,7 +94,9 @@ export const NotificationInfoModal: React.FC<NotificationInfoModalProps> =
|
|||||||
onCancel={props.onCancel}
|
onCancel={props.onCancel}
|
||||||
footer={
|
footer={
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={handleTest}>Test</Button>
|
<Button loading={testMutation.isLoading} onClick={handleTest}>
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
<Button type="primary" onClick={handleSave}>
|
<Button type="primary" onClick={handleSave}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
15
src/server/model/notification/index.ts
Normal file
15
src/server/model/notification/index.ts
Normal 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);
|
||||||
|
}
|
6
src/server/model/notification/provider/index.ts
Normal file
6
src/server/model/notification/provider/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { smtp } from './smtp';
|
||||||
|
import type { NotificationProvider } from './type';
|
||||||
|
|
||||||
|
export const notificationProviders: Record<string, NotificationProvider> = {
|
||||||
|
smtp,
|
||||||
|
};
|
50
src/server/model/notification/provider/smtp.ts
Normal file
50
src/server/model/notification/provider/smtp.ts
Normal 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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
8
src/server/model/notification/provider/type.ts
Normal file
8
src/server/model/notification/provider/type.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Notification } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface NotificationProvider {
|
||||||
|
send: (
|
||||||
|
notification: Pick<Notification, 'name' | 'type' | 'payload'>,
|
||||||
|
message: string
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
@ -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 });
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,6 +1,7 @@
|
|||||||
import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc';
|
import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { prisma } from '../../model/_client';
|
import { prisma } from '../../model/_client';
|
||||||
|
import { sendNotification } from '../../model/notification';
|
||||||
|
|
||||||
export const notificationRouter = router({
|
export const notificationRouter = router({
|
||||||
getAll: workspaceProcedure.query(({ input }) => {
|
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
|
upsert: workspaceOwnerProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
Loading…
Reference in New Issue
Block a user