mirror of https://github.com/fantasticit/think.git
commit
2cce76efe9
|
@ -1,7 +1,12 @@
|
||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
# 该脚本只保留生产环境运行所需文件到统一目录
|
# 该脚本只保留生产环境运行所需文件到统一目录
|
||||||
|
if [ ! -f './config/prod.yaml' ]; then
|
||||||
|
echo "缺少 config/prod.yaml 文件,可参考 docker-prod-sample.yaml 进行配置"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# 构建
|
# 构建
|
||||||
|
pnpm fetch --prod
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
|
@ -71,7 +76,7 @@ cd ../../
|
||||||
# @see https://github.com/typicode/husky/issues/914#issuecomment-826768549
|
# @see https://github.com/typicode/husky/issues/914#issuecomment-826768549
|
||||||
cd ${outputDir}
|
cd ${outputDir}
|
||||||
npm set-script prepare ""
|
npm set-script prepare ""
|
||||||
pnpm install -r --prod
|
pnpm install -r --offline --prod
|
||||||
cd ../
|
cd ../
|
||||||
|
|
||||||
echo "${outputDir} 打包完成"
|
echo "${outputDir} 打包完成"
|
||||||
|
|
|
@ -29,9 +29,9 @@ server:
|
||||||
user: ''
|
user: ''
|
||||||
password: ''
|
password: ''
|
||||||
admin:
|
admin:
|
||||||
name: 'sytemadmin' # 注意修改
|
name: 'admin' # 注意修改
|
||||||
password: 'sytemadmin' # 注意修改
|
password: 'admin' # 注意修改
|
||||||
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
|
email: 'admin@think.com' # 注意修改为真实邮箱地址
|
||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
db:
|
db:
|
||||||
|
|
|
@ -29,9 +29,9 @@ server:
|
||||||
user: ''
|
user: ''
|
||||||
password: ''
|
password: ''
|
||||||
admin:
|
admin:
|
||||||
name: 'sytemadmin' # 注意修改
|
name: 'admin' # 注意修改
|
||||||
password: 'sytemadmin' # 注意修改
|
password: 'admin' # 注意修改
|
||||||
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
|
email: 'admin@think.com' # 注意修改为真实邮箱地址
|
||||||
|
|
||||||
# 数据库配置
|
# 数据库配置
|
||||||
db:
|
db:
|
||||||
|
|
|
@ -2,13 +2,7 @@
|
||||||
### Author:jonnyan404
|
### Author:jonnyan404
|
||||||
### date:2022年5月22日
|
### date:2022年5月22日
|
||||||
|
|
||||||
CONFIG_FILE='/app/config/prod.yaml'
|
|
||||||
|
|
||||||
if [ ! -f $CONFIG_FILE ]; then
|
|
||||||
cp -f /app/config/docker-prod-sample.yaml $CONFIG_FILE
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
pnpm run pm2
|
pnpm run pm2
|
||||||
|
pm2 startup
|
||||||
|
pm2 save
|
||||||
pm2 logs
|
pm2 logs
|
||||||
|
|
|
@ -76,7 +76,7 @@ pm2 save
|
||||||
|
|
||||||
### docker-compose
|
### docker-compose
|
||||||
|
|
||||||
也可以使用 docker-compose 进行项目部署。首先,根据需要修改 `docker-compose.yml` 中的数据库、Redis 相关用户名、密码等配置,然后,修改 `config/docker-prod-sample.yaml` 中对应的配置。
|
也可以使用 docker-compose 进行项目部署。首先,根据需要修改 `docker-compose.yml` 中的数据库、Redis 相关用户名、密码等配置,然后,从 `config/docker-prod-sample.yaml` 复制出 `config/prod.yaml` 并修改其中对应的配置。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 首次安装
|
# 首次安装
|
||||||
|
|
|
@ -54,6 +54,13 @@ server {
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /static/ {
|
||||||
|
gzip_static on;
|
||||||
|
expires max;
|
||||||
|
add_header Cache-Control public;
|
||||||
|
alias /apps/think/packages/server/static/;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const isEmail = (email) => {
|
||||||
|
return !!String(email)
|
||||||
|
.toLowerCase()
|
||||||
|
.match(
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
);
|
||||||
|
};
|
|
@ -28,7 +28,7 @@ const Page: NextPage = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SingleColumnLayout>
|
<SingleColumnLayout>
|
||||||
<Seo title="管理后台" />
|
<Seo title="管理后台" key={tab} />
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{user && user.isSystemAdmin ? (
|
{user && user.isSystemAdmin ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: calc(100% - 52px);
|
||||||
padding: 10vh 24px;
|
padding: 10vh 24px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -3,11 +3,13 @@ import { Author } from 'components/author';
|
||||||
import { LogoImage, LogoText } from 'components/logo';
|
import { LogoImage, LogoText } from 'components/logo';
|
||||||
import { Seo } from 'components/seo';
|
import { Seo } from 'components/seo';
|
||||||
import { useRegister, useVerifyCode } from 'data/user';
|
import { useRegister, useVerifyCode } from 'data/user';
|
||||||
|
import { isEmail } from 'helpers/validator';
|
||||||
import { useInterval } from 'hooks/use-interval';
|
import { useInterval } from 'hooks/use-interval';
|
||||||
import { useRouterQuery } from 'hooks/use-router-query';
|
import { useRouterQuery } from 'hooks/use-router-query';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
|
import { emit } from 'process';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
@ -25,7 +27,13 @@ const Page = () => {
|
||||||
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
|
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
|
||||||
|
|
||||||
const onFormChange = useCallback((formState) => {
|
const onFormChange = useCallback((formState) => {
|
||||||
setEmail(formState.values.email);
|
const email = formState.values.email;
|
||||||
|
|
||||||
|
if (isEmail(email)) {
|
||||||
|
setEmail(email);
|
||||||
|
} else {
|
||||||
|
setEmail(null);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { start, stop } = useInterval(() => {
|
const { start, stop } = useInterval(() => {
|
||||||
|
@ -89,6 +97,7 @@ const Page = () => {
|
||||||
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
|
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||||
用户注册
|
用户注册
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
noLabel
|
noLabel
|
||||||
field="name"
|
field="name"
|
||||||
|
@ -97,6 +106,7 @@ const Page = () => {
|
||||||
placeholder="输入账户名称"
|
placeholder="输入账户名称"
|
||||||
rules={[{ required: true, message: '请输入账户' }]}
|
rules={[{ required: true, message: '请输入账户' }]}
|
||||||
></Form.Input>
|
></Form.Input>
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
noLabel
|
noLabel
|
||||||
mode="password"
|
mode="password"
|
||||||
|
@ -106,15 +116,6 @@ const Page = () => {
|
||||||
placeholder="输入用户密码"
|
placeholder="输入用户密码"
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
></Form.Input>
|
></Form.Input>
|
||||||
<Form.Input
|
|
||||||
noLabel
|
|
||||||
mode="password"
|
|
||||||
field="confirmPassword"
|
|
||||||
label="密码"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder="确认用户密码"
|
|
||||||
rules={[{ required: true, message: '请再次输入密码' }]}
|
|
||||||
></Form.Input>
|
|
||||||
|
|
||||||
<Form.Input
|
<Form.Input
|
||||||
noLabel
|
noLabel
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户注册
|
* 用户注册
|
||||||
|
@ -15,11 +15,6 @@ export class RegisterUserDto {
|
||||||
@IsNotEmpty({ message: '用户密码不能为空' })
|
@IsNotEmpty({ message: '用户密码不能为空' })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@MinLength(5, { message: '用户二次确认密码至少5个字符' })
|
|
||||||
@IsString({ message: '用户二次确认密码类型错误(正确类型为:String)' })
|
|
||||||
@IsNotEmpty({ message: '用户二次确认密码不能为空' })
|
|
||||||
confirmPassword: string;
|
|
||||||
|
|
||||||
@IsEmail({ message: '请输入正确的邮箱地址' })
|
@IsEmail({ message: '请输入正确的邮箱地址' })
|
||||||
@IsString({ message: '用户邮箱类型错误(正确类型为:String)' })
|
@IsString({ message: '用户邮箱类型错误(正确类型为:String)' })
|
||||||
@IsNotEmpty({ message: '用户邮箱不能为空' })
|
@IsNotEmpty({ message: '用户邮箱不能为空' })
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
|
import { getShortId } from '@helpers/shortid.herlper';
|
||||||
import { DocumentStatus } from '@think/domains';
|
import { DocumentStatus } from '@think/domains';
|
||||||
import { Exclude } from 'class-transformer';
|
import { Exclude } from 'class-transformer';
|
||||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('document')
|
@Entity('document')
|
||||||
export class DocumentEntity {
|
export class DocumentEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@BeforeInsert()
|
||||||
|
getShortId() {
|
||||||
|
this.id = getShortId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', comment: '文档所属知识库 Id' })
|
@Column({ type: 'varchar', comment: '文档所属知识库 Id' })
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
|
import { getShortId } from '@helpers/shortid.herlper';
|
||||||
import { DEFAULT_WIKI_AVATAR } from '@think/constants';
|
import { DEFAULT_WIKI_AVATAR } from '@think/constants';
|
||||||
import { WikiStatus } from '@think/domains';
|
import { WikiStatus } from '@think/domains';
|
||||||
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('wiki')
|
@Entity('wiki')
|
||||||
export class WikiEntity {
|
export class WikiEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@BeforeInsert()
|
||||||
|
getShortId() {
|
||||||
|
this.id = getShortId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PrimaryColumn()
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 200, comment: '知识库名称' })
|
@Column({ type: 'varchar', length: 200, comment: '知识库名称' })
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { randomFillSync } from 'node:crypto';
|
||||||
|
|
||||||
|
const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
|
||||||
|
|
||||||
|
// It is best to make fewer, larger requests to the crypto module to
|
||||||
|
// avoid system call overhead. So, random numbers are generated in a
|
||||||
|
// pool. The pool is a Buffer that is larger than the initial random
|
||||||
|
// request size by this multiplier. The pool is enlarged if subsequent
|
||||||
|
// requests exceed the maximum buffer size.
|
||||||
|
const POOL_SIZE_MULTIPLIER = 128;
|
||||||
|
let pool, poolOffset;
|
||||||
|
|
||||||
|
const fillPool = (bytes) => {
|
||||||
|
if (!pool || pool.length < bytes) {
|
||||||
|
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
||||||
|
randomFillSync(pool);
|
||||||
|
poolOffset = 0;
|
||||||
|
} else if (poolOffset + bytes > pool.length) {
|
||||||
|
randomFillSync(pool);
|
||||||
|
poolOffset = 0;
|
||||||
|
}
|
||||||
|
poolOffset += bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nanoid = (size = 21) => {
|
||||||
|
// `-=` convert `size` to number to prevent `valueOf` abusing
|
||||||
|
fillPool((size -= 0));
|
||||||
|
let id = '';
|
||||||
|
// We are reading directly from the random pool to avoid creating new array
|
||||||
|
for (let i = poolOffset - size; i < poolOffset; i++) {
|
||||||
|
// It is incorrect to use bytes exceeding the alphabet size.
|
||||||
|
// The following mask reduces the random byte in the 0-255 value
|
||||||
|
// range to the 0-63 value range. Therefore, adding hacks, such
|
||||||
|
// as empty string fallback or magic numbers, is unneccessary because
|
||||||
|
// the bitmask trims bytes down to the alphabet size.
|
||||||
|
id += urlAlphabet[pool[i] & 63];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getShortId = () => {
|
||||||
|
return nanoid(12);
|
||||||
|
};
|
|
@ -125,10 +125,6 @@ export class UserService {
|
||||||
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.password !== user.confirmPassword) {
|
|
||||||
throw new HttpException('两次密码不一致,请重试', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.userRepo.findOne({ name: user.name })) {
|
if (await this.userRepo.findOne({ name: user.name })) {
|
||||||
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue