Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[add] Captcha & SMS code API based on LeanCloud REST API #29

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading