Merge pull request #5 from Yaosanqi137/feature/p2-api-auth
Feature/p2 api auth
This commit is contained in:
+19
-1
@@ -1 +1,19 @@
|
|||||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/todolist?schema=public"
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/todolist?schema=public"
|
||||||
|
AUTH_ACCESS_SECRET="dev-access-secret"
|
||||||
|
AUTH_ACCESS_EXPIRES_IN_SECONDS="900"
|
||||||
|
AUTH_REFRESH_EXPIRES_IN_SECONDS="2592000"
|
||||||
|
AUTH_EMAIL_CODE_TTL_SECONDS="300"
|
||||||
|
AUTH_TOTP_ISSUER="TodoList"
|
||||||
|
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"
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
/generated/prisma
|
/generated/prisma
|
||||||
|
dist
|
||||||
|
prisma.config.js
|
||||||
|
prisma.config.js.map
|
||||||
|
|||||||
+24
-3
@@ -5,16 +5,37 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:format": "prisma format",
|
"prisma:format": "prisma format",
|
||||||
"prisma:validate": "prisma validate"
|
"prisma:validate": "prisma validate",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
|
"test": "echo api tests pending"
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.5.2",
|
"@types/node": "^25.5.2",
|
||||||
|
"@types/passport-github2": "^1.2.9",
|
||||||
|
"@types/passport-oauth2": "^1.8.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"prisma": "^7.6.0"
|
"prisma": "^7.6.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^7.6.0"
|
"@nestjs/common": "^11.1.18",
|
||||||
|
"@nestjs/config": "^4.0.3",
|
||||||
|
"@nestjs/core": "^11.1.18",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/platform-express": "^11.1.18",
|
||||||
|
"@otplib/preset-default": "^12.0.1",
|
||||||
|
"@prisma/client": "^7.6.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
|
"otplib": "^13.4.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { ConfigModule } from "@nestjs/config";
|
||||||
|
import { AuthModule } from "./auth/auth.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: ".env"
|
||||||
|
}),
|
||||||
|
AuthModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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";
|
||||||
|
import { SendEmailCodeDto } from "./dto/send-email-code.dto";
|
||||||
|
import { TwoFactorEnrollDto } from "./dto/two-factor-enroll.dto";
|
||||||
|
import { TwoFactorVerifyDto } from "./dto/two-factor-verify.dto";
|
||||||
|
|
||||||
|
@Controller("auth")
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post("email/send-code")
|
||||||
|
async sendEmailCode(
|
||||||
|
@Body() body: SendEmailCodeDto
|
||||||
|
): Promise<{ success: boolean; expiresInSeconds: number; debugCode: string }> {
|
||||||
|
return this.authService.sendEmailCode(body.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("email/login")
|
||||||
|
async loginWithEmailCode(@Body() body: EmailLoginDto): Promise<{
|
||||||
|
accessToken: string;
|
||||||
|
tokenType: "Bearer";
|
||||||
|
expiresInSeconds: number;
|
||||||
|
refreshToken: string;
|
||||||
|
refreshExpiresInSeconds: number;
|
||||||
|
user: { id: string; email: string };
|
||||||
|
}> {
|
||||||
|
return this.authService.loginWithEmailCode(body.email, body.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("token/refresh")
|
||||||
|
async refreshTokens(@Body() body: RefreshTokenDto): Promise<{
|
||||||
|
accessToken: string;
|
||||||
|
tokenType: "Bearer";
|
||||||
|
expiresInSeconds: number;
|
||||||
|
refreshToken: string;
|
||||||
|
refreshExpiresInSeconds: number;
|
||||||
|
user: { id: string; email: string };
|
||||||
|
}> {
|
||||||
|
return this.authService.refreshTokens(body.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("token/revoke")
|
||||||
|
async revokeRefreshToken(@Body() body: RefreshTokenDto): Promise<{ success: boolean }> {
|
||||||
|
return this.authService.revokeRefreshToken(body.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("2fa/enroll")
|
||||||
|
async enrollTwoFactor(@Body() body: TwoFactorEnrollDto): Promise<{
|
||||||
|
userId: string;
|
||||||
|
secret: string;
|
||||||
|
otpauthUrl: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}> {
|
||||||
|
return this.authService.enrollTwoFactor(body.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("2fa/verify")
|
||||||
|
async verifyTwoFactor(
|
||||||
|
@Body() body: TwoFactorVerifyDto
|
||||||
|
): Promise<{ success: boolean; enabled: boolean }> {
|
||||||
|
return this.authService.verifyTwoFactor(body.email, body.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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) => {
|
||||||
|
const expiresInSeconds = Number(configService.get("AUTH_ACCESS_EXPIRES_IN_SECONDS") ?? 900);
|
||||||
|
|
||||||
|
return {
|
||||||
|
secret: configService.get<string>("AUTH_ACCESS_SECRET") ?? "dev-access-secret",
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: expiresInSeconds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, GithubStrategy, QqStrategy, WechatStrategy]
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
import { JwtService } from "@nestjs/jwt";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { authenticator } from "@otplib/preset-default";
|
||||||
|
|
||||||
|
type EmailCodeEntry = {
|
||||||
|
code: string;
|
||||||
|
expiresAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuthUser = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RefreshTokenEntry = {
|
||||||
|
userId: string;
|
||||||
|
expiresAt: number;
|
||||||
|
revokedAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TwoFactorEntry = {
|
||||||
|
secret: string;
|
||||||
|
enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AuthTokenResult = {
|
||||||
|
accessToken: string;
|
||||||
|
tokenType: "Bearer";
|
||||||
|
expiresInSeconds: number;
|
||||||
|
refreshToken: string;
|
||||||
|
refreshExpiresInSeconds: number;
|
||||||
|
user: AuthUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
private readonly emailCodeStore = new Map<string, EmailCodeEntry>();
|
||||||
|
private readonly userStoreByEmail = new Map<string, AuthUser>();
|
||||||
|
private readonly userStoreById = new Map<string, AuthUser>();
|
||||||
|
private readonly refreshTokenStore = new Map<string, RefreshTokenEntry>();
|
||||||
|
private readonly twoFactorStore = new Map<string, TwoFactorEntry>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly jwtService: JwtService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sendEmailCode(
|
||||||
|
email: string
|
||||||
|
): Promise<{ success: boolean; expiresInSeconds: number; debugCode: string }> {
|
||||||
|
const ttlSeconds = Number(this.configService.get("AUTH_EMAIL_CODE_TTL_SECONDS") ?? 300);
|
||||||
|
const code = this.generateCode();
|
||||||
|
const expiresAt = Date.now() + ttlSeconds * 1000;
|
||||||
|
|
||||||
|
this.emailCodeStore.set(email.toLowerCase(), { code, expiresAt });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
expiresInSeconds: ttlSeconds,
|
||||||
|
debugCode: code
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginWithEmailCode(email: string, code: string): Promise<AuthTokenResult> {
|
||||||
|
const lowerEmail = email.toLowerCase();
|
||||||
|
const codeEntry = this.emailCodeStore.get(lowerEmail);
|
||||||
|
|
||||||
|
if (!codeEntry) {
|
||||||
|
throw new UnauthorizedException("验证码不存在或已失效");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeEntry.expiresAt < Date.now()) {
|
||||||
|
this.emailCodeStore.delete(lowerEmail);
|
||||||
|
throw new UnauthorizedException("验证码已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeEntry.code !== code) {
|
||||||
|
throw new UnauthorizedException("验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emailCodeStore.delete(lowerEmail);
|
||||||
|
|
||||||
|
const user = this.getOrCreateUser(lowerEmail);
|
||||||
|
return this.issueTokens(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshTokens(refreshToken: string): Promise<AuthTokenResult> {
|
||||||
|
const entry = this.refreshTokenStore.get(refreshToken);
|
||||||
|
if (!entry) {
|
||||||
|
throw new UnauthorizedException("刷新令牌不存在");
|
||||||
|
}
|
||||||
|
if (entry.revokedAt) {
|
||||||
|
throw new UnauthorizedException("刷新令牌已注销");
|
||||||
|
}
|
||||||
|
if (entry.expiresAt < Date.now()) {
|
||||||
|
this.refreshTokenStore.delete(refreshToken);
|
||||||
|
throw new UnauthorizedException("刷新令牌已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = this.userStoreById.get(entry.userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.revokedAt = Date.now();
|
||||||
|
return this.issueTokens(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeRefreshToken(refreshToken: string): Promise<{ success: boolean }> {
|
||||||
|
const entry = this.refreshTokenStore.get(refreshToken);
|
||||||
|
if (!entry) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.revokedAt = Date.now();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
async enrollTwoFactor(
|
||||||
|
email: string
|
||||||
|
): Promise<{ userId: string; secret: string; otpauthUrl: string; enabled: boolean }> {
|
||||||
|
const user = this.getOrCreateUser(email.toLowerCase());
|
||||||
|
const secret = authenticator.generateSecret();
|
||||||
|
const issuer = this.configService.get<string>("AUTH_TOTP_ISSUER") ?? "TodoList";
|
||||||
|
const otpauthUrl = authenticator.keyuri(user.email, issuer, secret);
|
||||||
|
|
||||||
|
this.twoFactorStore.set(user.id, {
|
||||||
|
secret,
|
||||||
|
enabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.id,
|
||||||
|
secret,
|
||||||
|
otpauthUrl,
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTwoFactor(
|
||||||
|
email: string,
|
||||||
|
token: string
|
||||||
|
): Promise<{ success: boolean; enabled: boolean }> {
|
||||||
|
const user = this.getOrCreateUser(email.toLowerCase());
|
||||||
|
const entry = this.twoFactorStore.get(user.id);
|
||||||
|
if (!entry) {
|
||||||
|
throw new UnauthorizedException("尚未启用两步验证");
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = authenticator.check(token, entry.secret);
|
||||||
|
if (!valid) {
|
||||||
|
throw new UnauthorizedException("两步验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.enabled = true;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOrCreateUser(email: string): AuthUser {
|
||||||
|
const existingUser = this.userStoreByEmail.get(email);
|
||||||
|
if (existingUser) {
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUser = {
|
||||||
|
id: randomUUID(),
|
||||||
|
email
|
||||||
|
};
|
||||||
|
this.userStoreByEmail.set(email, newUser);
|
||||||
|
this.userStoreById.set(newUser.id, newUser);
|
||||||
|
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCode(): string {
|
||||||
|
return String(Math.floor(100000 + Math.random() * 900000));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async issueTokens(user: AuthUser): Promise<AuthTokenResult> {
|
||||||
|
const accessExpiresInSeconds = Number(
|
||||||
|
this.configService.get("AUTH_ACCESS_EXPIRES_IN_SECONDS") ?? 900
|
||||||
|
);
|
||||||
|
const refreshExpiresInSeconds = Number(
|
||||||
|
this.configService.get("AUTH_REFRESH_EXPIRES_IN_SECONDS") ?? 2592000
|
||||||
|
);
|
||||||
|
const accessToken = await this.jwtService.signAsync({
|
||||||
|
sub: user.id,
|
||||||
|
email: user.email
|
||||||
|
});
|
||||||
|
const refreshToken = `${randomUUID()}${randomUUID()}`;
|
||||||
|
|
||||||
|
this.refreshTokenStore.set(refreshToken, {
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: Date.now() + refreshExpiresInSeconds * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
tokenType: "Bearer",
|
||||||
|
expiresInSeconds: accessExpiresInSeconds,
|
||||||
|
refreshToken,
|
||||||
|
refreshExpiresInSeconds,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { IsEmail, IsString, Length, Matches } from "class-validator";
|
||||||
|
|
||||||
|
export class EmailLoginDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 6)
|
||||||
|
@Matches(/^\d{6}$/)
|
||||||
|
code!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class RefreshTokenDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(20)
|
||||||
|
refreshToken!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class SendEmailCodeDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsEmail } from "class-validator";
|
||||||
|
|
||||||
|
export class TwoFactorEnrollDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { IsEmail, IsString, Length, Matches } from "class-validator";
|
||||||
|
|
||||||
|
export class TwoFactorVerifyDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 6)
|
||||||
|
@Matches(/^\d{6}$/)
|
||||||
|
token!: string;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import "reflect-metadata";
|
||||||
|
import { ValidationPipe } from "@nestjs/common";
|
||||||
|
import { NestFactory } from "@nestjs/core";
|
||||||
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
|
async function bootstrap(): Promise<void> {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
transform: true,
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "../../packages/tsconfig/nest-app.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
Generated
+2326
-9
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user