feat(auth): 注册接口 POST /auth/register

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:29:11 +08:00
parent 537ec4fd91
commit 53dbc38bab
3 changed files with 60 additions and 5 deletions

View File

@ -1,7 +1,7 @@
import { Body, Controller, Get, Post, Req, UnauthorizedException } from '@nestjs/common';
import type { Request } from 'express';
import { AuthService } from './auth.service.js';
import { LoginDto, RefreshDto } from './dto.js';
import { LoginDto, RefreshDto, RegisterDto } from './dto.js';
import { Public } from './decorators.js';
import { getRequestContext } from '../common/context/request-context.js';
@ -15,6 +15,12 @@ export class AuthController {
return this.auth.login(dto, req.ip, req.headers['user-agent']);
}
@Public()
@Post('register')
register(@Body() dto: RegisterDto) {
return this.auth.register(dto);
}
@Public()
@Post('refresh')
refresh(@Body() dto: RefreshDto) {

View File

@ -1,10 +1,10 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { BadRequestException, ConflictException, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import bcrypt from 'bcryptjs';
import { createHash, randomUUID } from 'node:crypto';
import { PrismaService } from '../prisma/prisma.service.js';
import { RedisService } from '../common/redis/redis.service.js';
import { LoginDto, RefreshDto } from './dto.js';
import { LoginDto, RefreshDto, RegisterDto } from './dto.js';
import { resolveDataScope, resolveVisibleOrgs } from '../common/data-scope.js';
import type { DataScope } from '../common/enums.js';
import type { RequestContext } from '../common/context/request-context.js';
@ -118,6 +118,28 @@ export class AuthService {
return this.issue(dto.tenantCode, user.id, user.username, user.tokenVersion);
}
/** 注册:在指定租户下创建普通用户(无角色,状态启用)。 */
async register(dto: RegisterDto): Promise<{ id: string; username: string }> {
const tenant = await this.prisma.client.tenant.findUnique({ where: { code: dto.tenantCode } });
if (!tenant || tenant.status !== 'enabled') throw new BadRequestException('租户不存在或已停用');
const exists = await this.prisma.client.user.findFirst({
where: { tenantCode: dto.tenantCode, username: dto.username, deletedAt: null },
});
if (exists) throw new ConflictException('用户名已存在');
const passwordHash = await bcrypt.hash(dto.password, 10);
const user = await this.prisma.client.user.create({
data: {
tenantCode: dto.tenantCode,
username: dto.username,
passwordHash,
email: dto.email ?? null,
phone: dto.phone ?? null,
status: 'enabled',
},
});
return { id: user.id, username: user.username };
}
async refresh(dto: RefreshDto): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
const record = await this.prisma.client.refreshToken.findUnique({ where: { jti: dto.refreshToken } });
if (!record || record.revokedAt || record.expireAt.getTime() < Date.now()) {

View File

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class LoginDto {
@ApiProperty({ description: '租户编码' })
@ -18,6 +18,33 @@ export class LoginDto {
password!: string;
}
export class RegisterDto {
@ApiProperty({ description: '租户编码' })
@IsString()
@IsNotEmpty()
tenantCode!: string;
@ApiProperty({ description: '用户名' })
@IsString()
@IsNotEmpty()
username!: string;
@ApiProperty({ description: '密码' })
@IsString()
@IsNotEmpty()
password!: string;
@ApiPropertyOptional({ description: '邮箱' })
@IsOptional()
@IsString()
email?: string;
@ApiPropertyOptional({ description: '手机号' })
@IsOptional()
@IsString()
phone?: string;
}
export class RefreshDto {
@ApiProperty({ description: '刷新令牌' })
@IsString()