diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 960eae2..f86eaf2 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,8 +1,13 @@ -import { Body, Controller, Get, Headers, Param, Post, UnauthorizedException } from '@nestjs/common'; +import { Body, Controller, Get, Headers, Param, Patch, Post, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; import { LoginDto } from './dto.login.dto'; import { RefreshDto } from './dto.refresh.dto'; import { LogoutDto } from './dto.logout.dto'; +import { UpdateMeDto } from './dto.update-me.dto'; + +function bearer(authorization?: string): string { + return authorization?.startsWith('Bearer ') ? authorization.slice(7) : ''; +} @Controller('auth') export class AuthController { @@ -23,6 +28,20 @@ export class AuthController { return this.authService.logout(body.refreshToken); } + @Get('me') + me(@Headers('authorization') authorization?: string) { + const token = bearer(authorization); + if (!token) throw new UnauthorizedException('missing bearer token'); + return this.authService.getMe(token); + } + + @Patch('me') + updateMe(@Headers('authorization') authorization: string | undefined, @Body() body: UpdateMeDto) { + const token = bearer(authorization); + if (!token) throw new UnauthorizedException('missing bearer token'); + return this.authService.updateMe(token, body.name); + } + @Get('me/guilds') meGuilds(@Headers('authorization') authorization?: string) { const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : ''; diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7b989b7..0cacc51 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -91,6 +91,7 @@ export class AuthService { const passwordHash = await bcrypt.hash(input.password, 10); const user = this.userRepo.create({ email: input.email, + name: input.email, passwordHash, refreshTokenHash: null, }); @@ -107,6 +108,7 @@ export class AuthService { return { id: saved.id, email: saved.email, + name: saved.name ?? saved.email, createdAt: saved.createdAt, }; } @@ -141,12 +143,44 @@ export class AuthService { user: { id: user.id, email: user.email, + name: user.name ?? user.email, }, guilds, guildAccessTokens, }; } + async getMe(accessToken: string) { + const payload = this.verifyCenterAccessToken(accessToken); + const userId = String(payload.sub ?? ''); + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new UnauthorizedException('user not found'); + return { id: user.id, email: user.email, name: user.name ?? user.email }; + } + + async updateMe(accessToken: string, name: string) { + const payload = this.verifyCenterAccessToken(accessToken); + const userId = String(payload.sub ?? ''); + const user = await this.userRepo.findOne({ where: { id: userId } }); + if (!user) throw new UnauthorizedException('user not found'); + + const trimmed = name.trim(); + if (!trimmed) throw new ConflictException('name must not be empty'); + + user.name = trimmed; + await this.userRepo.save(user); + + await this.audit.write({ + action: 'auth.update_profile', + actorId: user.id, + targetType: 'user', + targetId: user.id, + detail: JSON.stringify({ name: trimmed }), + }); + + return { id: user.id, email: user.email, name: user.name }; + } + async listMyGuilds(accessToken: string) { const payload = this.verifyCenterAccessToken(accessToken); const userId = String(payload.sub ?? ''); @@ -194,7 +228,15 @@ export class AuthService { const userMap = new Map(users.map((u) => [u.id, u])); return members - .map((m) => ({ userId: m.userId, email: userMap.get(m.userId)?.email ?? '', status: m.status })) + .map((m) => { + const u = userMap.get(m.userId); + return { + userId: m.userId, + email: u?.email ?? '', + name: u?.name ?? u?.email ?? '', + status: m.status, + }; + }) .filter((x) => !!x.email); } diff --git a/src/auth/dto.update-me.dto.ts b/src/auth/dto.update-me.dto.ts new file mode 100644 index 0000000..c538bdb --- /dev/null +++ b/src/auth/dto.update-me.dto.ts @@ -0,0 +1,8 @@ +import { IsString, MaxLength, MinLength } from 'class-validator'; + +export class UpdateMeDto { + @IsString() + @MinLength(1) + @MaxLength(120) + name!: string; +} diff --git a/src/common/center-api-key.guard.ts b/src/common/center-api-key.guard.ts index f2d4832..504b812 100644 --- a/src/common/center-api-key.guard.ts +++ b/src/common/center-api-key.guard.ts @@ -27,6 +27,8 @@ export class CenterApiKeyGuard implements CanActivate { (method === 'POST' && (path === '/auth/login' || path.endsWith('/auth/login'))) || (method === 'POST' && (path === '/auth/refresh' || path.endsWith('/auth/refresh'))) || (method === 'POST' && (path === '/auth/logout' || path.endsWith('/auth/logout'))) || + (method === 'GET' && (path === '/auth/me' || path.endsWith('/auth/me'))) || + (method === 'PATCH' && (path === '/auth/me' || path.endsWith('/auth/me'))) || (method === 'GET' && (path === '/auth/me/guilds' || path.endsWith('/auth/me/guilds'))) || (method === 'POST' && (path === '/auth/me/guilds/join' || path.endsWith('/auth/me/guilds/join'))) || (method === 'GET' && (path.includes('/auth/guilds/') && path.endsWith('/members'))); diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index ee58e84..31b2703 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -8,6 +8,9 @@ export class User { @Column({ unique: true }) email!: string; + @Column({ type: 'varchar', length: 120, nullable: true }) + name!: string | null; + @Column() passwordHash!: string;