- Add global WriteAccessGuard that blocks POST/PUT/PATCH/DELETE for viewer role - Add @AllowViewer() decorator for endpoints viewers need (switch-org, intro-seen, AI recommendations) - Add useIsReadOnly hook to auth store for frontend role checks - Hide write UI (add/edit/delete/import buttons, inline editors) in all 13 data pages for viewers - Disable inline NumberInputs on Budgets and Monthly Actuals pages for viewers - Skip onboarding wizard for viewer role users Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
import {
|
|
Controller,
|
|
Post,
|
|
Patch,
|
|
Body,
|
|
UseGuards,
|
|
Request,
|
|
Get,
|
|
} from '@nestjs/common';
|
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { AuthGuard } from '@nestjs/passport';
|
|
import { AuthService } from './auth.service';
|
|
import { RegisterDto } from './dto/register.dto';
|
|
import { LoginDto } from './dto/login.dto';
|
|
import { SwitchOrgDto } from './dto/switch-org.dto';
|
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|
import { AllowViewer } from '../../common/decorators/allow-viewer.decorator';
|
|
|
|
@ApiTags('auth')
|
|
@Controller('auth')
|
|
export class AuthController {
|
|
constructor(private authService: AuthService) {}
|
|
|
|
@Post('register')
|
|
@ApiOperation({ summary: 'Register a new user' })
|
|
async register(@Body() dto: RegisterDto) {
|
|
return this.authService.register(dto);
|
|
}
|
|
|
|
@Post('login')
|
|
@ApiOperation({ summary: 'Login with email and password' })
|
|
@UseGuards(AuthGuard('local'))
|
|
async login(@Request() req: any, @Body() _dto: LoginDto) {
|
|
const ip = req.headers['x-forwarded-for'] || req.ip;
|
|
const ua = req.headers['user-agent'];
|
|
return this.authService.login(req.user, ip, ua);
|
|
}
|
|
|
|
@Get('profile')
|
|
@ApiOperation({ summary: 'Get current user profile' })
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
async getProfile(@Request() req: any) {
|
|
return this.authService.getProfile(req.user.sub);
|
|
}
|
|
|
|
@Patch('intro-seen')
|
|
@ApiOperation({ summary: 'Mark the how-to intro as seen for the current user' })
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
@AllowViewer()
|
|
async markIntroSeen(@Request() req: any) {
|
|
await this.authService.markIntroSeen(req.user.sub);
|
|
return { success: true };
|
|
}
|
|
|
|
@Post('switch-org')
|
|
@ApiOperation({ summary: 'Switch active organization' })
|
|
@ApiBearerAuth()
|
|
@UseGuards(JwtAuthGuard)
|
|
@AllowViewer()
|
|
async switchOrg(@Request() req: any, @Body() dto: SwitchOrgDto) {
|
|
const ip = req.headers['x-forwarded-for'] || req.ip;
|
|
const ua = req.headers['user-agent'];
|
|
return this.authService.switchOrganization(req.user.sub, dto.organizationId, ip, ua);
|
|
}
|
|
}
|