Skip to content

Commit cce8cdf

Browse files
authored
Merge pull request #186 from import-ai/feat/auth
feat(auth): support wechat migration
2 parents 85a9586 + e4e5f1b commit cce8cdf

File tree

4 files changed

+155
-9
lines changed

4 files changed

+155
-9
lines changed

src/auth/social.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export class SocialService {
2929
}
3030
}
3131

32-
protected setState(type: string) {
33-
const state = generateId();
32+
protected setState(type: string, prefix: string = '') {
33+
const state = `${prefix ? prefix + '_' : ''}${generateId()}`;
3434
this.states.set(state, {
3535
type,
3636
createdAt: Date.now(),

src/auth/wechat/wechat.controller.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Request, Response } from 'express';
2-
import { Get, Post, Query, Controller, Req, Res } from '@nestjs/common';
2+
import { Get, Post, Query, Body, Controller, Req, Res } from '@nestjs/common';
33
import { AuthService } from 'omniboxd/auth/auth.service';
4-
import { WechatService } from 'omniboxd/auth/wechat/wechat.service';
4+
import {
5+
WechatService,
6+
WechatUserInfo,
7+
} from 'omniboxd/auth/wechat/wechat.service';
58
import { SocialController } from 'omniboxd/auth/social.controller';
69
import { Public } from 'omniboxd/auth/decorators/public.auth.decorator';
710
import { ConfigService } from '@nestjs/config';
@@ -67,6 +70,36 @@ export class WechatController extends SocialController {
6770
return res.json(loginData);
6871
}
6972

73+
@Public()
74+
@Get('migration/auth-url')
75+
migrationAuthUrl(@Query('type') type: 'new' | 'old' = 'new') {
76+
return this.wechatService.migrationAuthUrl(type);
77+
}
78+
79+
@Public()
80+
@Get('migration/qrcode')
81+
migrationQrCode(@Query('type') type: 'new' | 'old' = 'new') {
82+
return this.wechatService.migrationQrCode(type);
83+
}
84+
85+
@Public()
86+
@Get('migration/callback')
87+
async migrationCallback(
88+
@Query('code') code: string,
89+
@Query('state') state: string,
90+
): Promise<WechatUserInfo> {
91+
return await this.wechatService.migrationCallback(code, state);
92+
}
93+
94+
@Public()
95+
@Post('migration')
96+
async migration(
97+
@Body('oldUnionid') oldUnionid: string,
98+
@Body('newUnionid') newUnionid: string,
99+
) {
100+
await this.wechatService.migration(oldUnionid, newUnionid);
101+
}
102+
70103
@Post('unbind')
71104
unbind(@UserId() userId: string) {
72105
return this.wechatService.unbind(userId);

src/auth/wechat/wechat.service.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,26 @@ import {
1111
UnauthorizedException,
1212
} from '@nestjs/common';
1313

14+
export interface WechatUserInfo {
15+
unionid: string;
16+
nickname: string;
17+
openid: string;
18+
}
19+
1420
@Injectable()
1521
export class WechatService extends SocialService {
1622
private readonly logger = new Logger(WechatService.name);
1723

1824
private readonly appId: string;
1925
private readonly appSecret: string;
20-
private readonly redirectUri: string;
2126
private readonly openAppId: string;
2227
private readonly openAppSecret: string;
28+
private readonly oldAppId: string;
29+
private readonly oldAppSecret: string;
30+
private readonly oldOpenAppId: string;
31+
private readonly oldOpenAppSecret: string;
32+
private readonly redirectUri: string;
33+
private readonly migrationRedirectUri: string;
2334

2435
constructor(
2536
private readonly configService: ConfigService,
@@ -34,10 +45,6 @@ export class WechatService extends SocialService {
3445
'OBB_WECHAT_APP_SECRET',
3546
'',
3647
);
37-
this.redirectUri = this.configService.get<string>(
38-
'OBB_WECHAT_REDIRECT_URI',
39-
'',
40-
);
4148
this.openAppId = this.configService.get<string>(
4249
'OBB_OPEN_WECHAT_APP_ID',
4350
'',
@@ -46,6 +53,27 @@ export class WechatService extends SocialService {
4653
'OBB_OPEN_WECHAT_APP_SECRET',
4754
'',
4855
);
56+
this.oldAppId = this.configService.get<string>('OBB_WECHAT_OLD_APP_ID', '');
57+
this.oldAppSecret = this.configService.get<string>(
58+
'OBB_WECHAT_OLD_APP_SECRET',
59+
'',
60+
);
61+
this.oldOpenAppId = this.configService.get<string>(
62+
'OBB_OPEN_WECHAT_OLD_APP_ID',
63+
'',
64+
);
65+
this.oldOpenAppSecret = this.configService.get<string>(
66+
'OBB_OPEN_WECHAT_OLD_APP_SECRET',
67+
'',
68+
);
69+
this.redirectUri = this.configService.get<string>(
70+
'OBB_WECHAT_REDIRECT_URI',
71+
'',
72+
);
73+
this.migrationRedirectUri = this.configService.get<string>(
74+
'OBB_WECHAT_MIGRATION_REDIRECT_URI',
75+
'',
76+
);
4977
}
5078

5179
available() {
@@ -173,6 +201,72 @@ export class WechatService extends SocialService {
173201
});
174202
}
175203

204+
migrationQrCode(type: 'new' | 'old' = 'new') {
205+
const isOld = type === 'old';
206+
const state = this.setState('open_weixin', type);
207+
this.cleanExpiresState();
208+
return {
209+
state,
210+
appId: isOld ? this.oldOpenAppId : this.openAppId,
211+
scope: 'snsapi_login',
212+
redirectUri: encodeURIComponent(this.migrationRedirectUri),
213+
};
214+
}
215+
216+
migrationAuthUrl(type: 'new' | 'old' = 'new'): string {
217+
const isOld = type === 'old';
218+
const state = this.setState('weixin', type);
219+
this.cleanExpiresState();
220+
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${isOld ? this.oldAppId : this.appId}&redirect_uri=${encodeURIComponent(this.migrationRedirectUri)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
221+
}
222+
223+
async migrationCallback(
224+
code: string,
225+
state: string,
226+
): Promise<WechatUserInfo> {
227+
const stateInfo = this.getState(state);
228+
if (!stateInfo) {
229+
throw new UnauthorizedException('Invalid state identifier');
230+
}
231+
const isOld = state.startsWith('old_');
232+
const isWeixin = stateInfo.type === 'weixin';
233+
const rawAppid = isOld ? this.oldAppId : this.appId;
234+
const rawAppsecret = isOld ? this.oldAppSecret : this.appSecret;
235+
const rawOpenAppid = isOld ? this.oldOpenAppId : this.openAppId;
236+
const rawOpenAppsecret = isOld ? this.oldOpenAppSecret : this.openAppSecret;
237+
const appId = isWeixin ? rawAppid : rawOpenAppid;
238+
const appSecret = isWeixin ? rawAppsecret : rawOpenAppsecret;
239+
const accessTokenResponse = await fetch(
240+
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`,
241+
);
242+
if (!accessTokenResponse.ok) {
243+
throw new UnauthorizedException('Failed to get WeChat access token');
244+
}
245+
const accessTokenData = await accessTokenResponse.json();
246+
247+
if (accessTokenData.errmsg) {
248+
throw new BadRequestException(accessTokenData.errmsg);
249+
}
250+
251+
const userDataResponse = await fetch(
252+
`https://api.weixin.qq.com/sns/userinfo?access_token=${accessTokenData.access_token}&openid=${accessTokenData.openid}&lang=zh_CN`,
253+
);
254+
if (!userDataResponse.ok) {
255+
throw new UnauthorizedException('Failed to get WeChat user info');
256+
}
257+
const userData = await userDataResponse.json();
258+
259+
if (userData.errmsg) {
260+
throw new BadRequestException(userData.errmsg);
261+
}
262+
263+
return userData;
264+
}
265+
266+
async migration(oldUnionid: string, newUnionid: string) {
267+
await this.userService.updateBinding(oldUnionid, newUnionid);
268+
}
269+
176270
async unbind(userId: string) {
177271
await this.userService.unbindByLoginType(userId, 'wechat');
178272
}

src/user/user.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,25 @@ export class UserService {
131131
await repo.remove(binding);
132132
}
133133

134+
async updateBinding(oldUnionid: string, newUnionid: string) {
135+
// Unbind the associated new account
136+
const existBinding = await this.userBindingRepository.findOne({
137+
where: { loginId: newUnionid },
138+
});
139+
if (existBinding) {
140+
await this.userBindingRepository.remove(existBinding);
141+
}
142+
// Bind to old account
143+
const binding = await this.userBindingRepository.findOne({
144+
where: { loginId: oldUnionid },
145+
});
146+
if (binding) {
147+
await this.userBindingRepository.update(binding.id, {
148+
loginId: newUnionid,
149+
});
150+
}
151+
}
152+
134153
async listBinding(userId: string) {
135154
const repo = this.userBindingRepository;
136155
return await repo.find({

0 commit comments

Comments
 (0)