mirror of
https://gitee.com/bitdance-team/chrome-extension
synced 2025-10-07 16:35:15 +08:00
feat: services
This commit is contained in:
55
packages/services/src/app.ts
Normal file
55
packages/services/src/app.ts
Normal 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 }
|
56
packages/services/src/controllers/auth.ts
Normal file
56
packages/services/src/controllers/auth.ts
Normal 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: '' };
|
||||
}
|
||||
}
|
29
packages/services/src/controllers/files.ts
Normal file
29
packages/services/src/controllers/files.ts
Normal 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 }
|
||||
}
|
||||
|
||||
}
|
19
packages/services/src/controllers/index.ts
Normal file
19
packages/services/src/controllers/index.ts
Normal 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() }
|
||||
}
|
||||
}
|
50
packages/services/src/controllers/memo.ts
Normal file
50
packages/services/src/controllers/memo.ts
Normal 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: '' }
|
||||
}
|
||||
|
||||
}
|
@@ -1 +0,0 @@
|
||||
export * from './lib/services';
|
@@ -1,7 +0,0 @@
|
||||
import { services } from './services';
|
||||
|
||||
describe('services', () => {
|
||||
it('should work', () => {
|
||||
expect(services()).toEqual('services');
|
||||
});
|
||||
});
|
@@ -1,3 +0,0 @@
|
||||
export function services(): string {
|
||||
return 'services';
|
||||
}
|
15
packages/services/src/models/base.ts
Normal file
15
packages/services/src/models/base.ts
Normal 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>
|
||||
}
|
8
packages/services/src/models/memo.ts
Normal file
8
packages/services/src/models/memo.ts
Normal 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
|
||||
}
|
17
packages/services/src/models/user.ts
Normal file
17
packages/services/src/models/user.ts
Normal 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;
|
||||
}
|
32
packages/services/src/router.ts
Normal file
32
packages/services/src/router.ts
Normal 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
|
108
packages/services/src/services/base.ts
Normal file
108
packages/services/src/services/base.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
|
14
packages/services/src/services/files.ts
Normal file
14
packages/services/src/services/files.ts
Normal 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 }
|
19
packages/services/src/services/memo.ts
Normal file
19
packages/services/src/services/memo.ts
Normal 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
|
25
packages/services/src/services/user.ts
Normal file
25
packages/services/src/services/user.ts
Normal 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
|
||||
}
|
||||
}
|
27
packages/services/src/utils/multipage.ts
Normal file
27
packages/services/src/utils/multipage.ts
Normal 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' }
|
||||
})
|
||||
}
|
35
packages/services/src/utils/response.ts
Normal file
35
packages/services/src/utils/response.ts
Normal 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()
|
||||
}
|
24
packages/services/src/utils/swagger.ts
Normal file
24
packages/services/src/utils/swagger.ts
Normal 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;
|
||||
};
|
Reference in New Issue
Block a user