From 485fe43140013054b348d0858a74432c919ab6a6 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Sat, 4 Apr 2026 21:15:32 +0800 Subject: [PATCH] feat(api-auth): add oauth strategies for github qq wechat --- apps/api/.env.example | 13 ++ apps/api/package.json | 2 + apps/api/src/auth/auth.controller.ts | 57 ++++++++- apps/api/src/auth/auth.module.ts | 7 +- .../src/auth/strategies/github.strategy.ts | 32 +++++ apps/api/src/auth/strategies/qq.strategy.ts | 33 +++++ .../src/auth/strategies/wechat.strategy.ts | 36 ++++++ pnpm-lock.yaml | 116 ++++++++++++++++++ 8 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/auth/strategies/github.strategy.ts create mode 100644 apps/api/src/auth/strategies/qq.strategy.ts create mode 100644 apps/api/src/auth/strategies/wechat.strategy.ts diff --git a/apps/api/.env.example b/apps/api/.env.example index 41b5593..69712b2 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -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" diff --git a/apps/api/package.json b/apps/api/package.json index e02e42d..a136df6 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -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", diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 55d2329..ecf7bd3 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -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 + }; + } } diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index f8b73ce..e108ac3 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -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 {} diff --git a/apps/api/src/auth/strategies/github.strategy.ts b/apps/api/src/auth/strategies/github.strategy.ts new file mode 100644 index 0000000..3b3133d --- /dev/null +++ b/apps/api/src/auth/strategies/github.strategy.ts @@ -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("OAUTH_GITHUB_CLIENT_ID") ?? "github-client-id", + clientSecret: + configService.get("OAUTH_GITHUB_CLIENT_SECRET") ?? "github-client-secret", + callbackURL: + configService.get("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 + }; + } +} diff --git a/apps/api/src/auth/strategies/qq.strategy.ts b/apps/api/src/auth/strategies/qq.strategy.ts new file mode 100644 index 0000000..5191151 --- /dev/null +++ b/apps/api/src/auth/strategies/qq.strategy.ts @@ -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("OAUTH_QQ_AUTH_URL") ?? "https://graph.qq.com/oauth2.0/authorize", + tokenURL: + configService.get("OAUTH_QQ_TOKEN_URL") ?? "https://graph.qq.com/oauth2.0/token", + clientID: configService.get("OAUTH_QQ_CLIENT_ID") ?? "qq-client-id", + clientSecret: configService.get("OAUTH_QQ_CLIENT_SECRET") ?? "qq-client-secret", + callbackURL: + configService.get("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 + }; + } +} diff --git a/apps/api/src/auth/strategies/wechat.strategy.ts b/apps/api/src/auth/strategies/wechat.strategy.ts new file mode 100644 index 0000000..1e4343b --- /dev/null +++ b/apps/api/src/auth/strategies/wechat.strategy.ts @@ -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("OAUTH_WECHAT_AUTH_URL") ?? + "https://open.weixin.qq.com/connect/qrconnect", + tokenURL: + configService.get("OAUTH_WECHAT_TOKEN_URL") ?? + "https://api.weixin.qq.com/sns/oauth2/access_token", + clientID: configService.get("OAUTH_WECHAT_CLIENT_ID") ?? "wechat-client-id", + clientSecret: + configService.get("OAUTH_WECHAT_CLIENT_SECRET") ?? "wechat-client-secret", + callbackURL: + configService.get("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 + }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c8607a..a4d24c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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):