feat: owner read-only view-as from team users table

Allow org owners to inspect any member's data scope via dual JWT; server blocks mutations and team page stays reachable for return.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-17 19:57:26 +03:30
co-authored by Cursor
parent 52c7306e78
commit f7b07f6b7a
13 changed files with 435 additions and 49 deletions
+28 -12
View File
@@ -26,8 +26,7 @@ import {
} from './dto';
import { JwtAuthGuard } from './jwt-auth.guard';
import { CurrentUser } from './current-user.decorator';
type AuthActor = { userId: string; organizationId: string };
import type { Actor } from '../common/record-scope';
@Controller('auth')
export class AuthController {
@@ -69,7 +68,7 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@Post('switch-organization')
switchOrganization(
@CurrentUser() user: AuthActor,
@CurrentUser() user: Actor,
@Body() dto: SwitchOrganizationDto,
) {
return this.authService.switchOrganization(user.userId, dto.organizationId);
@@ -77,38 +76,55 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@Get('organizations')
listOrganizations(@CurrentUser() user: AuthActor) {
listOrganizations(@CurrentUser() user: Actor) {
return this.authService.listMyOrganizations(user.userId);
}
@UseGuards(JwtAuthGuard)
@Get('me')
me(@CurrentUser() user: AuthActor) {
return this.authService.me(user.userId, user.organizationId);
me(@CurrentUser() user: Actor) {
return this.authService.me(user.userId, user.organizationId, user);
}
@UseGuards(JwtAuthGuard)
@Post('view-as')
viewAs(
@CurrentUser() user: Actor,
@Body() body: { targetUserId?: string },
) {
if (!body.targetUserId?.trim()) {
throw new BadRequestException('کاربر هدف مشخص نیست.');
}
return this.authService.viewAs(user, body.targetUserId.trim());
}
@UseGuards(JwtAuthGuard)
@Patch('me')
updateMe(@CurrentUser() user: AuthActor, @Body() dto: UpdateProfileDto) {
updateMe(@CurrentUser() user: Actor, @Body() dto: UpdateProfileDto) {
return this.authService.updateMe(user.userId, user.organizationId, dto);
}
@UseGuards(JwtAuthGuard)
@Patch('me/prior-archive')
setPriorLens(
@CurrentUser() user: AuthActor,
@CurrentUser() user: Actor,
@Body() body: { lens?: string },
) {
if (body.lens !== 'archive' && body.lens !== 'current') {
throw new BadRequestException('حالت بایگانی نامعتبر است');
}
return this.authService.setPriorLens(user.userId, user.organizationId, body.lens);
return this.authService.setPriorLens(
user.userId,
user.organizationId,
body.lens,
user,
);
}
/** عکس پروفایل کاربر جاری */
@UseGuards(JwtAuthGuard)
@Get('me/avatar')
async avatar(@CurrentUser() user: AuthActor) {
async avatar(@CurrentUser() user: Actor) {
const file = await this.authService.openAvatar(user.userId);
return new StreamableFile(file.stream, { type: file.mimeType });
}
@@ -122,7 +138,7 @@ export class AuthController {
}),
)
uploadAvatar(
@CurrentUser() user: AuthActor,
@CurrentUser() user: Actor,
@UploadedFile() file: Express.Multer.File,
) {
return this.authService.saveAvatar(user.userId, user.organizationId, file);
@@ -130,7 +146,7 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@Delete('me/avatar')
removeAvatar(@CurrentUser() user: AuthActor) {
removeAvatar(@CurrentUser() user: Actor) {
return this.authService.removeAvatar(user.userId, user.organizationId);
}
}