Merge pull request #111 from fantasticit/feat/admin

Feat/admin
pull/115/head
fantasticit 2022-06-29 00:21:22 +08:00 committed by GitHub
commit 2cce76efe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 111 additions and 50 deletions

View File

@ -1,7 +1,12 @@
#! /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 run build
@ -71,7 +76,7 @@ cd ../../
# @see https://github.com/typicode/husky/issues/914#issuecomment-826768549
cd ${outputDir}
npm set-script prepare ""
pnpm install -r --prod
pnpm install -r --offline --prod
cd ../
echo "${outputDir} 打包完成"

View File

@ -29,9 +29,9 @@ server:
user: ''
password: ''
admin:
name: 'sytemadmin' # 注意修改
password: 'sytemadmin' # 注意修改
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
name: 'admin' # 注意修改
password: 'admin' # 注意修改
email: 'admin@think.com' # 注意修改为真实邮箱地址
# 数据库配置
db:

View File

@ -29,9 +29,9 @@ server:
user: ''
password: ''
admin:
name: 'sytemadmin' # 注意修改
password: 'sytemadmin' # 注意修改
email: 'sytemadmin@think.com' # 注意修改为真实邮箱地址
name: 'admin' # 注意修改
password: 'admin' # 注意修改
email: 'admin@think.com' # 注意修改为真实邮箱地址
# 数据库配置
db:

View File

@ -2,13 +2,7 @@
### Author:jonnyan404
### 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
pm2 startup
pm2 save
pm2 logs

View File

@ -76,7 +76,7 @@ pm2 save
### 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
# 首次安装

View File

@ -54,6 +54,13 @@ server {
proxy_set_header Upgrade $http_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 {

View File

@ -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,}))$/
);
};

View File

@ -28,7 +28,7 @@ const Page: NextPage = () => {
return (
<SingleColumnLayout>
<Seo title="管理后台" />
<Seo title="管理后台" key={tab} />
<div className="container">
{user && user.isSystemAdmin ? (
<>

View File

@ -9,6 +9,7 @@
position: relative;
z-index: 10;
display: flex;
height: calc(100% - 52px);
padding: 10vh 24px;
flex: 1;
flex-direction: column;

View File

@ -3,11 +3,13 @@ import { Author } from 'components/author';
import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo';
import { useRegister, useVerifyCode } from 'data/user';
import { isEmail } from 'helpers/validator';
import { useInterval } from 'hooks/use-interval';
import { useRouterQuery } from 'hooks/use-router-query';
import { useToggle } from 'hooks/use-toggle';
import Link from 'next/link';
import Router from 'next/router';
import { emit } from 'process';
import React, { useCallback, useState } from 'react';
import styles from './index.module.scss';
@ -25,7 +27,13 @@ const Page = () => {
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
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(() => {
@ -89,6 +97,7 @@ const Page = () => {
<Title type="tertiary" heading={5} style={{ marginBottom: 16, textAlign: 'center' }}>
</Title>
<Form.Input
noLabel
field="name"
@ -97,6 +106,7 @@ const Page = () => {
placeholder="输入账户名称"
rules={[{ required: true, message: '请输入账户' }]}
></Form.Input>
<Form.Input
noLabel
mode="password"
@ -106,15 +116,6 @@ const Page = () => {
placeholder="输入用户密码"
rules={[{ required: true, message: '请输入密码' }]}
></Form.Input>
<Form.Input
noLabel
mode="password"
field="confirmPassword"
label="密码"
style={{ width: '100%' }}
placeholder="确认用户密码"
rules={[{ required: true, message: '请再次输入密码' }]}
></Form.Input>
<Form.Input
noLabel

View File

@ -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: '用户密码不能为空' })
password: string;
@MinLength(5, { message: '用户二次确认密码至少5个字符' })
@IsString({ message: '用户二次确认密码类型错误正确类型为String' })
@IsNotEmpty({ message: '用户二次确认密码不能为空' })
confirmPassword: string;
@IsEmail({ message: '请输入正确的邮箱地址' })
@IsString({ message: '用户邮箱类型错误正确类型为String' })
@IsNotEmpty({ message: '用户邮箱不能为空' })

View File

@ -1,10 +1,16 @@
import { getShortId } from '@helpers/shortid.herlper';
import { DocumentStatus } from '@think/domains';
import { Exclude } from 'class-transformer';
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
@Entity('document')
export class DocumentEntity {
@PrimaryGeneratedColumn('uuid')
@BeforeInsert()
getShortId() {
this.id = getShortId();
}
@PrimaryColumn()
public id: string;
@Column({ type: 'varchar', comment: '文档所属知识库 Id' })

View File

@ -1,10 +1,16 @@
import { getShortId } from '@helpers/shortid.herlper';
import { DEFAULT_WIKI_AVATAR } from '@think/constants';
import { WikiStatus } from '@think/domains';
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';
@Entity('wiki')
export class WikiEntity {
@PrimaryGeneratedColumn('uuid')
@BeforeInsert()
getShortId() {
this.id = getShortId();
}
@PrimaryColumn()
public id: string;
@Column({ type: 'varchar', length: 200, comment: '知识库名称' })

View File

@ -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);
};

View File

@ -125,10 +125,6 @@ export class UserService {
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 })) {
throw new HttpException('该账户已被注册', HttpStatus.BAD_REQUEST);
}