feat: login

This commit is contained in:
mozzie 2023-08-28 17:00:35 +08:00
parent 2822f338dc
commit 063e812448
26 changed files with 7534 additions and 2878 deletions

View File

@ -10,7 +10,7 @@ export const AuthFailedReplacePath = "/login";
export const theme: ThemeConfig = {
token: {
colorPrimary: "#fa541c",
borderRadius: 2,
borderRadius: 5,
},
};

View File

@ -1,5 +1,7 @@
import React, { useRef, useState } from "react";
import { Study, parseDcmFiles } from "./util";
import { Button } from "antd";
import { CloudUploadOutlined } from "@ant-design/icons";
declare module "react" {
interface InputHTMLAttributes<T> extends HTMLAttributes<T> {
@ -44,17 +46,28 @@ export const useDicomUploader = () => {
setIsLoading(false);
};
const UploadInput = () => (
<input
type="file"
webkitdirectory="true"
ref={fileInputRef}
mozdirectory="true"
multiple
placeholder="选择文件"
onChange={(e) => handleFileChange(e)}
/>
);
const UploadInput = () => {
return (
<>
<input
type="file"
webkitdirectory="true"
ref={fileInputRef}
mozdirectory="true"
multiple
style={{ display: "none" }}
onChange={(e) => handleFileChange(e)}
/>
<Button
icon={<CloudUploadOutlined />}
type="primary"
onClick={() => fileInputRef.current?.click()}
>
dicom
</Button>
</>
);
};
return { UploadInput, fileCalculator, studys, isLoading };
};

View File

@ -1,4 +1,4 @@
import { Button, Col, Row, Space, Spin } from "antd";
import { Button, Col, Divider, Modal, Row, Select, Space, Spin } from "antd";
import { useDicomUploader } from "./DicomUploader";
import { Series, Study } from "./DicomUploader/util";
import { DicomTable } from "./DicomTable";
@ -12,11 +12,27 @@ interface DicomUploadProps {
children?: JSX.Element;
}
/**
* ohif阅片
*/
const openOHIFViewer = (
StudyInstanceUID: string,
SeriesInstanceUID: string
) => {
const target = `http://localhost:3000/viewer/${StudyInstanceUID}?SeriesInstanceUID=${SeriesInstanceUID}`;
window.open(target, "_blank");
};
export const DicomUpload = (props: DicomUploadProps) => {
const { UploadInput, fileCalculator, studys, isLoading } = useDicomUploader();
const { dicomDomainService } = useDomain();
const { dicomDomainService, userDomainService } = useDomain();
const { dcmFileNum, totalFileNum, dcmFileSize } = fileCalculator;
const [selectRows, setSelectedRows] = useState<Study[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [annotators, setAnnotators] = useState([]);
const [selectAnnotatorId, setSelectAnnotatorId] = useState<string | number>(
""
);
const onUploadFiles = async (study: Study, series: Series) => {
const { SeriesInstanceUID, subs } = series;
@ -25,10 +41,9 @@ export const DicomUpload = (props: DicomUploadProps) => {
SeriesInstanceUID,
StudyInstanceUID,
});
const dcmExistInPacs = instances.length === subs.length;
if (dcmExistInPacs) {
const target = `http://localhost:3000/viewer/${StudyInstanceUID}?SeriesInstanceUID=${SeriesInstanceUID}`;
window.open(target, "_blank");
// pacs已存在
if (instances.length === subs.length) {
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
} else {
let fullfilled = 0;
Promise.all(
@ -41,49 +56,79 @@ export const DicomUpload = (props: DicomUploadProps) => {
)
).then((res) => {
if (res.length === series.subs.length) {
const target = `http://localhost:3000/viewer/${StudyInstanceUID}?SeriesInstanceUID=${SeriesInstanceUID}`;
window.open(target, "_blank");
openOHIFViewer(StudyInstanceUID, SeriesInstanceUID);
}
});
}
};
const onClickAssign = () => {
userDomainService.getDmpAnnotators().then((res) => {
console.log(res);
const { data } = res;
setIsModalOpen(true);
setAnnotators(data.map((u) => ({ label: u.username, value: u.id })));
});
console.log(selectRows);
};
const dcmFileInfo = !!totalFileNum && (
<Space>
<Text type="secondary">: {totalFileNum}</Text>
<Divider type="vertical" />
<Text type="secondary">: {dcmFileNum} dicom文件</Text>
<Divider type="vertical" />
<Text type="secondary">dcm文件总体积: {dcmFileSize} </Text>
</Space>
);
const onAssignConfirm = () => {
if (!selectAnnotatorId) return;
console.log(selectAnnotatorId);
setSelectAnnotatorId(0);
setIsModalOpen(false);
};
return (
<div>
<header>
<Row style={{ padding: "10px 0" }}>
<Col span={16}>
<div style={{ padding: 20 }}>
<Row style={{ paddingBottom: 20 }}>
<Col span={24}>
<Space>
<UploadInput />
<Text type="secondary">
, dicom文件: {dcmFileNum}, : {totalFileNum}, :
{dcmFileSize}
</Text>
</Col>
<Col span={8} style={{ textAlign: "right" }}>
<Space>
<Button type="primary" size="small" onClick={onClickAssign}>
</Button>
<Button type="primary" size="small">
</Button>
<Button type="primary" size="small">
</Button>
</Space>
</Col>
</Row>
</header>
<Button
disabled={selectRows.length === 0}
type="primary"
onClick={onClickAssign}
>
</Button>
</Space>
</Col>
</Row>
{dcmFileInfo}
<DicomTable
studys={studys}
loading={isLoading}
onSelectedRows={(rows) => setSelectedRows(rows)}
onUploadFiles={onUploadFiles}
/>
<Modal
width={320}
title="选择标注"
open={isModalOpen}
closable
cancelText="再想想"
okText="确定"
onOk={onAssignConfirm}
onCancel={() => setIsModalOpen(false)}
>
<Select
style={{ width: "100%", marginBottom: 20 }}
defaultValue={selectAnnotatorId}
onChange={(e) => setSelectAnnotatorId(e)}
options={annotators}
/>
</Modal>
</div>
);
};

View File

@ -0,0 +1,8 @@
PORT=31222
# nacos中注册服务的名称
NACOS_SERVICE_NAME=dicom
NACOS_ADDR=127.0.0.1:8848
NACOS_NAMESPACE=56a3b295-f319-4ced-82b5-0df2e98cc541
# nacos配置中心
NACOS_DATAID='test'
NACOS_GROUP='DEFAULT_GROUP'

View File

View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

35
apps/services/dicom/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ pnpm install
```
## Running the app
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Test
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View File

@ -0,0 +1,56 @@
{
"name": "@tavi/dicom",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "cross-env NODE_ENV=dev nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=dev node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"dependencies": {
"@nestjs/common": "10.1.0",
"@nestjs/core": "10.1.0",
"@nestjs/platform-express": "10.1.0",
"reflect-metadata": "^0.1.13",
"@nestjs/config": "3.0.0",
"rxjs": "7.8.1",
"nats": "2.15.1",
"@nestjs/microservices": "10.0.5",
"nacos": "2.5.1",
"cross-env": "7.0.3",
"cookie-parser": "1.4.6",
"@casl/ability": "6.5.0",
"typeorm": "0.3.17",
"@nestjs/typeorm": "10.0.0",
"bcrypt": "5.1.0",
"minimatch": "9.0.3",
"dayjs": "1.11.9",
"flatted": "3.2.7",
"crypto-js": "4.1.1",
"@tavi/util": "workspace:*"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@types/express": "^4.17.13",
"@types/node": "18.16.12",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.2.0",
"typescript": "^5.0.0"
}
}

View File

@ -0,0 +1,27 @@
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { EventPattern } from '@nestjs/microservices';
import * as dayjs from 'dayjs';
import { SymmetricCrypto } from '@tavi/util';
interface UserSignLoggerDto {
platform: string;
username: string;
finger: string;
finger2: string;
isLegal: boolean;
}
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@EventPattern({ cmd: 'logger.user.signIn' })
async userSignIn(payload: UserSignLoggerDto) {
const dateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
const { finger2, ...rest } = payload;
const browserInfo = new SymmetricCrypto().decrypt(finger2);
console.log({ ...rest, dateTime, browserInfo });
return 1;
}
}

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { NacosModule } from './nacos/nacos.module';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.${process.env.NODE_ENV}`,
}),
NacosModule,
],
controllers: [AppController],
providers: [AppService],
exports: [],
})
export class AppModule {}

