Skip to content
80 changes: 68 additions & 12 deletions src/auth/wechat.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { DataSource } from 'typeorm';
import { DataSource, EntityManager } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import generateId from 'omnibox-backend/utils/generate-id';
import { UserService } from 'omnibox-backend/user/user.service';
import { NamespacesService } from 'omnibox-backend/namespaces/namespaces.service';
import { WechatCheckResponseDto } from './dto/wechat-login.dto';
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';

@Injectable()
export class WechatService {
private readonly logger = new Logger(WechatService.name);

private readonly appId: string;
private readonly appSecret: string;
private readonly redirectUri: string;
Expand All @@ -28,6 +32,9 @@ export class WechatService {
}
>();

private readonly minUsernameLength = 2;
private readonly maxUsernameLength = 32;

constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
Expand Down Expand Up @@ -90,6 +97,46 @@ export class WechatService {
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(this.redirectUri)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
}

generateSuffix(): string {
return (
'_' +
generateId(4, 'useandomTPXpxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict')
);
}

async getValidUsername(
nickname: string,
manager: EntityManager,
): Promise<string> {
let username = nickname;

if (username.length > this.maxUsernameLength) {
username = nickname.slice(0, this.maxUsernameLength);
}
if (username.length >= this.minUsernameLength) {
const user = await this.userService.findByUsername(username, manager);
if (!user) {
return username;
}
}

username = nickname.slice(0, this.maxUsernameLength - 5);
for (let i = 0; i < 5; i++) {
const suffix = this.generateSuffix();
const user = await this.userService.findByUsername(
username + suffix,
manager,
);
if (!user) {
return username + suffix;
}
}

throw new InternalServerErrorException(
'Unable to generate a valid username',
);
}

async handleCallback(code: string, state: string): Promise<any> {
const stateInfo = this.qrCodeStates.get(state);
if (!stateInfo) {
Expand All @@ -98,22 +145,28 @@ export class WechatService {
const isWeixin = stateInfo.type === 'weixin';
const appId = isWeixin ? this.appId : this.openAppId;
const appSecret = isWeixin ? this.appSecret : this.openAppSecret;
const response = await fetch(
const accessTokenResponse = await fetch(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`,
);
if (!response.ok) {
if (!accessTokenResponse.ok) {
throw new UnauthorizedException('Failed to get WeChat access token');
}
const userData = await response.json();
const accessTokenData = await accessTokenResponse.json();

if (userData.errmsg) {
throw new BadRequestException(userData.errmsg);
if (accessTokenData.errmsg) {
throw new BadRequestException(accessTokenData.errmsg);
}

if (!userData.unionid) {
throw new UnauthorizedException(
'Failed to get WeChat UnionID, please make sure you have followed the official account',
);
const userDataResponse = await fetch(
`https://api.weixin.qq.com/sns/userinfo?access_token=${accessTokenData.access_token}&openid=${accessTokenData.openid}&lang=zh_CN`,
);
if (!userDataResponse.ok) {
throw new UnauthorizedException('Failed to get WeChat user info');
}
const userData = await userDataResponse.json();

if (userData.errmsg) {
throw new BadRequestException(userData.errmsg);
}

const wechatUser = await this.userService.findByLoginId(userData.unionid);
Expand All @@ -127,10 +180,13 @@ export class WechatService {
stateInfo.userInfo = returnValue;
return returnValue;
}

return await this.dataSource.transaction(async (manager) => {
const nickname: string = userData.nickname;
const username: string = await this.getValidUsername(nickname, manager);
this.logger.debug({ nickname, username });
const wechatUser = await this.userService.createUserBinding(
{
username,
loginType: 'wechat',
loginId: userData.unionid,
},
Expand Down
3 changes: 3 additions & 0 deletions src/user/dto/create-user-binding.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export class CreateUserBindingDto {

@IsString()
loginType: string;

@IsString()
username: string;
}
13 changes: 12 additions & 1 deletion src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ export class UserService {
return await this.find(binding.userId);
}

async findByUsername(
username: string,
manager?: EntityManager,
): Promise<User | null> {
const repo = manager ? manager.getRepository(User) : this.userRepository;
return await repo.findOne({
where: { username },
select: ['id', 'username', 'email'],
});
}

async createUserBinding(
userData: CreateUserBindingDto,
manager?: EntityManager,
Expand All @@ -101,7 +112,7 @@ export class UserService {
const hash = await bcrypt.hash(Math.random().toString(36), 10);
const newUser = repo.create({
password: hash,
username: userData.loginId,
username: userData.username,
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
7 changes: 4 additions & 3 deletions src/utils/generate-id.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { customAlphabet } from 'nanoid';

export default function generateId(size = 16) {
const urlAlphabet =
'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
export default function generateId(
size = 16,
urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict',
) {
const nanoid = customAlphabet(urlAlphabet, size);
return nanoid();
}