diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index f0f5804..a25de64 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -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) { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index ae42873..1037adc 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -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()) { diff --git a/src/auth/dto.ts b/src/auth/dto.ts index 53b184b..ebc251e 100644 --- a/src/auth/dto.ts +++ b/src/auth/dto.ts @@ -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()