1
0
mirror of https://gitee.com/bitdance-team/chrome-extension synced 2025-10-07 16:35:15 +08:00
Code Issues Projects Releases Wiki Activity GitHub Gitee

feat: services

This commit is contained in:
kaz
2022-02-09 11:48:23 +08:00
parent 6b0e0efcb3
commit 18ca56db10
49 changed files with 11218 additions and 5626 deletions

View File

@@ -0,0 +1,55 @@
import Koa from 'koa'
import koaBody from 'koa-body';
import router from './router';
import helmet from 'koa-helmet'
import koaBunyanLogger from 'koa-bunyan-logger'
import koaCors from 'koa2-cors';
import { errorHandler } from './utils/response'
const app = new Koa();
// 跨域请求设置
app.use(koaCors({
origin: function (ctx) { //设置允许来自指定域名请求
return '*'
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: true, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法'
allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'x-tt-session-v2'], //设置服务器支持的所有头信息字段
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
}))
// 过滤不安全的请求内容
// app.use(helmet.contentSecurityPolicy());
app.use(helmet.dnsPrefetchControl());
app.use(helmet.expectCt());
app.use(helmet.frameguard());
app.use(helmet.hidePoweredBy());
app.use(helmet.hsts());
app.use(helmet.ieNoOpen());
app.use(helmet.noSniff());
app.use(helmet.permittedCrossDomainPolicies());
app.use(helmet.referrerPolicy());
app.use(helmet.xssFilter());
// 解析不同类别的请求
app.use(koaBody({
multipart: true,
formidable: {
maxFileSize: 30 * 1024 * 1024
}
}));
// 请求日志
app.use(koaBunyanLogger());
app.use(koaBunyanLogger.requestIdContext());
app.use(koaBunyanLogger.requestLogger());
// 若后面的路由抛错,则封装为错误响应返回
app.use(errorHandler);
// 为应用使用路由定义
app.use(router.routes()).use(router.allowedMethods());
export { app }

View File

@@ -0,0 +1,56 @@
import { getCurrentUser, login, logout, register } from '../services/user';
import { Context, description, middlewares, request, security, securityAll, summary, swaggerClass, swaggerProperty, tagsAll } from 'koa-swagger-decorator';
import { authenticate, swaggerBody } from '../utils/swagger';
import { isNil } from 'lodash';
import { authentication } from '../utils/response';
@swaggerClass()
export class LoginRegDto {
@swaggerProperty({ type: "string", required: true }) username = "";
@swaggerProperty({ type: "string", required: true }) password = "";
};
@tagsAll('认证系统')
export default class Auth {
@request('POST', '/auth/register')
@summary('注册一个用户')
@swaggerBody(LoginRegDto)
static async register(ctx: Context) {
const params = ctx.validatedBody as LoginRegDto;
const data = await register(
ctx,
params.username,
params.password
);
ctx.body = { code: 200, msg: '' }
}
@request('POST', '/auth/login')
@summary('登录一个用户')
@swaggerBody(LoginRegDto)
static async login(ctx: Context) {
const params = ctx.validatedBody as LoginRegDto;
const data = await login(
ctx,
params.username,
params.password
);
ctx.body = { code: 200, msg: '' }
}
@request('GET', '/auth/info')
@summary('获取当前的用户信息')
@authenticate
static async info(ctx: Context) {
const data = await getCurrentUser(ctx)
ctx.body = { code: 200, msg: '', data };
}
@request('POST', '/auth/logout')
@summary('登出当前系统')
@security([{ session: [] }])
static async logout(ctx: Context) {
await logout(ctx)
ctx.body = { code: 200, msg: '' };
}
}

View File

