chore(admin-next): init tailchat-admin-next

include client and server
pull/90/head
moonrailgun 2 years ago
parent 2394456436
commit 6640397b7a

@ -0,0 +1,3 @@
Only have one tailchat instance version.
Not include openapi

@ -0,0 +1,81 @@
version: "3.3"
services:
tailchat:
build:
context: ../../
image: tailchat
restart: unless-stopped
env_file: ../../docker-compose.env
environment:
SERVICEDIR: plugins,services
PORT: 3000
OPENAPI_PORT: 3003
OPENAPI_UNDER_PROXY: "true"
depends_on:
- mongo
- redis
labels:
- "traefik.enable=true"
- "traefik.http.routers.api-gw.rule=PathPrefix(`/`)"
- "traefik.http.services.api-gw.loadbalancer.server.port=3000"
- "traefik.http.routers.openapi-oidc.rule=PathPrefix(`/open`)"
- "traefik.http.services.openapi-oidc.loadbalancer.server.port=3003"
networks:
- internal
# Database
mongo:
image: mongo:4
restart: on-failure
volumes:
- data:/data/db
networks:
- internal
# Data cache and Transporter
redis:
image: redis:alpine
restart: on-failure
networks:
- internal
# Persist Storage
minio:
image: minio/minio
restart: on-failure
networks:
- internal
environment:
MINIO_ROOT_USER: tailchat
MINIO_ROOT_PASSWORD: com.msgbyte.tailchat
volumes:
- storage:/data
command: minio server /data --console-address ":9001"
# Router
traefik:
image: traefik:v2.1
restart: unless-stopped
command:
- "--api.insecure=true" # Don't do that in production!
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.web.forwardedHeaders.insecure" # Not good
ports:
- 11000:80
- 127.0.0.1:11001:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- internal
- default
networks:
internal:
name: tailchat-internal
volumes:
data:
storage:

File diff suppressed because it is too large Load Diff

@ -7,7 +7,7 @@ packages:
- 'client/packages/**'
- 'server'
- 'server/admin'
- 'server/admin-next' # wait for delete
- 'server/admin-next'
- 'server/packages/**'
- 'server/plugins/**'
- 'server/test/demo/**'

@ -1,3 +0,0 @@
AdminJS.UserComponents = {};
import Component1 from '../src/dashboard';
AdminJS.UserComponents.Component1 = Component1;

@ -1,5 +0,0 @@
## tailchat-admin
**WIP**
tailchat的后台管理系统

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tushan</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client/main.tsx"></script>
</body>
</html>

@ -0,0 +1,7 @@
{
"verbose": true,
"watch": ["./src/server"],
"ext": "ts",
"delay": 1000,
"exec": "ts-node ./src/server/index.ts"
}

@ -1,38 +1,28 @@
{
"name": "tailchat-admin-next",
"private": true,
"version": "1.0.0",
"description": "",
"main": "index.js",
"version": "0.0.0",
"author": "moonrailgun",
"scripts": {
"start": "ts-node --transpile-only src/index.ts",
"dev": "nodemon --watch 'src/**' --ext 'ts' --ignore 'src/**/*.spec.ts' --exec \"ts-node --transpile-only src/index.ts\"",
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "nodemon",
"start": "NODE_ENV=production ts-node src/server/index.ts",
"build": "vite build"
},
"keywords": [],
"author": "moonrailgun",
"license": "MIT",
"dependencies": {
"@adminjs/koa": "^2.1.0",
"@adminjs/mongoose": "^2.0.4",
"@koa/router": "^10.1.1",
"adminjs": "^5.10.4",
"koa": "^2.13.4",
"koa-session": "^6.2.0",
"koa-static": "^5.0.0",
"koa2-formidable": "^1.0.3",
"react-use": "^17.4.0",
"recharts": "^2.1.12",
"tushan": "^0.0.1"
"express": "^4.18.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tushan": "^0.2.1",
"vite-express": "^0.5.4"
},
"devDependencies": {
"@types/koa": "^2.13.4",
"@types/koa-session": "^5.10.6",
"@types/koa-static": "^4.0.2",
"@types/node": "16.11.7",
"@types/react": "^17.0.38",
"nodemon": "^2.0.18",
"ts-node": "^10.8.0",
"typescript": "^4.3.3"
"@types/express": "^4.17.15",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1",
"typescript": "^4.9.3",
"vite": "^4.2.0"
}
}

