Skip to content

Commit

Permalink
[add] Captcha & SMS code API based on LeanCloud REST API (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
TechQuery authored Oct 18, 2024
1 parent c5a8238 commit 6b3be17
Show file tree
Hide file tree
Showing 12 changed files with 605 additions and 82 deletions.
401 changes: 401 additions & 0 deletions .github/workflows/Lark.yml

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -66,7 +71,7 @@ pnpm i
pnpm dev
```

or just press <kbd>F5</kbd> key in [VS Code][14].
or just press <kbd>F5</kbd> key in [VS Code][15].

### Migration

Expand Down Expand Up @@ -101,15 +106,15 @@ 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
```

### Publish Type Package

```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
```

Expand All @@ -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/
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kys-service",
"version": "0.9.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"author": "[email protected]",
"description": "RESTful API service of KaiYuanShe",
Expand Down
10 changes: 9 additions & 1 deletion src/controller/CheckEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
User,
dataSource
} from '../model';
import { ActivityLogController } from './ActivityLog';

@JsonController('/event/check')
export class CheckEventController {
Expand All @@ -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()
Expand Down
9 changes: 3 additions & 6 deletions src/controller/Crawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,8 +25,6 @@ import {
LarkBaseTableFileModel
} from '../model';

const OWSBlobHost = blobEndPointOf(AZURE_BLOB_CONNECTION);

@JsonController('/crawler')
export class CrawlerController {
@Post('/task/page')
Expand All @@ -38,7 +35,7 @@ export class CrawlerController {
): Promise<PageTaskModel> {
const scope = parse(source).name,
folder = 'article';
const baseURI = `${OWSBlobHost}/$web/${folder}/`,
const baseURI = `${OWSBlobRoot}/${folder}/`,
{
window: { document }
} = await loadPage(source);
Expand Down Expand Up @@ -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);

Expand Down
116 changes: 78 additions & 38 deletions src/controller/User.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createHash } from 'crypto';
import { JsonWebTokenError, sign } from 'jsonwebtoken';
import {
Authorized,
Expand All @@ -19,72 +18,116 @@ 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)
getSession(@CurrentUser() user: User) {
return user;
}

@Post('/session')
@HttpCode(201)
@ResponseSchema(User)
async signIn(@Body() { mobilePhone, password }: SignInData): Promise<User> {
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<Record<`captcha_${'token' | 'url'}`, string>>(
'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<User> {
await UserController.verifySMSCode(mobilePhone, code);

const user = await UserController.signUp({ mobilePhone, code });

return UserController.sign(user);
}

@Put('/:id')
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand Down
3 changes: 2 additions & 1 deletion src/model/ActivityLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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])
Expand Down
Loading

0 comments on commit 6b3be17

Please sign in to comment.