Files
HOA_Financial_Platform/backend/src/common/guards/write-access.guard.ts
olsch01 a996208cb8 feat: add annual billing, free trial, upgrade/downgrade, and ACH invoice support
- Add monthly/annual billing toggle with 25% annual discount on pricing page
- Implement 14-day no-card free trial (server-side Stripe subscription creation)
- Enable upgrade/downgrade via Stripe Customer Portal
- Add admin-initiated ACH/invoice billing for enterprise customers
- Add billing card to Settings page with plan info and Manage Billing button
- Handle past_due status with read-only grace period access
- Add trial ending and trial expired email templates
- Add DB migration for billing_interval and collection_method columns
- Update ONBOARDING-AND-AUTH.md documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 08:04:51 -04:00

43 lines
1.5 KiB
TypeScript

import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ALLOW_VIEWER_KEY } from '../decorators/allow-viewer.decorator';
@Injectable()
export class WriteAccessGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const method = request.method;
// Allow all read methods
if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return true;
// Determine role from either req.userRole (set by TenantMiddleware which runs
// before guards) or req.user.role (set by JwtAuthGuard Passport strategy).
const role = request.userRole || request.user?.role;
if (!role) return true; // unauthenticated endpoints like login/register
// Check for @AllowViewer() exemption on handler or class
const allowViewer = this.reflector.getAllAndOverride<boolean>(ALLOW_VIEWER_KEY, [
context.getHandler(),
context.getClass(),
]);
if (allowViewer) return true;
// Block viewer role from write operations
if (role === 'viewer') {
throw new ForbiddenException('Read-only users cannot modify data');
}
// Block writes for past_due organizations (grace period: read-only access)
if (request.orgPastDue) {
throw new ForbiddenException(
'Your subscription is past due. Please update your payment method to continue making changes.',
);
}
return true;
}
}