@@ -0,0 +1,29 @@
import { getCurrentUser, login, logout, register } from '../services/user';
import { Context, description, middlewares, formData, request, security, securityAll, summary, swaggerClass, swaggerProperty, tagsAll } from 'koa-swagger-decorator';
import { swaggerBody } from '../utils/swagger';
import { isArray, isNil, uniqueId } from 'lodash';
import { authentication } from '../utils/response';
import { uploadFile } from '../services/files';
import { readFileSync } from 'fs'
@tagsAll('文件系统')
export default class RemoteFile {
@request('POST', '/files')
@summary('上传文件')
@formData({ file: { type: 'file', required: true, description: '文件内容' } })
@security([{ session: [] }])
@middlewares([authentication])
static async upload(ctx: Context) {
const file = ctx.request.files?.['file']
if (isNil(file)) throw { code: 400, msg: '未上传任何文件' }
if (isArray(file)) throw { code: 400, msg: '只能上传一个文件' }
const content = readFileSync(file.path)
const data = await uploadFile(file.name || uniqueId('file'), content)
ctx.body = { code: 200, msg: '', data }
}
}

View File

@@ -0,0 +1,19 @@
import { Context, description, middlewares, request, security, summary, swaggerClass, swaggerProperty, tagsAll } from 'koa-swagger-decorator';
import { authentication } from '../utils/response';
import { authenticate, swaggerBody } from '../utils/swagger';
@tagsAll('测试')
export default class Index {
@request('GET', '/test')
@summary('测试服务器是否正常')
static async test(ctx: Context) {
ctx.body = { code: 200, msg: '', data: new Date() }
}
@request('GET', '/authtest')
@summary('测试服务器是否正常')
@authenticate
static async authtest(ctx: Context) {
ctx.body = { code: 200, msg: '', data: new Date() }
}
}

View File

@@ -0,0 +1,50 @@
import { getCurrentUser } from './../services/user';
import { Context, description, middlewares, path, request, security, summary, swaggerClass, swaggerProperty, tagsAll } from 'koa-swagger-decorator';
import { authentication } from '../utils/response';
import { authenticate, swaggerBody } from '../utils/swagger';
import { ObjectId, UserContext } from '../models/base';
import memoService from '../services/memo';
import { multipageHttpQuery } from '../utils/multipage';
import { User } from '../models/user';
export class CreateMemoDto {
@swaggerProperty({ type: "string", required: true }) content = "";
};
@tagsAll('备忘录')
export default class Memo {
@request('GET', '/memos')
@summary('获取个人所有的备忘录')
@authenticate
@multipageHttpQuery()
static async getAll(ctx: UserContext) {
const data = await memoService.findByUserPage(ctx.user._id, ctx.validatedQuery)
ctx.body = { code: 200, msg: '', data }
}
@request('POST', '/memos')
@summary('创建一个备忘录')
@authenticate
@swaggerBody(CreateMemoDto)
static async createOne(ctx: UserContext) {
// ctx.user 获取的只是一个JSON字段不支持数据库关联
const user = await memoService.db.table<User>('_user').where({ _id: new ObjectId(ctx.user._id) }).projection({ username: 1, lastLogin: 1, status: 1 }).findOne()
const data = await memoService.createOne({
content: ctx.validatedBody.content,
user
})
ctx.body = { code: 200, msg: '', data }
}
@request('DELETE', '/memos/{id}')
@summary('删除一个备忘录')
@authenticate
@path({ id: { type: 'string', required: true, description: 'ID' } })
static async deleteOne(ctx: UserContext) {
await memoService.deleteOne(ctx.validatedParams.id)
ctx.body = { code: 200, msg: '' }
}
}

View File

@@ -1 +0,0 @@
export * from './lib/services';

View File

@@ -1,7 +0,0 @@
import { services } from './services';
describe('services', () => {
it('should work', () => {
expect(services()).toEqual('services');
});
});

View File

@@ -1,3 +0,0 @@
export function services(): string {
return 'services';
}

View File

@@ -0,0 +1,15 @@
import { Context } from 'koa-swagger-decorator';
export type WithId<T extends object> = T & { _id: string }
import inspirecloud from '@byteinspire/api';
import { User } from './user';
export const ObjectId = inspirecloud.db.ObjectId
export type ObjectIdType = InstanceType<typeof ObjectId>
export interface PageQuery { page: number, pageSize: number }
export interface UserContext extends Context {
user: WithId<User>
}

View File

@@ -0,0 +1,8 @@
import { IEntity } from '@byteinspire/db';
import { User } from './user';
import { ObjectIdType } from './base';
export interface Memo {
content: string;
user: User
}

View File

