feat: add custom domain support for status page
This commit is contained in:
parent
f0bd4dd993
commit
a73720411c
@ -1,7 +1,7 @@
|
||||
import { Switch, Divider, Form, Input, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { MonitorPicker } from '../MonitorPicker';
|
||||
import { urlSlugValidator } from '../../../utils/validator';
|
||||
import { domainValidator, urlSlugValidator } from '../../../utils/validator';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LuMinusCircle, LuPlus } from 'react-icons/lu';
|
||||
@ -12,6 +12,7 @@ export interface MonitorStatusPageEditFormValues {
|
||||
title: string;
|
||||
slug: string;
|
||||
monitorList: PrismaJson.MonitorStatusPageList;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
interface MonitorStatusPageEditFormProps {
|
||||
@ -71,6 +72,25 @@ export const MonitorStatusPageEditForm: React.FC<MonitorStatusPageEditFormProps>
|
||||
<Input addonBefore={`${window.origin}/status/`} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('Custom Domain')}
|
||||
name="domain"
|
||||
extra={
|
||||
<div>
|
||||
{t(
|
||||
'You can config your status page in your own domain, for example: status.example.com'
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
validator: domainValidator,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.List name="monitorList">
|
||||
{(fields, { add, remove }, { errors }) => {
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { trpc } from '../../../api/trpc';
|
||||
import { useAllowEdit } from './useAllowEdit';
|
||||
import {
|
||||
@ -38,6 +38,17 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
|
||||
|
||||
const monitorList = info?.monitorList ?? [];
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
if (!info) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
...info,
|
||||
domain: info.domain ?? '',
|
||||
};
|
||||
}, [info]);
|
||||
|
||||
const [{ loading }, handleSave] = useRequest(
|
||||
async (values: MonitorStatusPageEditFormValues) => {
|
||||
if (!info) {
|
||||
@ -84,7 +95,7 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
|
||||
<div className="w-1/3 overflow-auto border-r border-gray-300 px-4 py-8 dark:border-gray-600">
|
||||
<MonitorStatusPageEditForm
|
||||
isLoading={loading}
|
||||
initialValues={info ?? {}}
|
||||
initialValues={initialValues}
|
||||
onFinish={handleSave}
|
||||
onCancel={() => setEditMode(false)}
|
||||
/>
|
||||
|
@ -17,6 +17,15 @@ export const hostnameValidator: Validator = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const domainValidator: Validator = (rule, value, callback) => {
|
||||
try {
|
||||
z.string().regex(hostnameRegex).parse(value);
|
||||
callback();
|
||||
} catch (err) {
|
||||
callback('Not valid, it should be domain, for example: example.com');
|
||||
}
|
||||
};
|
||||
|
||||
export const urlSlugValidator: Validator = (rule, value, callback) => {
|
||||
try {
|
||||
z.string().regex(slugRegex).parse(value);
|
||||
|
@ -19,6 +19,7 @@ import { logger } from './utils/logger';
|
||||
import { monitorRouter } from './router/monitor';
|
||||
import { healthRouter } from './router/health';
|
||||
import path from 'path';
|
||||
import { monitorPageManager } from './model/monitor/page/manager';
|
||||
|
||||
const app = express();
|
||||
|
||||
@ -27,7 +28,6 @@ app.use(express.json());
|
||||
app.use(passport.initialize());
|
||||
app.use(morgan('tiny'));
|
||||
app.use(cors());
|
||||
app.use(express.static('public'));
|
||||
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by');
|
||||
@ -60,6 +60,26 @@ if (env.allowOpenapi) {
|
||||
app.use('/open', trpcOpenapiHttpHandler);
|
||||
}
|
||||
|
||||
// Custom Status Page
|
||||
app.use('/*', (req, res, next) => {
|
||||
if (req.baseUrl === '/' || req.baseUrl === '') {
|
||||
const customDomain = monitorPageManager.findPageDomain(req.hostname);
|
||||
if (customDomain) {
|
||||
res
|
||||
.status(200)
|
||||
.send(
|
||||
`<body style="padding: 0; margin: 0;"><iframe style="border:none; width: 100%; height: 100%;" title="" src="/status/${customDomain.slug}" /></body>`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Static
|
||||
app.use(express.static('public'));
|
||||
|
||||
// fallback
|
||||
app.use('/*', (req, res) => {
|
||||
if (req.method === 'GET' && req.accepts('html')) {
|
||||
|
84
src/server/model/monitor/page/manager.ts
Normal file
84
src/server/model/monitor/page/manager.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { prisma } from '../../_client';
|
||||
|
||||
class MonitorPageManager {
|
||||
private customDomainPage: Record<
|
||||
string,
|
||||
{
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
slug: string;
|
||||
}
|
||||
> = {}; // key: domain
|
||||
|
||||
constructor() {
|
||||
prisma.monitorStatusPage
|
||||
.findMany({
|
||||
where: {
|
||||
domain: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
domain: true,
|
||||
workspaceId: true,
|
||||
slug: true,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
res.forEach((item) => {
|
||||
if (!item.domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.customDomainPage[item.domain] = {
|
||||
pageId: item.id,
|
||||
workspaceId: item.workspaceId,
|
||||
slug: item.slug,
|
||||
};
|
||||
});
|
||||
|
||||
logger.info(`Loaded ${res.length} custom domain for status page`);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Cannot load monitor page domain list:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* check domain existed
|
||||
* if domain not been used, return true
|
||||
*/
|
||||
async checkDomain(domain: string) {
|
||||
const res = await prisma.monitorStatusPage.findFirst({
|
||||
where: {
|
||||
domain,
|
||||
},
|
||||
});
|
||||
|
||||
return !res;
|
||||
}
|
||||
|
||||
async updatePageDomain(
|
||||
domain: string,
|
||||
info: {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
slug: string;
|
||||
}
|
||||
) {
|
||||
this.customDomainPage[domain] = info;
|
||||
logger.info(`Update page domain: ${domain} to page ${info.pageId}`);
|
||||
}
|
||||
|
||||
findPageDomain(domain: string) {
|
||||
if (!this.customDomainPage[domain]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.customDomainPage[domain];
|
||||
}
|
||||
}
|
||||
|
||||
export const monitorPageManager = new MonitorPageManager();
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "MonitorStatusPage" ADD COLUMN "domain" TEXT;
|
@ -355,6 +355,7 @@ model MonitorStatusPage {
|
||||
/// [MonitorStatusPageList]
|
||||
/// @zod.custom(imports.MonitorStatusPageListSchema)
|
||||
monitorList Json @default("[]") // monitor list
|
||||
domain String? // custom domain which can add cname record
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||
|
||||
|
@ -18,6 +18,7 @@ export const MonitorStatusPageModelSchema = z.object({
|
||||
* [MonitorStatusPageList]
|
||||
*/
|
||||
monitorList: imports.MonitorStatusPageListSchema,
|
||||
domain: z.string().nullish(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
MonitorInfoWithNotificationIds,
|
||||
MonitorPublicInfoSchema,
|
||||
} from '../../model/_schema/monitor';
|
||||
import { monitorPageManager } from '../../model/monitor/page/manager';
|
||||
|
||||
export const monitorRouter = router({
|
||||
all: workspaceProcedure
|
||||
@ -599,12 +600,14 @@ export const monitorRouter = router({
|
||||
MonitorStatusPageModelSchema.pick({
|
||||
description: true,
|
||||
monitorList: true,
|
||||
domain: true,
|
||||
}).partial()
|
||||
)
|
||||
)
|
||||
.output(MonitorStatusPageModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { workspaceId, slug, title, description, monitorList } = input;
|
||||
const { workspaceId, slug, title, description, monitorList, domain } =
|
||||
input;
|
||||
|
||||
const existSlugCount = await prisma.monitorStatusPage.count({
|
||||
where: {
|
||||
@ -616,17 +619,30 @@ export const monitorRouter = router({
|
||||
throw new Error('This slug has been existed');
|
||||
}
|
||||
|
||||
const res = await prisma.monitorStatusPage.create({
|
||||
if (domain && !(await monitorPageManager.checkDomain(domain))) {
|
||||
throw new Error('This domain has been used');
|
||||
}
|
||||
|
||||
const page = await prisma.monitorStatusPage.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
slug,
|
||||
title,
|
||||
description,
|
||||
monitorList,
|
||||
domain: domain || null, // make sure not ''
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
if (page.domain) {
|
||||
monitorPageManager.updatePageDomain(page.domain, {
|
||||
workspaceId: page.workspaceId,
|
||||
pageId: page.id,
|
||||
slug: page.slug,
|
||||
});
|
||||
}
|
||||
|
||||
return page;
|
||||
}),
|
||||
editPage: workspaceOwnerProcedure
|
||||
.meta(
|
||||
@ -644,12 +660,14 @@ export const monitorRouter = router({
|
||||
title: true,
|
||||
description: true,
|
||||
monitorList: true,
|
||||
domain: true,
|
||||
}).partial()
|
||||
)
|
||||
)
|
||||
.output(MonitorStatusPageModelSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, workspaceId, slug, title, description, monitorList } = input;
|
||||
const { id, workspaceId, slug, title, description, monitorList, domain } =
|
||||
input;
|
||||
|
||||
if (slug) {
|
||||
const existSlugCount = await prisma.monitorStatusPage.count({
|
||||
@ -666,7 +684,11 @@ export const monitorRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.monitorStatusPage.update({
|
||||
if (domain && !(await monitorPageManager.checkDomain(domain))) {
|
||||
throw new Error('This domain has been used');
|
||||
}
|
||||
|
||||
const page = await prisma.monitorStatusPage.update({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
@ -676,8 +698,19 @@ export const monitorRouter = router({
|
||||
title,
|
||||
description,
|
||||
monitorList,
|
||||
domain: domain || null,
|
||||
},
|
||||
});
|
||||
|
||||
if (page.domain) {
|
||||
monitorPageManager.updatePageDomain(page.domain, {
|
||||
workspaceId: page.workspaceId,
|
||||
pageId: page.id,
|
||||
slug: page.slug,
|
||||
});
|
||||
}
|
||||
|
||||
return page;
|
||||
}),
|
||||
deletePage: workspaceOwnerProcedure
|
||||
.meta(
|
||||
|
@ -3,6 +3,7 @@ import { Server as HTTPServer } from 'http';
|
||||
import { jwtVerify } from '../middleware/auth';
|
||||
import { socketEventBus } from './shared';
|
||||
import { isCuid } from '../utils/common';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export function initSocketio(httpServer: HTTPServer) {
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
@ -30,7 +31,7 @@ export function initSocketio(httpServer: HTTPServer) {
|
||||
try {
|
||||
const user = jwtVerify(token);
|
||||
|
||||
console.log('[Socket] Authenticated via JWT:', user.username);
|
||||
logger.info('[Socket] Authenticated via JWT:', user.username);
|
||||
|
||||
socket.data.user = user;
|
||||
socket.data.token = token;
|
||||
|
Loading…
x
Reference in New Issue
Block a user