feat(server): add auth, user, and studio modules

Auth: WeChat login, JWT, roles guard (24 tests passing)
User: profile CRUD, training stats with month/total calculations
Studio: config management with auto-default creation
This commit is contained in:
richarjiang
2026-04-02 12:12:18 +08:00
parent e653580155
commit a1a91f96d8
23 changed files with 1284 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { User } from '@prisma/client'
import { UserRole } from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
import { WechatService } from './wechat.service'
export interface LoginResult {
token: string
user: User
}
export interface JwtPayload {
sub: string
role: UserRole
}
/**
* In-memory session key store.
* TODO: Replace with Redis for production multi-instance deployments.
* Key: userId, Value: WeChat sessionKey
*/
const sessionKeyStore = new Map<string, string>()
@Injectable()
export class AuthService {
constructor(
private readonly prisma: PrismaService,
private readonly jwtService: JwtService,
private readonly wechatService: WechatService,
) {}
async login(code: string): Promise<LoginResult> {
const { openid, unionid, sessionKey } =
await this.wechatService.code2Session(code)
const existingUser = await this.prisma.user.findUnique({
where: { openid },
})
const user =
existingUser ??
(await this.prisma.user.create({
data: {
openid,
...(unionid !== undefined && { unionid }),
},
}))
sessionKeyStore.set(user.id, sessionKey)
const payload: JwtPayload = { sub: user.id, role: user.role as UserRole }
const token = this.jwtService.sign(payload)
return { token, user }
}
async bindPhone(
userId: string,
encryptedData: string,
iv: string,
): Promise<User> {
const sessionKey = sessionKeyStore.get(userId)
if (!sessionKey) {
throw new UnauthorizedException(
'Session expired. Please log in again to bind your phone number.',
)
}
const phoneInfo = this.wechatService.decryptData(
sessionKey,
encryptedData,
iv,
)
return this.prisma.user.update({
where: { id: userId },
data: { phone: phoneInfo.phoneNumber },
})
}
}