diff --git a/Fabric.Backend.Center/package-lock.json b/Fabric.Backend.Center/package-lock.json index 4cbe145..57d26b6 100644 --- a/Fabric.Backend.Center/package-lock.json +++ b/Fabric.Backend.Center/package-lock.json @@ -12,6 +12,10 @@ "@nestjs/core": "^10.4.8", "@nestjs/platform-express": "^10.4.8", "@nestjs/typeorm": "^11.0.1", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "jsonwebtoken": "^9.0.3", "mysql2": "^3.22.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -19,6 +23,8 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.10.1", "@typescript-eslint/eslint-plugin": "^8.59.3", "@typescript-eslint/parser": "^8.59.3", @@ -562,6 +568,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -583,6 +596,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", @@ -592,6 +623,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", @@ -1144,6 +1181,15 @@ ], "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -1201,6 +1247,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1290,6 +1342,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1594,6 +1663,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2568,6 +2646,55 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2592,6 +2719,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.1.tgz", + "integrity": "sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2608,6 +2741,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3183,7 +3358,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3962,6 +4136,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/Fabric.Backend.Center/package.json b/Fabric.Backend.Center/package.json index c024aa2..ae5d0e5 100644 --- a/Fabric.Backend.Center/package.json +++ b/Fabric.Backend.Center/package.json @@ -16,6 +16,10 @@ "@nestjs/core": "^10.4.8", "@nestjs/platform-express": "^10.4.8", "@nestjs/typeorm": "^11.0.1", + "bcryptjs": "^3.0.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "jsonwebtoken": "^9.0.3", "mysql2": "^3.22.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -23,6 +27,8 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^22.10.1", "@typescript-eslint/eslint-plugin": "^8.59.3", "@typescript-eslint/parser": "^8.59.3", diff --git a/Fabric.Backend.Center/src/auth/auth.controller.ts b/Fabric.Backend.Center/src/auth/auth.controller.ts index fe4f07c..0a1a11f 100644 --- a/Fabric.Backend.Center/src/auth/auth.controller.ts +++ b/Fabric.Backend.Center/src/auth/auth.controller.ts @@ -1,19 +1,31 @@ import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { RegisterDto } from './dto.register.dto'; +import { LoginDto } from './dto.login.dto'; +import { RefreshDto } from './dto.refresh.dto'; +import { LogoutDto } from './dto.logout.dto'; @Controller('auth') export class AuthController { + constructor(private readonly authService: AuthService) {} + @Post('register') - register(@Body() body: Record) { - return { status: 'todo', action: 'register', received: body }; + register(@Body() body: RegisterDto) { + return this.authService.register(body); } @Post('login') - login(@Body() body: Record) { - return { status: 'todo', action: 'login', received: body }; + login(@Body() body: LoginDto) { + return this.authService.login(body); } @Post('refresh') - refresh(@Body() body: Record) { - return { status: 'todo', action: 'refresh', received: body }; + refresh(@Body() body: RefreshDto) { + return this.authService.refresh(body.refreshToken); + } + + @Post('logout') + logout(@Body() body: LogoutDto) { + return this.authService.logout(body.refreshToken); } } diff --git a/Fabric.Backend.Center/src/auth/auth.module.ts b/Fabric.Backend.Center/src/auth/auth.module.ts index adc6f34..344681c 100644 --- a/Fabric.Backend.Center/src/auth/auth.module.ts +++ b/Fabric.Backend.Center/src/auth/auth.module.ts @@ -1,7 +1,12 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { User } from '../entities/user.entity'; @Module({ + imports: [TypeOrmModule.forFeature([User])], controllers: [AuthController], + providers: [AuthService], }) export class AuthModule {} diff --git a/Fabric.Backend.Center/src/auth/auth.service.ts b/Fabric.Backend.Center/src/auth/auth.service.ts new file mode 100644 index 0000000..e228609 --- /dev/null +++ b/Fabric.Backend.Center/src/auth/auth.service.ts @@ -0,0 +1,140 @@ +import { + ConflictException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcryptjs'; +import * as jwt from 'jsonwebtoken'; +import { User } from '../entities/user.entity'; +import { RegisterDto } from './dto.register.dto'; +import { LoginDto } from './dto.login.dto'; + +function parseDurationToSeconds(input: string, fallbackSeconds: number): number { + const raw = input.trim(); + if (/^\d+$/.test(raw)) return Number(raw); + + const m = raw.match(/^(\d+)([smhd])$/i); + if (!m) return fallbackSeconds; + + const value = Number(m[1]); + const unit = m[2].toLowerCase(); + if (unit === 's') return value; + if (unit === 'm') return value * 60; + if (unit === 'h') return value * 3600; + if (unit === 'd') return value * 86400; + return fallbackSeconds; +} + +function signAccessToken(userId: string, email: string): string { + const secret = process.env.JWT_ACCESS_SECRET as string; + const expiresIn = parseDurationToSeconds(process.env.JWT_ACCESS_EXPIRES_IN ?? '15m', 900); + return jwt.sign({ sub: userId, email }, secret, { expiresIn }); +} + +function signRefreshToken(userId: string, email: string): string { + const secret = process.env.JWT_REFRESH_SECRET as string; + const expiresIn = parseDurationToSeconds(process.env.JWT_REFRESH_EXPIRES_IN ?? '30d', 2592000); + return jwt.sign({ sub: userId, email, typ: 'refresh' }, secret, { expiresIn }); +} + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private readonly userRepo: Repository, + ) {} + + async register(input: RegisterDto) { + const exists = await this.userRepo.findOne({ where: { email: input.email } }); + if (exists) { + throw new ConflictException('email already exists'); + } + + const passwordHash = await bcrypt.hash(input.password, 10); + const user = this.userRepo.create({ + email: input.email, + passwordHash, + refreshTokenHash: null, + }); + const saved = await this.userRepo.save(user); + + return { + id: saved.id, + email: saved.email, + createdAt: saved.createdAt, + }; + } + + async login(input: LoginDto) { + const user = await this.userRepo.findOne({ where: { email: input.email } }); + if (!user) throw new UnauthorizedException('invalid credentials'); + + const ok = await bcrypt.compare(input.password, user.passwordHash); + if (!ok) throw new UnauthorizedException('invalid credentials'); + + const accessToken = signAccessToken(user.id, user.email); + const refreshToken = signRefreshToken(user.id, user.email); + user.refreshTokenHash = await bcrypt.hash(refreshToken, 10); + await this.userRepo.save(user); + + return { + accessToken, + refreshToken, + tokenType: 'Bearer', + user: { + id: user.id, + email: user.email, + }, + }; + } + + async refresh(refreshToken: string) { + let payload: jwt.JwtPayload; + try { + payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as jwt.JwtPayload; + } catch { + throw new UnauthorizedException('invalid refresh token'); + } + + const userId = String(payload.sub ?? ''); + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user || !user.refreshTokenHash) { + throw new UnauthorizedException('invalid refresh token'); + } + + const tokenOk = await bcrypt.compare(refreshToken, user.refreshTokenHash); + if (!tokenOk) throw new UnauthorizedException('invalid refresh token'); + + const newAccessToken = signAccessToken(user.id, user.email); + const newRefreshToken = signRefreshToken(user.id, user.email); + user.refreshTokenHash = await bcrypt.hash(newRefreshToken, 10); + await this.userRepo.save(user); + + return { + accessToken: newAccessToken, + refreshToken: newRefreshToken, + tokenType: 'Bearer', + }; + } + + async logout(refreshToken: string) { + let payload: jwt.JwtPayload; + try { + payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as jwt.JwtPayload; + } catch { + return { status: 'ok' }; + } + + const userId = String(payload.sub ?? ''); + if (!userId) return { status: 'ok' }; + + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) return { status: 'ok' }; + + user.refreshTokenHash = null; + await this.userRepo.save(user); + return { status: 'ok' }; + } +} diff --git a/Fabric.Backend.Center/src/auth/dto.login.dto.ts b/Fabric.Backend.Center/src/auth/dto.login.dto.ts new file mode 100644 index 0000000..205887b --- /dev/null +++ b/Fabric.Backend.Center/src/auth/dto.login.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class LoginDto { + @IsEmail() + email!: string; + + @IsString() + @MinLength(8) + password!: string; +} diff --git a/Fabric.Backend.Center/src/auth/dto.logout.dto.ts b/Fabric.Backend.Center/src/auth/dto.logout.dto.ts new file mode 100644 index 0000000..aeab39a --- /dev/null +++ b/Fabric.Backend.Center/src/auth/dto.logout.dto.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator'; + +export class LogoutDto { + @IsString() + @MinLength(16) + refreshToken!: string; +} diff --git a/Fabric.Backend.Center/src/auth/dto.refresh.dto.ts b/Fabric.Backend.Center/src/auth/dto.refresh.dto.ts new file mode 100644 index 0000000..0e64bef --- /dev/null +++ b/Fabric.Backend.Center/src/auth/dto.refresh.dto.ts @@ -0,0 +1,7 @@ +import { IsString, MinLength } from 'class-validator'; + +export class RefreshDto { + @IsString() + @MinLength(16) + refreshToken!: string; +} diff --git a/Fabric.Backend.Center/src/auth/dto.register.dto.ts b/Fabric.Backend.Center/src/auth/dto.register.dto.ts new file mode 100644 index 0000000..296c0c7 --- /dev/null +++ b/Fabric.Backend.Center/src/auth/dto.register.dto.ts @@ -0,0 +1,10 @@ +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class RegisterDto { + @IsEmail() + email!: string; + + @IsString() + @MinLength(8) + password!: string; +} diff --git a/Fabric.Backend.Center/src/entities/user.entity.ts b/Fabric.Backend.Center/src/entities/user.entity.ts index 502e258..ee58e84 100644 --- a/Fabric.Backend.Center/src/entities/user.entity.ts +++ b/Fabric.Backend.Center/src/entities/user.entity.ts @@ -11,6 +11,9 @@ export class User { @Column() passwordHash!: string; + @Column({ type: 'varchar', length: 255, nullable: true }) + refreshTokenHash!: string | null; + @CreateDateColumn() createdAt!: Date; } diff --git a/Fabric.Backend.Center/src/main.ts b/Fabric.Backend.Center/src/main.ts index f54ea59..3b31a2b 100644 --- a/Fabric.Backend.Center/src/main.ts +++ b/Fabric.Backend.Center/src/main.ts @@ -1,4 +1,5 @@ import 'reflect-metadata'; +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; @@ -26,6 +27,13 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); const port = process.env.PORT ? Number(process.env.PORT) : 7001; await app.listen(port); console.log(`Fabric.Backend.Center listening on :${port}`); diff --git a/docs/TODO-backend-center-guild.md b/docs/TODO-backend-center-guild.md index 96dc2c6..6afa350 100644 --- a/docs/TODO-backend-center-guild.md +++ b/docs/TODO-backend-center-guild.md @@ -15,12 +15,12 @@ ## 1. Fabric.Backend.Center(Identity Hub) ### 1.1 Auth -- [ ] 用户注册(email/password) -- [ ] 用户登录(JWT access + refresh) -- [ ] token 刷新 -- [ ] 登出(refresh token 失效) -- [ ] 密码哈希(bcrypt/argon2) -- [ ] DTO + 参数校验 + 错误码规范 +- [x] 用户注册(email/password) +- [x] 用户登录(JWT access + refresh) +- [x] token 刷新 +- [x] 登出(refresh token 失效) +- [x] 密码哈希(bcrypt/argon2) +- [x] DTO + 参数校验 + 错误码规范 ### 1.2 Guild Node 注册与握手 - [ ] `POST /nodes/register` shared-secret 校验