From 6b3be175e9479be4498740a23bcb01605aeb248c Mon Sep 17 00:00:00 2001 From: South Drifted Date: Sat, 19 Oct 2024 02:57:06 +0800 Subject: [PATCH] [add] Captcha & SMS code API based on LeanCloud REST API (#29) --- .github/workflows/Lark.yml | 401 +++++++++++++++++++++++++++++++++++ ReadMe.md | 24 ++- docker-compose.yml | 4 +- package.json | 2 +- src/controller/CheckEvent.ts | 10 +- src/controller/Crawler.ts | 9 +- src/controller/User.ts | 116 ++++++---- src/index.ts | 4 +- src/model/ActivityLog.ts | 3 +- src/model/User.ts | 45 ++-- src/utility.ts | 67 +++++- type/package.json | 2 +- 12 files changed, 605 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/Lark.yml diff --git a/.github/workflows/Lark.yml b/.github/workflows/Lark.yml new file mode 100644 index 0000000..6463903 --- /dev/null +++ b/.github/workflows/Lark.yml @@ -0,0 +1,401 @@ +name: Lark notification + +# https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows +on: + push: + issues: + pull_request: + discussion: + issue_comment: + discussion_comment: + +jobs: + send-Lark-message: + runs-on: ubuntu-latest + steps: + - name: Commit message cleaning + id: commit_message + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + run: | + echo "$COMMIT_MESSAGE" | sed 's/"/\\"/g' > commit_message.txt + { + echo 'commit_message<> $GITHUB_OUTPUT + + - name: Commits pushed + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'push' + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub 代码提交 + content: + - - tag: text + text: 提交链接: + - tag: a + text: ${{ github.event.head_commit.url }} + href: ${{ github.event.head_commit.url }} + - - tag: text + text: 代码分支: + - tag: a + text: ${{ github.ref }} + href: ${{ github.server_url }}/${{ github.repository }}/tree/${{ github.ref_name }} + - - tag: text + text: 提交作者: + - tag: a + text: ${{ github.actor }} + href: ${{ github.server_url }}/${{ github.actor }} + - - tag: text + text: 提交信息: + - tag: text + text: "${{ steps.commit_message.outputs.commit_message }}" + + - name: Issue body cleaning + id: issue_body + env: + ISSUE_BODY: ${{ github.event.issue.body }} + run: | + echo "$ISSUE_BODY" | sed 's/"/\\"/g' > issue_body.txt + { + echo 'issue_body<> $GITHUB_OUTPUT + + - name: Issue opened + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub issue 打开:${{ github.event.issue.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.issue.html_url }} + href: ${{ github.event.issue.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.issue.user.login }} + href: ${{ github.event.issue.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.issue.assignee.login }}" + href: "${{ github.event.issue.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.issue.labels }} + - - tag: text + text: 里程碑:${{ github.event.issue.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.issue_body.outputs.issue_body }}" + + - name: Issue edited + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'issues' && (github.event.action == 'edited' || github.event.action == 'transferred' || github.event.action == 'labeled' || github.event.action == 'unlabeled' || github.event.action == 'assigned' || github.event.action == 'unassigned') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub issue 编辑:${{ github.event.issue.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.issue.html_url }} + href: ${{ github.event.issue.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.issue.user.login }} + href: ${{ github.event.issue.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.issue.assignee.login }}" + href: "${{ github.event.issue.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.issue.labels }} + - - tag: text + text: 里程碑:${{ github.event.issue.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.issue_body.outputs.issue_body }}" + + - name: Issue closed + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'issues' && github.event.action == 'closed' + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub issue 关闭:${{ github.event.issue.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.issue.html_url }} + href: ${{ github.event.issue.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.issue.user.login }} + href: ${{ github.event.issue.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.issue.assignee.login }}" + href: "${{ github.event.issue.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.issue.labels }} + - - tag: text + text: 里程碑:${{ github.event.issue.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.issue_body.outputs.issue_body }}" + + - name: PR body cleaning + id: PR_body + env: + PR_BODY: ${{ github.event.pull_request.body }} + run: | + echo "$PR_BODY" | sed 's/"/\\"/g' > PR_body.txt + { + echo 'PR_body<> $GITHUB_OUTPUT + + - name: PR opened + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub PR 打开:${{ github.event.pull_request.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.pull_request.html_url }} + href: ${{ github.event.pull_request.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.pull_request.user.login }} + href: ${{ github.event.pull_request.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.pull_request.assignee.login }}" + href: "${{ github.event.pull_request.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.pull_request.labels }} + - - tag: text + text: 里程碑:${{ github.event.pull_request.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.PR_body.outputs.PR_body }}" + + - name: PR edited + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'pull_request' && (github.event.action == 'edited' || github.event.action == 'labeled' || github.event.action == 'unlabeled' || github.event.action == 'assigned' || github.event.action == 'unassigned') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub PR 编辑:${{ github.event.pull_request.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.pull_request.html_url }} + href: ${{ github.event.pull_request.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.pull_request.user.login }} + href: ${{ github.event.pull_request.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.pull_request.assignee.login }}" + href: "${{ github.event.pull_request.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.pull_request.labels }} + - - tag: text + text: 里程碑:${{ github.event.pull_request.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.PR_body.outputs.PR_body }}" + + - name: PR closed + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'pull_request' && github.event.action == 'closed' + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub PR 关闭:${{ github.event.pull_request.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.pull_request.html_url }} + href: ${{ github.event.pull_request.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.pull_request.user.login }} + href: ${{ github.event.pull_request.user.html_url }} + - - tag: text + text: 指派: + - tag: a + text: "${{ github.event.pull_request.assignee.login }}" + href: "${{ github.event.pull_request.assignee.html_url }}" + - - tag: text + text: 标签:${{ github.event.pull_request.labels }} + - - tag: text + text: 里程碑:${{ github.event.pull_request.milestone.title }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.PR_body.outputs.PR_body }}" + + - name: Discussion body cleaning + id: discussion_body + env: + DISCUSSION_BODY: ${{ github.event.discussion.body }} + run: | + echo "$DISCUSSION_BODY" | sed 's/"/\\"/g' > discussion_body.txt + { + echo 'discussion_body<> $GITHUB_OUTPUT + + - name: Discussion created + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'discussion' && github.event.action == 'created' + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub 帖子发布:${{ github.event.discussion.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.discussion.html_url }} + href: ${{ github.event.discussion.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.discussion.user.login }} + href: ${{ github.event.discussion.user.html_url }} + - - tag: text + text: 分类:${{ github.event.discussion.category }} + - - tag: text + text: 标签:${{ github.event.discussion.labels }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.discussion_body.outputs.discussion_body }}" + + - name: Discussion edited + uses: foxundermoon/feishu-action@v2 + if: github.event_name == 'discussion' && (github.event.action == 'edited' || github.event.action == 'transferred' || github.event.action == 'category_changed' || github.event.action == 'labeled' || github.event.action == 'unlabeled') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub 帖子修改:${{ github.event.discussion.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.discussion.html_url }} + href: ${{ github.event.discussion.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.discussion.user.login }} + href: ${{ github.event.discussion.user.html_url }} + - - tag: text + text: 分类:${{ github.event.discussion.category }} + - - tag: text + text: 标签:${{ github.event.discussion.labels }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.discussion_body.outputs.discussion_body }}" + + - name: Comment body cleaning + id: comment_body + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + echo "$COMMENT_BODY" | sed 's/"/\\"/g' > comment_body.txt + { + echo 'comment_body<> $GITHUB_OUTPUT + + - name: Issue/Discussion commented + uses: foxundermoon/feishu-action@v2 + if: (github.event_name == 'issue_comment' || github.event_name == 'discussion_comment') && (github.event.action == 'created' || github.event.action == 'edited') + with: + url: ${{ secrets.LARK_CHATBOT_HOOK_URL }} + msg_type: post + content: | + post: + zh_cn: + title: GitHub 帖子评论:${{ github.event.issue.title }} + content: + - - tag: text + text: 链接: + - tag: a + text: ${{ github.event.comment.html_url }} + href: ${{ github.event.comment.html_url }} + - - tag: text + text: 作者: + - tag: a + text: ${{ github.event.comment.user.login }} + href: ${{ github.event.comment.user.html_url }} + - - tag: text + text: 描述: + - tag: text + text: "${{ steps.comment_body.outputs.comment_body }}" diff --git a/ReadMe.md b/ReadMe.md index 0938fda..0e1c351 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -45,11 +45,16 @@ pnpm i @kaiyuanshe/kys-service -D | Name | Usage | | :---------------------: | :---------------------------------------: | | `DATABASE_URL` | [PostgreSQL][11] connection string | -| `AZURE_BLOB_CONNECTION` | [Azure Blob Storage][12] service | +| `APP_SECRET` | encrypt Password & Token | | `WEB_HOOK_TOKEN` | `Authorization` token of Custom Web hooks | -| `AUTHING_APP_SECRET` | encrypt Password & Token | -| `LARK_APP_ID` | App ID of [Lark API][13] | -| `LARK_APP_SECRET` | App Secret of [Lark API][13] | +| `AZURE_BLOB_CONNECTION` | [Azure Blob Storage][12] service | +| `LEANCLOUD_API_HOST` | API domain of [LeanCloud][13] | +| `LEANCLOUD_APP_ID` | App ID of [LeanCloud][13] | +| `LEANCLOUD_APP_KEY` | App Key of [LeanCloud][13] | +| `LARK_APP_ID` | App ID of [Lark API][14] | +| `LARK_APP_SECRET` | App Secret of [Lark API][14] | +| `HR_BASE_ID` | BI Table ID of HR data in Lark | +| `PERSON_TABLE_ID` | BI Data Table ID of Person data in Lark | ## Development @@ -66,7 +71,7 @@ pnpm i pnpm dev ``` -or just press F5 key in [VS Code][14]. +or just press F5 key in [VS Code][15]. ### Migration @@ -101,7 +106,7 @@ pnpm container ```shell git checkout master -git tag v0.6.0 # this version tag comes from ./package.json +git tag v1.0.0 # this version tag comes from ./package.json git push origin master --tags ``` @@ -109,7 +114,7 @@ git push origin master --tags ```shell git checkout master -git tag type-v0.6.0 # this version tag comes from ./type/package.json +git tag type-v1.0.0 # this version tag comes from ./type/package.json git push origin master --tags ``` @@ -125,5 +130,6 @@ git push origin master --tags [10]: https://github.com/settings/tokens [11]: https://www.postgresql.org/ [12]: https://azure.microsoft.com/en-us/products/storage/blobs -[13]: https://open.feishu.cn/ -[14]: https://code.visualstudio.com/ +[13]: https://www.leancloud.cn/ +[14]: https://open.feishu.cn/ +[15]: https://code.visualstudio.com/ diff --git a/docker-compose.yml b/docker-compose.yml index 122019c..5403613 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: postgres: image: postgres environment: - - POSTGRES_PASSWORD=${AUTHING_APP_SECRET} + - POSTGRES_PASSWORD=${APP_SECRET} volumes: - ./data:/var/lib/postgresql/data/ ports: @@ -20,7 +20,7 @@ services: - postgres image: kaiyuanshe/kys-service environment: - - DATABASE_URL=postgres://postgres:${AUTHING_APP_SECRET}@postgres:5432/postgres + - DATABASE_URL=postgres://postgres:${APP_SECRET}@postgres:5432/postgres - NODE_ENV=production - PORT=8080 ports: diff --git a/package.json b/package.json index 6f5bde6..167bd13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kys-service", - "version": "0.9.0", + "version": "1.0.0", "license": "AGPL-3.0", "author": "shiy2008@gmail.com", "description": "RESTful API service of KaiYuanShe", diff --git a/src/controller/CheckEvent.ts b/src/controller/CheckEvent.ts index cf519c1..5e55bd4 100644 --- a/src/controller/CheckEvent.ts +++ b/src/controller/CheckEvent.ts @@ -19,6 +19,7 @@ import { User, dataSource } from '../model'; +import { ActivityLogController } from './ActivityLog'; @JsonController('/event/check') export class CheckEventController { @@ -44,7 +45,14 @@ export class CheckEventController { if (checked) throw new ForbiddenError('No duplicated check'); - return this.store.save({ ...data, createdBy, user }); + const saved = await this.store.save({ ...data, createdBy, user }); + + await ActivityLogController.logCreate( + createdBy, + 'CheckEvent', + saved.id + ); + return saved; } @Get() diff --git a/src/controller/Crawler.ts b/src/controller/Crawler.ts index 3d456b8..5fd7c07 100644 --- a/src/controller/Crawler.ts +++ b/src/controller/Crawler.ts @@ -11,8 +11,7 @@ import { ResponseSchema } from 'routing-controllers-openapi'; import { loadPage, fetchAsset } from 'web-fetch'; import { - AZURE_BLOB_CONNECTION, - blobEndPointOf, + OWSBlobRoot, uploadToAzureBlob, lark, CommonBiDataTable @@ -26,8 +25,6 @@ import { LarkBaseTableFileModel } from '../model'; -const OWSBlobHost = blobEndPointOf(AZURE_BLOB_CONNECTION); - @JsonController('/crawler') export class CrawlerController { @Post('/task/page') @@ -38,7 +35,7 @@ export class CrawlerController { ): Promise { const scope = parse(source).name, folder = 'article'; - const baseURI = `${OWSBlobHost}/$web/${folder}/`, + const baseURI = `${OWSBlobRoot}/${folder}/`, { window: { document } } = await loadPage(source); @@ -71,7 +68,7 @@ export class CrawlerController { await lark.downloadFile(item.file_token) ), path = `file/${item.name}`; - const URI = `${OWSBlobHost}/$web/${path}`; + const URI = `${OWSBlobRoot}/${path}`; await uploadToAzureBlob(file, path, item.type); diff --git a/src/controller/User.ts b/src/controller/User.ts index eab7b3c..7eb108a 100644 --- a/src/controller/User.ts +++ b/src/controller/User.ts @@ -1,4 +1,3 @@ -import { createHash } from 'crypto'; import { JsonWebTokenError, sign } from 'jsonwebtoken'; import { Authorized, @@ -19,47 +18,63 @@ import { import { ResponseSchema } from 'routing-controllers-openapi'; import { + Captcha, dataSource, + Gender, JWTAction, SignInData, + SMSCodeInput, User, UserFilter, UserListChunk } from '../model'; -import { AUTHING_APP_SECRET, searchConditionOf } from '../utility'; +import { + APP_SECRET, + blobURLOf, + leanClient, + PersonBiDataTable, + searchConditionOf +} from '../utility'; import { ActivityLogController } from './ActivityLog'; const store = dataSource.getRepository(User); @JsonController('/user') export class UserController { - static encrypt = (raw: string) => - createHash('sha1') - .update(AUTHING_APP_SECRET + raw) - .digest('hex'); - static sign = (user: User): User => ({ ...user, - token: sign({ ...user }, AUTHING_APP_SECRET) + token: sign({ ...user }, APP_SECRET) }); - static async signUp({ mobilePhone, password }: SignInData) { - const { password: _, ...user } = await store.save({ - name: mobilePhone, + static async signUp({ mobilePhone }: SignInData) { + const [{ name, gender, avatar, email } = {}] = + await new PersonBiDataTable().getList({ 手机号: mobilePhone }), + existed = await store.findOneBy({ mobilePhone }); + + const saved = await store.save({ + ...existed, mobilePhone, - password: UserController.encrypt(password) + email, + nickName: name || existed.nickName || mobilePhone, + gender: + gender === '女' + ? Gender.Female + : gender === '男' + ? Gender.Male + : Gender.Other, + avatar: blobURLOf(avatar) }); - await ActivityLogController.logCreate(user, 'User', user.id); - return user; - } + if (!existed) + await ActivityLogController.logCreate(saved, 'User', saved.id); + else await ActivityLogController.logUpdate(saved, 'User', saved.id); - static getSession({ context: { state } }: JWTAction) { - return state instanceof JsonWebTokenError - ? console.error(state) - : state.user; + return saved; } + static getSession = ({ context: { state } }: JWTAction) => + state instanceof JsonWebTokenError ? console.error(state) : state.user; + @Get('/session') @Authorized() @ResponseSchema(User) @@ -67,24 +82,52 @@ export class UserController { return user; } - @Post('/session') - @HttpCode(201) - @ResponseSchema(User) - async signIn(@Body() { mobilePhone, password }: SignInData): Promise { - const user = await store.findOneBy({ - mobilePhone, - password: UserController.encrypt(password) - }); - if (!user) throw new ForbiddenError(); + @Post('/session/captcha') + @ResponseSchema(Captcha) + async createCaptcha() { + const { body } = + await leanClient.get>( + 'requestCaptcha' + ); + return { token: body.captcha_token, link: body.captcha_url }; + } - return UserController.sign(user); + static async verifyCaptcha(captcha_token: string, captcha_code: string) { + const { body } = await leanClient.post<{ validate_token: string }>( + 'verifyCaptcha', + { captcha_code, captcha_token } + ); + return { token: body.validate_token }; } - @Post() + @Post('/session/code') + @OnUndefined(201) + async createSMSCode( + @Body() { captchaToken, captchaCode, mobilePhone }: SMSCodeInput + ) { + if (captchaToken && captchaCode) + var { token } = await UserController.verifyCaptcha( + captchaToken, + captchaCode + ); + await leanClient.post<{}>('requestSmsCode', { + mobilePhoneNumber: mobilePhone, + validate_token: token + }); + } + + static verifySMSCode = (mobilePhoneNumber: string, code: string) => + leanClient.post<{}>(`verifySmsCode/${code}`, { mobilePhoneNumber }); + + @Post('/session') @HttpCode(201) @ResponseSchema(User) - signUp(@Body() data: SignInData) { - return UserController.signUp(data); + async signIn(@Body() { mobilePhone, code }: SignInData): Promise { + await UserController.verifySMSCode(mobilePhone, code); + + const user = await UserController.signUp({ mobilePhone, code }); + + return UserController.sign(user); } @Put('/:id') @@ -93,15 +136,12 @@ export class UserController { async updateOne( @Param('id') id: number, @CurrentUser() updatedBy: User, - @Body() { password, ...data }: User + @Body() data: User ) { if (id !== updatedBy.id) throw new ForbiddenError(); - const saved = await store.save({ - ...data, - password: password && UserController.encrypt(password), - id - }); + const saved = await store.save({ ...data, id }); + await ActivityLogController.logUpdate(updatedBy, 'User', id); return UserController.sign(saved); diff --git a/src/index.ts b/src/index.ts index ba40d36..286446c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,13 +19,13 @@ import { UserController } from './controller'; import { dataSource } from './model'; -import { AUTHING_APP_SECRET, isProduct, PORT } from './utility'; +import { APP_SECRET, isProduct, PORT } from './utility'; const HOST = `localhost:${PORT}`, app = new Koa() .use(KoaLogger()) .use(swagger({ exposeSpec: true })) - .use(jwt({ secret: AUTHING_APP_SECRET, passthrough: true })); + .use(jwt({ secret: APP_SECRET, passthrough: true })); if (!isProduct) app.use(mocker()); diff --git a/src/model/ActivityLog.ts b/src/model/ActivityLog.ts index 12c3a8c..d568e3e 100644 --- a/src/model/ActivityLog.ts +++ b/src/model/ActivityLog.ts @@ -10,6 +10,7 @@ import { import { Column, Entity, ViewColumn, ViewEntity } from 'typeorm'; import { Base, BaseFilter, InputData, ListChunk } from './Base'; +import { CheckEvent } from './CheckEvent'; import { User, UserBase } from './User'; export enum Operation { @@ -18,7 +19,7 @@ export enum Operation { Delete = 'delete' } -export const LogableTable = { User }; +export const LogableTable = { User, CheckEvent }; const LogableTableEnum = Object.fromEntries( Object.entries(LogableTable).map(([key]) => [key, key]) diff --git a/src/model/User.ts b/src/model/User.ts index 53f4c50..d876bbc 100644 --- a/src/model/User.ts +++ b/src/model/User.ts @@ -1,12 +1,12 @@ import { Type } from 'class-transformer'; import { + IsEmail, IsEnum, IsInt, IsJWT, IsMobilePhone, IsOptional, IsString, - IsStrongPassword, IsUrl, Min, ValidateNested @@ -29,12 +29,17 @@ export class User extends Base { @IsString() @IsOptional() @Column({ nullable: true }) - uuid: string; + uuid?: string; @IsMobilePhone() @Column({ unique: true }) mobilePhone: string; + @IsEmail() + @IsOptional() + @Column({ nullable: true }) + email?: string; + @IsString() @IsOptional() @Column({ nullable: true }) @@ -50,16 +55,9 @@ export class User extends Base { @Column({ nullable: true }) avatar?: string; - @IsStrongPassword() - @IsOptional() - @Column({ nullable: true, select: false }) - password?: string; - @IsJWT() @IsOptional() token?: string; - - iat?: number; } export class UserFilter extends BaseFilter implements Partial> { @@ -117,22 +115,33 @@ export class UserBaseFilter updatedBy?: number; } -export class SignInData - implements Required> -{ +export class Captcha { + @IsString() + token: string; + + @IsUrl() + link: string; +} + +export class SMSCodeInput { @IsMobilePhone() mobilePhone: string; @IsString() - password: string; + @IsOptional() + captchaToken?: string; + + @IsString() + @IsOptional() + captchaCode?: string; } -export class SignUpData - extends SignInData - implements Required> -{ +export class SignInData implements Required> { + @IsMobilePhone() + mobilePhone: string; + @IsString() - nickName: string; + code: string; } export interface JWTAction { diff --git a/src/utility.ts b/src/utility.ts index 0ae547f..f380f5c 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -1,6 +1,14 @@ import { BlobServiceClient } from '@azure/storage-blob'; import { fromBuffer } from 'file-type'; -import { BiDataTable, LarkApp, TableRecordFields } from 'mobx-lark'; +import { HTTPClient, HTTPError } from 'koajax'; +import { + BiDataQueryOptions, + BiDataTable, + LarkApp, + TableCellValue, + TableRecordFields +} from 'mobx-lark'; +import { HttpError } from 'routing-controllers'; import { FindOptionsWhere, ILike } from 'typeorm'; import { Base } from './model'; @@ -11,9 +19,14 @@ export const { DATABASE_URL, AZURE_BLOB_CONNECTION, WEB_HOOK_TOKEN, - AUTHING_APP_SECRET, + APP_SECRET, + LEANCLOUD_API_HOST, + LEANCLOUD_APP_ID, + LEANCLOUD_APP_KEY, LARK_APP_ID, LARK_APP_SECRET, + HR_BASE_ID, + PERSON_TABLE_ID, INFURA_API_KEY, SEPOLIA_PVK, SEPOLIA_CONTRACT_ADDRESS @@ -30,6 +43,28 @@ export const searchConditionOf = ( ? keys.map(key => ({ [key]: ILike(`%${keywords}%`), ...filter })) : filter; +export const leanClient = new HTTPClient({ + baseURI: `https://${LEANCLOUD_API_HOST}/1.1/`, + responseType: 'json' +}).use(async ({ request }, next) => { + request.headers = { + ...request.headers, + 'X-LC-Id': LEANCLOUD_APP_ID, + 'X-LC-Key': LEANCLOUD_APP_KEY + }; + try { + await next(); + } catch (error) { + const { response } = error as HTTPError<{ + code: number; + error: string; + }>; + const { status, body } = response; + + throw new HttpError(status, body.error); + } +}); + export const lark = new LarkApp({ id: LARK_APP_ID, secret: LARK_APP_SECRET @@ -39,6 +74,28 @@ export class CommonBiDataTable extends BiDataTable() { client = lark.client; } +export interface Person + extends Record<'name' | 'gender' | 'email' | '手机号', string> { + avatar: TableCellValue; +} + +export class PersonBiDataTable extends BiDataTable() { + client = lark.client; + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + constructor(appId = HR_BASE_ID, tableId = PERSON_TABLE_ID) { + super(appId, tableId); + } +} + +export const blobURLOf = (value: TableCellValue) => + value instanceof Array + ? typeof value[0] === 'object' && + ('file_token' in value[0] || 'attachmentToken' in value[0]) + ? `${OWSBlobRoot}/${value[0].name}` + : '' + : value + ''; + export const parseBlobConnection = (raw: string) => Object.fromEntries( raw.split(';').map(item => { @@ -61,11 +118,15 @@ export function blobEndPointOf(connection: string) { return `${DefaultEndpointsProtocol}://${AccountName}.blob.${EndpointSuffix}`; } +export const OWSBlobHost = blobEndPointOf(AZURE_BLOB_CONNECTION); +export const OWSBlobContainer = '$web'; +export const OWSBlobRoot = `${OWSBlobHost}/${OWSBlobContainer}`; + export async function uploadToAzureBlob( data: Buffer, fileName: string, fileType = 'application/octet-stream', - containerName = '$web' + containerName = OWSBlobContainer ) { const client = BlobServiceClient.fromConnectionString( AZURE_BLOB_CONNECTION diff --git a/type/package.json b/type/package.json index 56d262f..204e7cf 100644 --- a/type/package.json +++ b/type/package.json @@ -1,6 +1,6 @@ { "name": "@kaiyuanshe/kys-service", - "version": "0.7.0", + "version": "1.0.0-rc.0", "types": "index.d.ts", "dependencies": { "@types/jsonwebtoken": "^9.0.7",