@ -0,0 +1,10 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 88L49.5 57L118.5 29.5L248 51L323.5 122.5L360.5 324L301 421.5L164.5 412.5L118.5 324L127.5 225.5L143.5 184.5L151.5 130.5L127.5 95L82.5 80L49.5 95L28 88Z" fill="#DFDFDF"/>
<path d="M144.734 22.04C139.186 22.0047 133.638 22.1568 128.1 22.496C84.33 25.196 40.5 49 24.238 67.492C7.97598 85.984 4 91.601 4 91.601C4 91.601 34.922 98.392 57 97.5C79.078 96.608 111.355 88.82 127.692 104.564C144.032 120.309 151.428 146.017 135.232 175.709C116.062 210.852 102.516 271.862 115.086 332.235C127.656 392.609 168.054 451.995 254.814 478.007C288.29 488.043 333.639 494.757 376.459 485.673C420.966 476.885 472.309 450.915 483.351 422.563C474.101 431.448 463.911 437.703 453.149 442.353C471.455 421.433 484.884 392.621 489.939 354.179L492.469 334.939L476.147 345.435C465.644 352.19 455.562 358.838 446.054 363.831C448.692 357.959 451.092 350.611 453.784 341.054C442.687 356.244 430.054 366.409 415.186 372.526C405.952 372.023 396.833 367.659 385.976 356.429C374.618 344.682 367.856 324.334 363.513 298.763C359.169 273.191 357.053 242.836 352.845 211.886C344.425 149.984 326.933 84.013 263.105 50.851C226.15 31.651 184.013 22.274 144.733 22.038L144.734 22.04ZM144.611 40.05C181.073 40.305 220.721 49.115 254.808 66.824C311.201 96.124 326.802 153.964 335.011 214.312C339.115 244.487 341.197 274.866 345.769 301.777C347.085 309.53 348.604 317.019 350.462 324.162C335.014 324.202 323.208 315.855 308.758 299.445C316.143 329.855 320.748 335.979 334.463 354.995C306.243 346.76 273.823 320.255 253.513 290.932C250.239 330.979 273.736 362.506 286.788 374.862C261.612 360.666 226.075 333.326 202.165 286.207C201.149 327.633 214.095 373.939 238.615 402.672C204.1 391.136 173.645 303.2 153.195 275.039C140.155 308.256 150.247 364.124 169.267 405.161C149.639 382.323 138.38 355.786 132.712 328.565C121.188 273.223 134.462 214.718 151.037 184.327C170.587 148.485 161.952 112.577 140.187 91.601C118.419 70.625 66 81 53.633 83.286C41.266 85.572 31 83.286 31 83.286C31 83.286 41.3371 75.1684 48 70C74.6656 49.3155 88.786 42.954 129.211 40.461C134.263 40.149 139.406 40.011 144.614 40.047L144.611 40.05Z" fill="url(#paint0_linear_1104_3)"/>
<defs>
<linearGradient id="paint0_linear_1104_3" x1="384.5" y1="480" x2="256" y2="256" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF6011"/>
<stop offset="1" stop-color="#FF9411"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,50 @@
import {
createTextField,
jsonServerProvider,
ListTable,
Resource,
Tushan,
} from 'tushan';
import { authProvider } from './auth';
import { photoFields, userFields } from './fields';
const dataProvider = jsonServerProvider('https://jsonplaceholder.typicode.com');
function App() {
return (
<Tushan
basename="/admin"
dataProvider={dataProvider}
authProvider={authProvider}
>
<Resource
name="users"
label="User"
list={
<ListTable
filter={[
createTextField('q', {
label: 'Query',
}),
]}
fields={userFields}
action={{ create: true, detail: true, edit: true, delete: true }}
/>
}
/>
<Resource
name="photos"
label="Photos"
list={
<ListTable
fields={photoFields}
action={{ detail: true, edit: true, delete: true }}
/>
}
/>
</Tushan>
);
}
export default App;