@@ -0,0 +1,17 @@
export interface User {
avatar: string | null;
email: string | null;
phoneNumber: string | null;
intro: string | null;
lastLogin: number;
loginCount: number;
lastIp: string;
status: boolean;
createAt: number;
createdAt: number;
updatedAt: number;
username: string;
firstProvider: string;
loginAt: number;
expireAt: number;
}

View File

@@ -0,0 +1,32 @@
import '@koa/router'
import { SwaggerRouter } from 'koa-swagger-decorator'
import path from 'path'
const router = new SwaggerRouter({ prefix: '/api' }) // extends from koa-router
// swagger docs avaliable at http://localhost:3000/api/swagger-html
router.swagger({
title: 'BitDance浏览器插件后台服务',
description: '请求认证头为 `x-tt-session-v2`',
version: '1.0.0',
prefix: '/api',
swaggerHtmlEndpoint: '/swagger-html',
swaggerJsonEndpoint: '/swagger-json',
swaggerOptions: {
securityDefinitions: {
session: {
type: 'apiKey',
in: 'header',
name: 'x-tt-session-v2',
},
},
},
})
// mapDir will scan the input dir, and automatically call router.map to all Router Class
router.mapDir(path.resolve(__dirname, 'controllers'), {
ignore: ["**.spec.ts", "**.d.ts"],
})
export default router

View File

@@ -0,0 +1,108 @@
import { isNil } from 'lodash';
import { IEntity, ITable, API } from '@byteinspire/db';
import { Memo } from '../models/memo';
import { Context } from 'koa-swagger-decorator';
import inspirecloud from '@byteinspire/api'
import { multipageDbQuery } from '../utils/multipage';
import { PageQuery, ObjectIdType, ObjectId } from '../models/base';
export default class BaseDbService<T> {
table: ITable<T>
db: API = inspirecloud.db
/**
*
* @param name 表名
*/
constructor(name: string) {
this.table = inspirecloud.db.table<T>(name)
}
/**
* 查找多个
* @returns
*/
findMany(query: any): Promise<(T & IEntity)[]> {
return query.find()
}
/**
* 查找全部
* @returns
*/
findAll(): Promise<(T & IEntity)[]> {
return this.findMany(this.table.where())
}
/**
* 查找单个
* @returns
*/
findOne(id: string): Promise<(T & IEntity)> {
const result = this.table.where({ _id: new ObjectId(id) }).findOne()
if (isNil(result)) throw { status: 404, message: 'Not Found' }
return result
}
/**
* 分页查找
* @returns
*/
findPage(pageArg: PageQuery, query: any) {
return multipageDbQuery(pageArg, query)
}
/**
* 分页全部查找
* @param pageArg
* @returns
*/
findAllPage(pageArg: PageQuery) {
return this.findPage(pageArg, this.table.where())
}
/**
* 创建单个
* @returns
*/
createOne(item: T): Promise<(T & IEntity)> {
return this.table.save(item)
}
/**
* 更新单个
* @returns
*/
async updateOne(item: Partial<T> & { _id: string }): Promise<T & IEntity> {
const result = await this.findOne(item._id)
for (const [k, v] of Object.entries(item) as [keyof (T & IEntity), any][]) {
if (k === '_id' || k === 'createdAt' || k === 'updatedAt') continue
result[k] = v
}
return this.table.save(result)
}
/**
* 删除数据
* @param id
*/
async deleteOne(id: string) {
const result = await this.table.where({ _id: new ObjectId(id) }).findOne()
if (isNil(result)) throw { status: 404, message: 'Not Found' }
return await this.table.delete([result])
}
/**
* 删除多个数据
* @param ids
* @returns 成功删除数据的数目
*/
async deleteMany(ids: string[]) {
const result = await this.table.where({ _id: this.db.in(ids) }).find()
const delRes = await this.table.delete(result)
return (delRes as any).deletedCount
}
}

View File

@@ -0,0 +1,14 @@
import inspirecloud from '@byteinspire/api';
const fileService = inspirecloud.file
const uploadFile = fileService.upload.bind(fileService) as any as (
name: string,
buffer: Buffer | string | { url: string },
) => Promise<{ url: string; id: string }>;
const downloadFile = fileService.download.bind(fileService)
const removeFile = fileService.delete.bind(fileService)
export { uploadFile, downloadFile, removeFile }

