feat(api-auth): add oauth strategies for github qq wechat

This commit is contained in:
2026-04-04 21:15:32 +08:00
parent 074942fab4
commit 485fe43140
8 changed files with 294 additions and 2 deletions
+13
View File
@@ -3,3 +3,16 @@ AUTH_ACCESS_SECRET="dev-access-secret"
AUTH_ACCESS_EXPIRES_IN_SECONDS="900"
AUTH_REFRESH_EXPIRES_IN_SECONDS="2592000"
AUTH_EMAIL_CODE_TTL_SECONDS="300"
OAUTH_GITHUB_CLIENT_ID="github-client-id"
OAUTH_GITHUB_CLIENT_SECRET="github-client-secret"
OAUTH_GITHUB_CALLBACK_URL="http://localhost:3000/auth/oauth/github/callback"
OAUTH_QQ_CLIENT_ID="qq-client-id"
OAUTH_QQ_CLIENT_SECRET="qq-client-secret"
OAUTH_QQ_CALLBACK_URL="http://localhost:3000/auth/oauth/qq/callback"
OAUTH_QQ_AUTH_URL="https://graph.qq.com/oauth2.0/authorize"
OAUTH_QQ_TOKEN_URL="https://graph.qq.com/oauth2.0/token"
OAUTH_WECHAT_CLIENT_ID="wechat-client-id"
OAUTH_WECHAT_CLIENT_SECRET="wechat-client-secret"
OAUTH_WECHAT_CALLBACK_URL="http://localhost:3000/auth/oauth/wechat/callback"
OAUTH_WECHAT_AUTH_URL="https://open.weixin.qq.com/connect/qrconnect"
OAUTH_WECHAT_TOKEN_URL="https://api.weixin.qq.com/sns/oauth2/access_token"
+2
View File
@@ -15,6 +15,8 @@
"license": "GPL-3.0-or-later",
"devDependencies": {
"@types/node": "^25.5.2",
"@types/passport-github2": "^1.2.9",
"@types/passport-oauth2": "^1.8.0",
"dotenv": "^16.6.1",
"prisma": "^7.6.0",
"ts-node": "^10.9.2",
+56 -1
View File
@@ -1,4 +1,5 @@
import { Body, Controller, Post } from "@nestjs/common";
import { Body, Controller, Get, Post, Req, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { AuthService } from "./auth.service";
import { EmailLoginDto } from "./dto/email-login.dto";
import { RefreshTokenDto } from "./dto/refresh-token.dto";
@@ -43,4 +44,58 @@ export class AuthController {
async revokeRefreshToken(@Body() body: RefreshTokenDto): Promise<{ success: boolean }> {
return this.authService.revokeRefreshToken(body.refreshToken);
}
@Get("oauth/github")
@UseGuards(AuthGuard("github"))
githubLogin(): void {}
@Get("oauth/github/callback")
@UseGuards(AuthGuard("github"))
githubCallback(@Req() req: { user: unknown }): {
success: boolean;
provider: "github";
profile: unknown;
} {
return {
success: true,
provider: "github",
profile: req.user
};
}
@Get("oauth/qq")
@UseGuards(AuthGuard("qq"))
qqLogin(): void {}
@Get("oauth/qq/callback")
@UseGuards(AuthGuard("qq"))
qqCallback(@Req() req: { user: unknown }): {
success: boolean;
provider: "qq";
profile: unknown;
} {
return {
success: true,
provider: "qq",
profile: req.user
};
}
@Get("oauth/wechat")
@UseGuards(AuthGuard("wechat"))
wechatLogin(): void {}
@Get("oauth/wechat/callback")
@UseGuards(AuthGuard("wechat"))
wechatCallback(@Req() req: { user: unknown }): {
success: boolean;
provider: "wechat";
profile: unknown;
} {
return {
success: true,
provider: "wechat",
profile: req.user
};
}
}
+6 -1
View File
@@ -1,12 +1,17 @@
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { GithubStrategy } from "./strategies/github.strategy";
import { QqStrategy } from "./strategies/qq.strategy";
import { WechatStrategy } from "./strategies/wechat.strategy";
@Module({
imports: [
ConfigModule,
PassportModule.register({ session: false }),
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
@@ -22,6 +27,6 @@ import { AuthService } from "./auth.service";
})
],
controllers: [AuthController],
providers: [AuthService]
providers: [AuthService, GithubStrategy, QqStrategy, WechatStrategy]
})
export class AuthModule {}
@@ -0,0 +1,32 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Profile, Strategy } from "passport-github2";
@Injectable()
export class GithubStrategy extends PassportStrategy(Strategy, "github") {
constructor(configService: ConfigService) {
super({
clientID: configService.get<string>("OAUTH_GITHUB_CLIENT_ID") ?? "github-client-id",
clientSecret:
configService.get<string>("OAUTH_GITHUB_CLIENT_SECRET") ?? "github-client-secret",
callbackURL:
configService.get<string>("OAUTH_GITHUB_CALLBACK_URL") ??
"http://localhost:3000/auth/oauth/github/callback",
scope: ["user:email"]
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: Profile
): Promise<{ provider: "github"; accessToken: string; refreshToken: string; profile: Profile }> {
return {
provider: "github",
accessToken,
refreshToken,
profile
};
}
}
@@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-oauth2";
@Injectable()
export class QqStrategy extends PassportStrategy(Strategy, "qq") {
constructor(configService: ConfigService) {
super({
authorizationURL:
configService.get<string>("OAUTH_QQ_AUTH_URL") ?? "https://graph.qq.com/oauth2.0/authorize",
tokenURL:
configService.get<string>("OAUTH_QQ_TOKEN_URL") ?? "https://graph.qq.com/oauth2.0/token",
clientID: configService.get<string>("OAUTH_QQ_CLIENT_ID") ?? "qq-client-id",
clientSecret: configService.get<string>("OAUTH_QQ_CLIENT_SECRET") ?? "qq-client-secret",
callbackURL:
configService.get<string>("OAUTH_QQ_CALLBACK_URL") ??
"http://localhost:3000/auth/oauth/qq/callback",
scope: ["get_user_info"]
});
}
async validate(
accessToken: string,
refreshToken: string
): Promise<{ provider: "qq"; accessToken: string; refreshToken: string }> {
return {
provider: "qq",
accessToken,
refreshToken
};
}
}
@@ -0,0 +1,36 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-oauth2";
@Injectable()
export class WechatStrategy extends PassportStrategy(Strategy, "wechat") {
constructor(configService: ConfigService) {
super({
authorizationURL:
configService.get<string>("OAUTH_WECHAT_AUTH_URL") ??
"https://open.weixin.qq.com/connect/qrconnect",
tokenURL:
configService.get<string>("OAUTH_WECHAT_TOKEN_URL") ??
"https://api.weixin.qq.com/sns/oauth2/access_token",
clientID: configService.get<string>("OAUTH_WECHAT_CLIENT_ID") ?? "wechat-client-id",
clientSecret:
configService.get<string>("OAUTH_WECHAT_CLIENT_SECRET") ?? "wechat-client-secret",
callbackURL:
configService.get<string>("OAUTH_WECHAT_CALLBACK_URL") ??
"http://localhost:3000/auth/oauth/wechat/callback",
scope: ["snsapi_login"]
});
}
async validate(
accessToken: string,
refreshToken: string
): Promise<{ provider: "wechat"; accessToken: string; refreshToken: string }> {
return {
provider: "wechat",
accessToken,
refreshToken
};
}
}
+116
View File
@@ -43,6 +43,9 @@ importers:
"@nestjs/jwt":
specifier: ^11.0.2
version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))
"@nestjs/passport":
specifier: ^11.0.5
version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
"@nestjs/platform-express":
specifier: ^11.1.18
version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)
@@ -55,6 +58,15 @@ importers:
class-validator:
specifier: ^0.15.1
version: 0.15.1
passport:
specifier: ^0.7.0
version: 0.7.0
passport-github2:
specifier: ^0.1.12
version: 0.1.12
passport-oauth2:
specifier: ^1.8.0
version: 1.8.0
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
@@ -320,6 +332,15 @@ packages:
peerDependencies:
"@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
"@nestjs/passport@11.0.5":
resolution:
{
integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==
}
peerDependencies:
"@nestjs/common": ^10.0.0 || ^11.0.0
passport: ^0.5.0 || ^0.6.0 || ^0.7.0
"@nestjs/platform-express@11.1.18":
resolution:
{
@@ -787,6 +808,13 @@ packages:
}
engines: { node: 18 || 20 || >=22 }
base64url@3.0.1:
resolution:
{
integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
}
engines: { node: ">=6.0.0" }
better-result@2.7.0:
resolution:
{
@@ -2002,6 +2030,12 @@ packages:
engines: { node: ">=18" }
hasBin: true
oauth@0.10.2:
resolution:
{
integrity: sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==
}
object-assign@4.1.1:
resolution:
{
@@ -2070,6 +2104,34 @@ packages:
}
engines: { node: ">= 0.8" }
passport-github2@0.1.12:
resolution:
{
integrity: sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==
}
engines: { node: ">= 0.8.0" }
passport-oauth2@1.8.0:
resolution:
{
integrity: sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==
}
engines: { node: ">= 0.4.0" }
passport-strategy@1.0.0:
resolution:
{
integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==
}
engines: { node: ">= 0.4.0" }
passport@0.7.0:
resolution:
{
integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==
}
engines: { node: ">= 0.4.0" }
path-exists@4.0.0:
resolution:
{
@@ -2109,6 +2171,12 @@ packages:
integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
}
pause@0.0.1:
resolution:
{
integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
}
perfect-debounce@1.0.0:
resolution:
{
@@ -2685,6 +2753,12 @@ packages:
engines: { node: ">=14.17" }
hasBin: true
uid2@0.0.4:
resolution:
{
integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==
}
uid@2.0.2:
resolution:
{
@@ -2724,6 +2798,13 @@ packages:
integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
}
utils-merge@1.0.1:
resolution:
{
integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
}
engines: { node: ">= 0.4.0" }
v8-compile-cache-lib@3.0.1:
resolution:
{
@@ -2951,6 +3032,11 @@ snapshots:
"@types/jsonwebtoken": 9.0.10
jsonwebtoken: 9.0.3
"@nestjs/passport@11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)":
dependencies:
"@nestjs/common": 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
passport: 0.7.0
"@nestjs/platform-express@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)":
dependencies:
"@nestjs/common": 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -3223,6 +3309,8 @@ snapshots:
balanced-match@4.0.4: {}
base64url@3.0.1: {}
better-result@2.7.0:
dependencies:
"@clack/prompts": 0.11.0
@@ -3919,6 +4007,8 @@ snapshots:
pathe: 2.0.3
tinyexec: 1.0.4
oauth@0.10.2: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -3956,6 +4046,26 @@ snapshots:
parseurl@1.3.3: {}
passport-github2@0.1.12:
dependencies:
passport-oauth2: 1.8.0
passport-oauth2@1.8.0:
dependencies:
base64url: 3.0.1
oauth: 0.10.2
passport-strategy: 1.0.0
uid2: 0.0.4
utils-merge: 1.0.1
passport-strategy@1.0.0: {}
passport@0.7.0:
dependencies:
passport-strategy: 1.0.0
pause: 0.0.1
utils-merge: 1.0.1
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -3968,6 +4078,8 @@ snapshots:
pathe@2.0.3: {}
pause@0.0.1: {}
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {}
@@ -4318,6 +4430,8 @@ snapshots:
typescript@5.9.3: {}
uid2@0.0.4: {}
uid@2.0.2:
dependencies:
"@lukeed/csprng": 1.1.0
@@ -4334,6 +4448,8 @@ snapshots:
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
v8-compile-cache-lib@3.0.1: {}
valibot@1.2.0(typescript@5.9.3):