@ -0,0 +1,33 @@
import { AuthProvider } from 'tushan';
export const authProvider: AuthProvider = {
login: ({ username, password }) => {
if (username !== 'tushan' || password !== 'tushan') {
return Promise.reject();
}
localStorage.setItem('username', username);
return Promise.resolve();
},
logout: () => {
localStorage.removeItem('username');
return Promise.resolve();
},
checkAuth: () =>
localStorage.getItem('username') ? Promise.resolve() : Promise.reject(),
checkError: (error) => {
const status = error.status;
if (status === 401 || status === 403) {
localStorage.removeItem('username');
return Promise.reject();
}
return Promise.resolve();
},
getIdentity: () =>
Promise.resolve({
id: '0',
fullName: 'Admin',
}),
getPermissions: () => Promise.resolve(''),
};

@ -0,0 +1,43 @@
import {
createAvatarField,
createEmailField,
createImageField,
createTextField,
createUrlField,
} from 'tushan';
export const userFields = [
createTextField('id', {
label: 'ID',
}),
createTextField('name', {
label: 'Name',
list: {
sort: true,
},
}),
createEmailField('email', {
label: 'Email',
}),
createUrlField('website', {
label: 'Website',
}),
];
export const photoFields = [
createTextField('id', {
label: 'ID',
}),
createTextField('albumId', {
label: 'AlbumId',
}),
createTextField('title', {
label: 'Title',
}),
createImageField('url', {
label: 'Url',
}),
createAvatarField('thumbnailUrl', {
label: 'ThumbnailUrl',
}),
];

@ -0,0 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />
);

@ -0,0 +1 @@
/// <reference types="vite/client" />

