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:
82
packages/server/src/auth/auth.service.ts
Normal file
82
packages/server/src/auth/auth.service.ts
Normal 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 },
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user