feat: add official wechat session exchange boundary

This commit is contained in:
2026-09-18 15:27:02 +08:00
parent 5e71b89e95
commit de8f989083
3 changed files with 79 additions and 2 deletions

View File

@@ -1,9 +1,10 @@
import "reflect-metadata";
import { Controller, Get, Injectable, Module } from "@nestjs/common";
import { Body, Controller, Get, Injectable, Module, Post } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter } from "@nestjs/platform-fastify";
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
import { PrismaClient } from "@prisma/client";
import { WechatAuthService } from "./wechat-auth.js";
@Injectable()
export class PrismaService extends PrismaClient {
@@ -20,7 +21,35 @@ export class HealthController {
}
}
@Module({ controllers: [HealthController], providers: [PrismaService], exports: [PrismaService] })
@Controller("public")
export class PublicController {
@Get("brand")
brand(): { message: string } {
return { message: "public brand endpoint is ready" };
}
@Get("services")
services(): { message: string } {
return { message: "public services endpoint is ready" };
}
}
@Controller("customer/session")
export class CustomerSessionController {
constructor(private readonly wechatAuth: WechatAuthService) {}
@Post("wechat")
async wechatLogin(@Body() body: { code?: string }): Promise<{ openid: string }> {
const session = await this.wechatAuth.exchangeCode(body.code ?? "");
return { openid: session.openid };
}
}
@Module({
controllers: [HealthController, PublicController, CustomerSessionController],
providers: [PrismaService, WechatAuthService],
exports: [PrismaService, WechatAuthService],
})
export class AppModule {}
export async function createApp(): Promise<NestFastifyApplication> {

View File

@@ -0,0 +1,44 @@
import { Injectable, ServiceUnavailableException, UnauthorizedException } from "@nestjs/common";
interface WechatSessionResponse {
openid?: string;
session_key?: string;
unionid?: string;
errcode?: number;
errmsg?: string;
}
export interface WechatSession {
openid: string;
sessionKey: string;
unionid?: string;
}
@Injectable()
export class WechatAuthService {
async exchangeCode(code: string): Promise<WechatSession> {
if (!code.trim()) throw new UnauthorizedException("微信登录 code 不能为空");
const appId = process.env.WECHAT_APP_ID;
const appSecret = process.env.WECHAT_APP_SECRET;
if (!appId || !appSecret) throw new ServiceUnavailableException("微信登录尚未配置");
const query = new URLSearchParams({
appid: appId,
secret: appSecret,
js_code: code,
grant_type: "authorization_code",
});
const response = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?${query.toString()}`,
);
if (!response.ok) throw new ServiceUnavailableException("微信登录服务暂时不可用");
const result = (await response.json()) as WechatSessionResponse;
if (!result.openid || !result.session_key)
throw new UnauthorizedException(result.errmsg ?? "微信登录失败");
return {
openid: result.openid,
sessionKey: result.session_key,
...(result.unionid ? { unionid: result.unionid } : {}),
};
}
}