feat(center): user display name + GET/PATCH /auth/me
- User.name column, defaults to email on register - GET /auth/me, PATCH /auth/me to view/change own name (api-key exempt) - login + guild members responses now include name Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 { AuthService } from './auth.service';
|
||||||
import { LoginDto } from './dto.login.dto';
|
import { LoginDto } from './dto.login.dto';
|
||||||
import { RefreshDto } from './dto.refresh.dto';
|
import { RefreshDto } from './dto.refresh.dto';
|
||||||
import { LogoutDto } from './dto.logout.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')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@@ -23,6 +28,20 @@ export class AuthController {
|
|||||||
return this.authService.logout(body.refreshToken);
|
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')
|
@Get('me/guilds')
|
||||||
meGuilds(@Headers('authorization') authorization?: string) {
|
meGuilds(@Headers('authorization') authorization?: string) {
|
||||||
const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : '';
|
const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : '';
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class AuthService {
|
|||||||
const passwordHash = await bcrypt.hash(input.password, 10);
|
const passwordHash = await bcrypt.hash(input.password, 10);
|
||||||
const user = this.userRepo.create({
|
const user = this.userRepo.create({
|
||||||
email: input.email,
|
email: input.email,
|
||||||
|
name: input.email,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
refreshTokenHash: null,
|
refreshTokenHash: null,
|
||||||
});
|
});
|
||||||
@@ -107,6 +108,7 @@ export class AuthService {
|
|||||||
return {
|
return {
|
||||||
id: saved.id,
|
id: saved.id,
|
||||||
email: saved.email,
|
email: saved.email,
|
||||||
|
name: saved.name ?? saved.email,
|
||||||
createdAt: saved.createdAt,
|
createdAt: saved.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -141,12 +143,44 @@ export class AuthService {
|
|||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
name: user.name ?? user.email,
|
||||||
},
|
},
|
||||||
guilds,
|
guilds,
|
||||||
guildAccessTokens,
|
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) {
|
async listMyGuilds(accessToken: string) {
|
||||||
const payload = this.verifyCenterAccessToken(accessToken);
|
const payload = this.verifyCenterAccessToken(accessToken);
|
||||||
const userId = String(payload.sub ?? '');
|
const userId = String(payload.sub ?? '');
|
||||||
@@ -194,7 +228,15 @@ export class AuthService {
|
|||||||
|
|
||||||
const userMap = new Map(users.map((u) => [u.id, u]));
|
const userMap = new Map(users.map((u) => [u.id, u]));
|
||||||
return members
|
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);
|
.filter((x) => !!x.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
src/auth/dto.update-me.dto.ts
Normal file
8
src/auth/dto.update-me.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsString, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateMeDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(120)
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@ export class CenterApiKeyGuard implements CanActivate {
|
|||||||
(method === 'POST' && (path === '/auth/login' || path.endsWith('/auth/login'))) ||
|
(method === 'POST' && (path === '/auth/login' || path.endsWith('/auth/login'))) ||
|
||||||
(method === 'POST' && (path === '/auth/refresh' || path.endsWith('/auth/refresh'))) ||
|
(method === 'POST' && (path === '/auth/refresh' || path.endsWith('/auth/refresh'))) ||
|
||||||
(method === 'POST' && (path === '/auth/logout' || path.endsWith('/auth/logout'))) ||
|
(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 === 'GET' && (path === '/auth/me/guilds' || path.endsWith('/auth/me/guilds'))) ||
|
||||||
(method === 'POST' && (path === '/auth/me/guilds/join' || path.endsWith('/auth/me/guilds/join'))) ||
|
(method === 'POST' && (path === '/auth/me/guilds/join' || path.endsWith('/auth/me/guilds/join'))) ||
|
||||||
(method === 'GET' && (path.includes('/auth/guilds/') && path.endsWith('/members')));
|
(method === 'GET' && (path.includes('/auth/guilds/') && path.endsWith('/members')));
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export class User {
|
|||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
email!: string;
|
email!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 120, nullable: true })
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
passwordHash!: string;
|
passwordHash!: string;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user