View File

@@ -0,0 +1,19 @@
import { getCurrentUser } from './user';
import { ObjectId } from './../models/base';
import { Memo } from './../models/memo';
import BaseDbService from './base'
import { IEntity, ITable, API } from '@byteinspire/db';
import { PageQuery } from '../models/base';
class MemoService extends BaseDbService<Memo> {
findByUserPage(uid: string, pageArg: PageQuery) {
return this.findPage(pageArg, this.table.where({ user: new ObjectId(uid) }).populate({
ref: 'user',
projection: ['_id', 'username', 'nickname', 'lastLogin', 'status']
} as any))
}
}
const memoService = new MemoService('memo')
export default memoService

View File

@@ -0,0 +1,25 @@
import { User } from './../models/user';
import { WithId } from '../models/base';
import { Context } from 'koa-swagger-decorator';
import inspirecloud from '@byteinspire/api';
import { isNil } from 'lodash';
const userService = (inspirecloud as any).user
export const login = userService.login as (ctx: Context, username: string, password: string) => Promise<undefined>
export const register = userService.register as (ctx: Context, username: string, password: string) => Promise<undefined>
export const getCurrentUser = userService.current as (ctx: Context) => Promise<WithId<User> | undefined>
export const logout = userService.logout as (ctx: Context) => Promise<undefined>
export const updateNewPassword = userService.changePassword as (context: Context, newPassword: string, originPassword?: string) => Promise<any>;
export async function isLogin(ctx: Context) {
try {
return !isNil(await getCurrentUser(ctx))
} catch (error) {
return false
}
}

View File

@@ -0,0 +1,27 @@
import { Context, query } from 'koa-swagger-decorator';
import { PageQuery } from '../models/base';
// 用于分页的数据库查询辅助函数
export async function multipageDbQuery<T = any>(pageArg: PageQuery, query: any) {
const { page = 1, pageSize = 10 } = pageArg
if (page < 1 || pageSize < 1) throw { status: 400, message: '分页参数不合法' }
const total = await query.count();
const content = await query.skip((page - 1) * pageSize).limit(pageSize).find();
return {
total,
page,
pageSize,
content: (content || []) as T[]
}
}
//用于分页的HTTP Query查询
export function multipageHttpQuery() {
return query(
{
page: { type: 'number', required: false, default: 1, description: 'type' },
pageSize: { type: 'number', required: false, default: 10, description: 'type' }
})
}

View File

@@ -0,0 +1,35 @@
import { Context } from 'koa-swagger-decorator';
import { UserContext } from '../models/base';
import { getCurrentUser, isLogin } from '../services/user';
// 处理请求中的错误
export async function errorHandler(ctx: Context, next: any) {
try {
await next();
} catch (err) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const error = err as any
// 抛出的错误可以附带 status 字段,代表 http 状态码
// 若没有提供,则默认状态码为 500代表服务器内部错误
ctx.status = error.status || 500;
ctx.body = { code: ctx.status, msg: error.message };
console.error(err)
}
}
/**
* 对当前的请求进行登录验证,并注入用户信息到 ctx.user
* @param ctx 请求上下文
* @param next
*/
export async function authentication(ctx: UserContext, next: any) {
try {
const user = await getCurrentUser(ctx)
if (!user) throw ""
ctx.user = user
} catch (error) {
console.error(error)
throw { status: 401, message: "用户未登录" }
}
await next()
}

View File

@@ -0,0 +1,24 @@
import { body, security } from 'koa-swagger-decorator'
import { authentication } from './response'
export function swaggerDocument(cls: any) {
return (cls as any).swaggerDocument
}
export function swaggerBody<T extends object>(cls: T) {
return body(swaggerDocument(cls))
}
export function authenticate(
target: any,
name: string,
descriptor: PropertyDescriptor
) {
if (!descriptor.value.middlewares) descriptor.value.middlewares = []
descriptor.value.middlewares.push(authentication)
// call other decorators
security([{ session: [] }])(target, name, descriptor)
return descriptor;
};