- 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>
43 lines
1.5 KiB
TypeScript
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;
|
|
}
|
|
}
|