@ -1,66 +0,0 @@
import { buildRouter, Tushan } from 'tushan';
import { User } from './resources/User';
import Koa from 'koa';
import path from 'path';
import session from 'koa-session';
import dotenv from 'dotenv';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const mongoUri = process.env.MONGO_URL;
const adminPass = process.env.ADMIN_PASS;
async function start() {
if (!mongoUri) {
console.warn(`MONGO_URL has not been set.`);
return;
}
if (!adminPass) {
console.warn(`ADMIN_PASS has not been set.`);
return;
}
const app = new Koa();
app.keys = ['tushan'];
app.use(session({ key: 'sess:tushan' }, app));
const tushan = new Tushan({
datasourceOptions: {
type: 'mongodb',
url: mongoUri,
} as any,
resources: [
{
entity: User,
options: {
label: '用户',
},
},
],
pages: [],
});
await tushan.initialize();
const router = await buildRouter({
tushan,
auth: {
local: async (username, password) => {
if (username === 'tailchat' && password === adminPass) {
return { username, title: 'Admin' };
}
throw new Error(`User not found: ${username}`);
},
},
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(6789, () => {
console.log('服务器已启动:', `http://localhost:6789/admin/`);
});
}
start();

@ -1,3 +0,0 @@
import { ApiClient } from 'adminjs';
export const api = new ApiClient();

@ -1,32 +0,0 @@
import React from 'react';
import { api } from './api';
import { useAsync } from 'react-use';
/**
* WIP
*/
const Dashboard = (props) => {
useAsync(async () => {
try {
const all = await api
.resourceAction({ resourceId: 'User', actionName: 'list' })
.then(({ data }) => data.meta.total);
const temporary = await api
.searchRecords({
resourceId: 'User',
// actionName: 'list',
query: '?filters.temporary=true&page=1',
})
.then((list) => list.length);
console.log('用户情况:', all, temporary);
} catch (err) {
console.error(err);
}
}, []);
return <div>My custom dashboard</div>;
};
export default Dashboard;

@ -1,67 +0,0 @@
import AdminJS from 'adminjs';
import { buildAuthenticatedRouter } from '@adminjs/koa';
import AdminJSMongoose from '@adminjs/mongoose';
import Koa from 'koa';
import { mongoose } from '@typegoose/typegoose';
import dotenv from 'dotenv';
import path from 'path';
import { getResources } from './resources';
import serve from 'koa-static';
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const mongoUri = process.env.MONGO_URL;
const adminPass = process.env.ADMIN_PASS;
AdminJS.registerAdapter(AdminJSMongoose);
async function run() {
if (!mongoUri) {
console.warn(`MONGO_URL has not been set.`);
return;
}
if (!adminPass) {
console.warn(`ADMIN_PASS has not been set.`);
return;
}
const app = new Koa();
app.keys = ['tailchat-admin-secret'];
const mongooseDb = await mongoose.connect(mongoUri);
const adminJs = new AdminJS({
branding: {
companyName: 'tailchat',
logo: '/images/logo.svg',
favicon: '/images/logo.svg',
softwareBrothers: false,
},
databases: [mongooseDb],
rootPath: '/admin',
resources: getResources(),
// dashboard: {
// component: AdminJS.bundle('./dashboard'),
// },
});
const router = buildAuthenticatedRouter(adminJs, app, {
authenticate: async (email, password) => {
if (email === 'tailchat@msgbyte.com' && password === adminPass) {
return { email, title: 'Admin' };
}
return null;
},
});
app.use(router.routes()).use(router.allowedMethods());
app.use(serve(path.resolve(__dirname, '../static')));
app.listen(14100, () => {
console.log('AdminJS is under http://localhost:14100/admin');
console.log(`please login with: tailchat@msgbyte.com/${adminPass}`);
});
}
run();

@ -1,42 +0,0 @@
import type { ResourceWithOptions } from 'adminjs';
import User from '../../models/user/user';
import Group from '../../models/group/group';
import Message from '../../models/chat/message';
import File from '../../models/file';
export function getResources() {
return [
{
resource: User,
options: {
properties: {
email: {
isDisabled: true,
},
username: {
isVisible: false,
},
password: {
isVisible: false,
},
},
sort: {
direction: 'desc',
sortBy: 'createdAt',
},
},
} as ResourceWithOptions,
Group,
{
resource: Message,
options: {
properties: {
content: {
type: 'textarea',
},
},
},
} as ResourceWithOptions,
File,
];
}

@ -1,43 +0,0 @@
import { Entity, ObjectIdColumn, Column, PrimaryColumn } from 'tushan';
@Entity({
name: 'users',
})
export class User {
@ObjectIdColumn()
_id!: string;
@Column()
username!: string;
@Column()
email!: string;
@Column()
password!: string;
@Column()
nickname!: string;
@Column()
discriminator!: string;
@Column({
default: false,
})
temporary!: boolean;
@Column()
avatar!: boolean;
@Column({
enum: ['normalUser', 'pluginBot', 'openapiBot'],
default: 'normalUser',
})
type: string;
@Column({
default: {},
})
settings: string;
}

@ -0,0 +1,16 @@
import express from 'express';
import ViteExpress from 'vite-express';
const app = express();
const port = Number(process.env.PORT || 13000);
app.get('/hello', (_, res) => {
res.send('Hello Vite + React + TypeScript!');
});
ViteExpress.listen(app, port, () =>
console.log(
`Server is listening on port ${port}, visit with: http://localhost:${port}`
)
);

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

@ -1,7 +1,20 @@
{
"extends": "../tsconfig.json",
"ts-node": {
"skipIgnore": true
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"jsx": "react-jsx",
"forceConsistentCasingInFileNames": true,
"module": "CommonJS",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["./**/*"]
"include": ["src"]
}

@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});

@ -53,9 +53,9 @@ class LinkmetaService extends TcService {
// 转存图片
if (Array.isArray(data.images) && data.images.length > 0) {
try {
const { url } = await ctx.call('file.saveFileWithUrl', {
const { url } = (await ctx.call('file.saveFileWithUrl', {
fileUrl: data.images[0],
});
})) as { url: string };
data.images[0] = url;
} catch (e) {}
}

Loading…
Cancel
Save