View File

@ -0,0 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class AppService {}

View File

@ -0,0 +1,21 @@
import * as CryptoJS from 'crypto-js';
import { parse } from 'flatted';
export class SymmetricCrypto {
private key: string;
constructor(key: string) {
this.key = key;
}
decrypt(encryptedData: string): object | null {
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, this.key);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);
return parse(decryptedData);
} catch (error) {
console.error('Decryption failed:', error);
return null;
}
}
}

View File

@ -0,0 +1,19 @@
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.NATS,
options: {
servers: ['nats://localhost:4222'], // 可以指定链接到多个nats的消息队列
maxReconnectAttempts: 5,
reconnectTimeWait: 1000,
},
},
);
await app.listen();
}
bootstrap();

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { NacosService } from './nacos.service';
@Module({
providers: [NacosService],
exports: [NacosService],
})
export class NacosModule {}

View File

@ -0,0 +1,108 @@
// nacos.service.ts
import {
Injectable,
OnApplicationBootstrap,
OnApplicationShutdown,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosConfigClient, NacosNamingClient } from 'nacos'; // ts
import * as os from 'os';
@Injectable()
export class NacosService
implements OnApplicationBootstrap, OnApplicationShutdown
{
private nacosNamingClient: NacosNamingClient;
private nacosConfigClient: NacosConfigClient;
serviceName: string;
instance: { ip: string; port: number };
group: string;
dataId: string;
constructor(private configService: ConfigService) {
this.nacosNamingClient = new NacosNamingClient({
logger: console,
serverList: configService.get('NACOS_ADDR'),
namespace: configService.get('NACOS_NAMESPACE'),
});
this.nacosConfigClient = new NacosConfigClient({
namespace: configService.get('NACOS_NAMESPACE'),
serverAddr: configService.get('NACOS_ADDR'),
});
this.serviceName = configService.get('NACOS_SERVICE_NAME');
this.dataId = configService.get('NACOS_DATAID');
this.group = configService.get('NACOS_GROUP');
this.instance = {
ip: this.getServerIP(),
port: configService.get('PORT'),
};
}
/**
* nestjs应用被关闭前
* @param {string} signal 'SIGTERM' | 'SIGINT' | 'SIGHUP' | 'SIGBREAK'
*/
onApplicationShutdown(signal?: string) {
if (signal) {
const { serviceName, instance, group } = this;
this.nacosNamingClient.deregisterInstance(serviceName, instance, group);
this.nacosConfigClient.close();
}
}
/**
* &
*/
onApplicationBootstrap() {
const { serviceName, instance } = this;
this.nacosNamingClient.registerInstance(serviceName, instance);
}
/**
* onApplicationBootstrap
*/
async onModuleInit() {
this.nacosNamingClient.ready();
}
/**
* nacos获取最新的配置信息
*/
async getConfig() {
const { dataId, group } = this;
const configFromNacos = await this.nacosConfigClient.getConfig(
dataId,
group,
);
return configFromNacos;
}
/**
* nacos的配置时
*/
async subscribeConfiguration() {
const { dataId, group } = this;
this.nacosConfigClient.subscribe(
{
dataId,
group,
},
(content) => console.log('content', content),
);
}
getServerIP(): string {
const networkInterfaces = os.networkInterfaces();
for (const name of Object.keys(networkInterfaces)) {
for (const iface of networkInterfaces[name]) {
// 跳过IPv6和内部地址
if ('IPv4' !== iface.family || iface.internal !== false) {
continue;
}
// 返回第一个找到的IPv4地址
return iface.address;
}
}
return 'localhost'; // 如果找不到外部IPv4地址返回localhost
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

View File

@ -1,6 +1,5 @@
version: '3.1'
version: "3.1"
services:
nats:
image: nats
container_name: tavi-nats
@ -46,14 +45,13 @@ services:
ports:
- "5432:5432"
volumes:
# postgrel->data 挂载出来
# postgrel->data 挂载出来如果出现docker报错清空 page_data/data以及/orthanc_db文件夹
- ./orthancOHIF/pg_data/data:/var/lib/postgresql/data
- ./orthancOHIF/orthanc.sql:/docker-entrypoint-initdb.d/orthanc.sql
environment:
- PGDATA=/var/lib/postgresql/data
- POSTGRES_DB=tavi-orthanc
- POSTGRES_USER=orthanc
- POSTGRES_PASSWORD=orthanc
- TZ=Asia/Shanghai
- POSTGRES_DB=postgres
pacs:
image: osimis/orthanc:20.11.2

1
orthancOHIF/orthanc.sql Normal file
View File

@ -0,0 +1 @@
CREATE DATABASE orthanc;

View File

@ -4,6 +4,7 @@
"scripts": {
"dev:all": "chmod 777 ./terminal.sh && ./terminal.sh",
"mock": "pnpm run --filter @tavi/mock mock",
"dev:dicom": "pnpm run --filter @tavi/dicom start:dev",
"dev:logger": "pnpm run --filter @tavi/logger start:dev",
"dev:dmp-web": "pnpm run --filter @tavi/dmp-web dev",
"dev:dmp-gateway": "pnpm run --filter @tavi/dmp-gateway start:dev",

File diff suppressed because it is too large Load Diff

23
structures.md Normal file
View File

@ -0,0 +1,23 @@
# dicom 微服务
## 脱敏
针对PaitentName、扫描日期
假脱敏:
真脱敏针对下载影像请求pacs洗掉patient信息压缩zip
## 上传影像
关联业务网关:
- dmp-gateway
- 管理员上传dicom前端吐pacs数据表记录病人信息脱敏病人姓名
- 标注上传标注文件:……
- aorta等业务系统
- 用户上传dicom前端吐pacs过dicom微服务假脱敏
## 下载影像
- dmp-gateway: 标注、算法下载dicom脱敏