consent-core
v1.0.11
Published
Lightweight, type-safe consent management library for NestJS applications with TypeORM - GDPR compliant
Maintainers
Readme
consent-core
Lightweight, type-safe consent management library for NestJS applications
A production-ready, GDPR/CCPA-compliant consent management solution for NestJS with TypeORM. Simple to integrate, highly customizable, and built with TypeScript for complete type safety.
🚀 Features
- ✅ Type-Safe - Full TypeScript support with comprehensive type definitions
- ✅ Framework Agnostic Auth - Works with any authentication strategy (JWT, Passport, etc.)
- ✅ Optional Controller - Use default REST endpoints or build your own
- ✅ Custom Entities - Extend or replace default entities with your own
- ✅ Expiry Management - Automatic consent expiration based on validity period
- ✅ Version Tracking - Handle policy version changes with automatic re-consent
- ✅ Suppression Support - "Ask me later" functionality
- ✅ GDPR/CCPA Ready - Built-in audit trails and compliance features
- ✅ Production Ready - Used in production environments
📦 Installation
npm install consent-corePeer Dependencies
npm install @nestjs/common @nestjs/core @nestjs/typeorm typeorm class-validator class-transformer reflect-metadata⚡ Quick Start
1. Install and Setup Database
# Install the package
npm install consent-core
# Run the migration
psql your_database < node_modules/consent-core/migrations.sql2. Import the Module
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConsentModule } from 'consent-core';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'your_user',
password: 'your_password',
database: 'your_db',
autoLoadEntities: true, // 👈 REQUIRED: Auto-register entities
synchronize: false, // Use migrations in production
}),
ConsentModule.forRoot(), // 👈 Add this
],
})
export class AppModule {}Important: Either use autoLoadEntities: true OR explicitly register the entities:
import { ConsentDefinition, UserConsent } from 'consent-core';
TypeOrmModule.forRoot({
// ... other config
entities: [ConsentDefinition, UserConsent, /* your other entities */],
})3. Secure Your Endpoints (If Using Default Controller)
The default controller expects req.user.id to be set by YOUR authentication.
// Apply your existing auth guard globally or to specific routes
@UseGuards(YourAuthGuard) // Your existing JWT/Passport guardThe library is authentication-agnostic - use whatever you already have (JWT, Passport, sessions, etc).
4. Use the API
# Get user consent status
GET /consents/status
Authorization: Bearer <token>
# Accept a consent
POST /consents/respond
{
"consentDefinitionId": 1,
"status": "ACCEPTED"
}
# Suppress a consent for 7 days
POST /consents/suppress
{
"consentDefinitionId": 1,
"suppressDays": 7
}📖 Usage Scenarios
Scenario 1: Default Setup (Zero Configuration) ⚡
Use Case: Quick setup with default controller and entities - perfect for getting started.
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConsentModule } from 'consent-core';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
// ... your database config
autoLoadEntities: true,
}),
ConsentModule.forRoot(), // 👈 No configuration needed!
],
})
export class AppModule {}What you get:
- ✅ Default REST API endpoints (
/consents/status,/consents/respond,/consents/suppress) - ✅ Built-in entities (
ConsentDefinition,UserConsent) - ✅ Full service and repository layer
- ✅ Ready to use - just run migrations and go!
Default API Endpoints:
GET /consents/status # Get user's consent status
POST /consents/respond # Accept/reject consent
POST /consents/suppress # Suppress consent ("ask later")Scenario 2: Custom Entities Only 🔧
Use Case: You need additional fields in consent tables but want to keep the default controller.
// entities/custom-consent-definition.entity.ts
import { Entity, Column } from 'typeorm';
import { ConsentDefinition } from 'consent-core';
@Entity('consent_definitions')
export class CustomConsentDefinition extends ConsentDefinition {
@Column({ nullable: true })
category: string; // Your custom field
@Column({ nullable: true })
region: string; // Your custom field
@Column({ type: 'jsonb', nullable: true })
metadata: any; // Additional data
}// entities/custom-user-consent.entity.ts
import { Entity, Column } from 'typeorm';
import { UserConsent } from 'consent-core';
@Entity('user_consents')
export class CustomUserConsent extends UserConsent {
@Column({ nullable: true })
ipAddress: string; // Track IP for compliance
@Column({ nullable: true })
deviceInfo: string; // Track device
}// app.module.ts
import { ConsentModule } from 'consent-core';
import { CustomConsentDefinition } from './entities/custom-consent-definition.entity';
import { CustomUserConsent } from './entities/custom-user-consent.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... your database config
}),
ConsentModule.forRoot({
consentDefinitionEntity: CustomConsentDefinition, // 👈 Your entity
userConsentEntity: CustomUserConsent, // 👈 Your entity
}),
],
})
export class AppModule {}What you get:
- ✅ Your custom entity fields
- ✅ Default REST API endpoints still work
- ✅ Full backward compatibility
- ⚠️ Important: Update migrations to include your custom columns
Scenario 3: Custom Controller Only 🎮
Use Case: You want your own API endpoints, authentication, and routing but keep the default entities.
// app.module.ts
import { ConsentModule } from 'consent-core';
@Module({
imports: [
ConsentModule.forRoot({
disableControllers: true, // 👈 Disable default endpoints
}),
],
controllers: [MyCustomConsentController], // 👈 Your controller
})
export class AppModule {}// controllers/my-custom-consent.controller.ts
import { Controller, Get, Post, Body, UseGuards, Req } from '@nestjs/common';
import { ConsentService, ConsentStatusDto, ConsentResponseDto } from 'consent-core';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser } from '../decorators/current-user.decorator';
@Controller('api/v2/user/consents') // 👈 Your custom route
@UseGuards(JwtAuthGuard) // 👈 Your custom auth
export class MyCustomConsentController {
constructor(private readonly consentService: ConsentService) {}
@Get('my-status')
async getMyConsentStatus(@CurrentUser() user): Promise<ConsentStatusDto> {
return this.consentService.getStatus(user.id);
}
@Post('accept')
async acceptConsent(
@CurrentUser() user,
@Body() dto: { consentDefinitionId: number }
): Promise<ConsentResponseDto> {
return this.consentService.respond(
user.id,
dto.consentDefinitionId,
'ACCEPTED',
user.email, // actor for audit
);
}
@Post('decline')
async declineConsent(
@CurrentUser() user,
@Body() dto: { consentDefinitionId: number }
): Promise<ConsentResponseDto> {
return this.consentService.respond(
user.id,
dto.consentDefinitionId,
'REJECTED',
user.email,
);
}
@Post('remind-later')
async remindLater(
@CurrentUser() user,
@Body() dto: { consentDefinitionId: number; days?: number }
) {
return this.consentService.suppress(
user.id,
dto.consentDefinitionId,
dto.days || 7, // Default 7 days
user.email,
);
}
}What you get:
- ✅ Full control over routes and authentication
- ✅ Use
ConsentServiceandConsentRepositorydirectly - ✅ Default entities (or extend them)
- ✅ Keep all business logic from the library
Scenario 4: Both Custom Entities and Controller 🚀
Use Case: Maximum flexibility - your own entities AND your own controller.
// app.module.ts
import { ConsentModule } from 'consent-core';
import { CustomConsentDefinition } from './entities/custom-consent-definition.entity';
import { CustomUserConsent } from './entities/custom-user-consent.entity';
import { MyCustomConsentController } from './controllers/my-custom-consent.controller';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... your database config
}),
ConsentModule.forRoot({
consentDefinitionEntity: CustomConsentDefinition, // 👈 Your entities
userConsentEntity: CustomUserConsent,
disableControllers: true, // 👈 No default endpoints
}),
],
controllers: [MyCustomConsentController], // 👈 Your controller
})
export class AppModule {}Custom entities with extra fields:
// entities/custom-consent-definition.entity.ts
import { Entity, Column } from 'typeorm';
import { ConsentDefinition } from 'consent-core';
@Entity('consent_definitions')
export class CustomConsentDefinition extends ConsentDefinition {
@Column({ nullable: true })
category: string;
@Column({ nullable: true })
legalBasis: string;
@Column({ nullable: true })
retentionPeriod: number;
}Custom controller using your entities:
// controllers/my-custom-consent.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ConsentService, ConsentRepository } from 'consent-core';
import { AdminGuard } from '../guards/admin.guard';
@Controller('admin/consents')
@UseGuards(AdminGuard) // Admin-only endpoints
export class MyCustomConsentController {
constructor(
private readonly consentService: ConsentService,
private readonly consentRepository: ConsentRepository,
) {}
@Get('definitions')
async getAllDefinitions() {
// Access your custom entity fields
return this.consentRepository.findActiveDefinitions();
}
@Post('bulk-suppress')
async bulkSuppressForUsers(@Body() dto: { userIds: number[]; days: number }) {
const results = [];
for (const userId of dto.userIds) {
const status = await this.consentService.getStatus(userId);
for (const item of status.consents) {
if (item.status === 'PENDING') {
await this.consentService.suppress(userId, item.id, dto.days, 'admin');
}
}
results.push({ userId, suppressed: true });
}
return results;
}
}What you get:
- ✅ Complete control over data structure
- ✅ Complete control over API design
- ✅ Full access to service and repository layers
- ✅ Keep all the consent management logic
Scenario 5: Programmatic Usage (Service Layer Only) 🔨
Use Case: No REST API needed - use consent management in your business logic.
// app.module.ts
import { ConsentModule } from 'consent-core';
@Module({
imports: [
ConsentModule.forRoot({
disableControllers: true, // 👈 No HTTP endpoints
}),
],
})
export class AppModule {}// services/onboarding.service.ts
import { Injectable } from '@nestjs/common';
import { ConsentService } from 'consent-core';
@Injectable()
export class OnboardingService {
constructor(private readonly consentService: ConsentService) {}
async canUserAccessFeature(userId: number): Promise<boolean> {
const { mandatoryPending } = await this.consentService.getStatus(userId);
return !mandatoryPending; // Block if mandatory consents pending
}
async completeUserOnboarding(userId: number, email: string) {
const { consents } = await this.consentService.getStatus(userId);
// Auto-accept non-mandatory consents
for (const consent of consents) {
if (!consent.mandatory && consent.status === 'PENDING') {
await this.consentService.respond(
userId,
consent.id,
'ACCEPTED',
`onboarding:${email}`,
);
}
}
}
async checkMarketingConsent(userId: number): Promise<boolean> {
const { consents } = await this.consentService.getStatus(userId);
const marketing = consents.find(c => c.key === 'marketing');
return marketing?.status === 'ACCEPTED';
}
}// services/user-registration.service.ts
import { Injectable } from '@nestjs/common';
import { ConsentService } from 'consent-core';
@Injectable()
export class UserRegistrationService {
constructor(private readonly consentService: ConsentService) {}
async registerUser(userData: any): Promise<any> {
// Create user account
const user = await this.createUserAccount(userData);
// Auto-accept mandatory terms during registration
const { consents } = await this.consentService.getStatus(user.id);
for (const consent of consents) {
if (consent.mandatory && userData.acceptedTerms) {
await this.consentService.respond(
user.id,
consent.id,
'ACCEPTED',
`registration:${userData.email}`,
);
}
}
return user;
}
private async createUserAccount(userData: any) {
// Your user creation logic
return { id: 123, email: userData.email };
}
}
---
## 🗃️ Database Setup
### Option 1: Run Migration SQL
```bash
psql your_database < node_modules/consent-core/migrations.sqlOption 2: Copy Migration Content
See migrations.sql in the package for the complete schema.
Option 3: TypeORM Sync (Development Only)
TypeOrmModule.forRoot({
synchronize: true, // Only in development
entities: [ConsentDefinition, UserConsent],
})🔌 API Reference
GET /consents/status
Description: Get all consent definitions with user's current status
Headers:
Authorization: Bearer <token>Response (200):
{
"mandatoryPending": boolean, // Block app if true
"consents": [
{
"id": 1,
"key": "terms",
"title": "Terms & Conditions",
"content": "<p>By using our service...</p>",
"contentType": "html",
"version": "v1.0",
"mandatory": true,
"status": "PENDING" | "ACCEPTED" | "REJECTED",
"validUntil": "2027-01-22T00:00:00.000Z" | null,
"suppressedUntil": "2026-01-29T00:00:00.000Z" | null
}
]
}POST /consents/respond
Description: User accepts or rejects a consent
Headers:
Authorization: Bearer <token>Request Body:
{
"consentDefinitionId": number,
"status": "ACCEPTED" | "REJECTED",
"actor": string (optional) // e.g., user email
}Response (200):
{
"success": true,
"expireAt": "2027-01-22T00:00:00.000Z" | null
}POST /consents/suppress
Description: Temporarily hide consent ("Ask me later")
Headers:
Authorization: Bearer <token>Request Body:
{
"consentDefinitionId": number,
"suppressDays": number, // e.g., 7 for one week
"actor": string (optional)
}Response (200):
{
"success": true,
"suppressedUntil": "2026-01-29T00:00:00.000Z"
}🎯 Frontend Integration
React Example
import { useEffect, useState } from 'react';
function ConsentModal() {
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchConsentStatus();
}, []);
const fetchConsentStatus = async () => {
const res = await fetch('/consents/status', {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
setStatus(data);
setLoading(false);
};
const acceptConsent = async (consentId) => {
await fetch('/consents/respond', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
consentDefinitionId: consentId,
status: 'ACCEPTED'
})
});
fetchConsentStatus(); // Refresh
};
const suppressConsent = async (consentId) => {
await fetch('/consents/suppress', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
consentDefinitionId: consentId,
suppressDays: 7
})
});
fetchConsentStatus();
};
if (loading) return <div>Loading...</div>;
if (!status.mandatoryPending) return null; // No consents needed
return (
<div className="modal">
{status.consents
.filter(c => c.status !== 'ACCEPTED')
.map(consent => (
<div key={consent.id}>
<h3>{consent.title}</h3>
<div dangerouslySetInnerHTML={{ __html: consent.content }} />
{consent.mandatory && <span className="badge">Required</span>}
<button onClick={() => acceptConsent(consent.id)}>
Accept
</button>
{!consent.mandatory && (
<button onClick={() => suppressConsent(consent.id)}>
Ask me later
</button>
)}
</div>
))}
</div>
);
}🧪 Testing
import { Test } from '@nestjs/testing';
import { ConsentService } from 'consent-core';
describe('Consent Integration', () => {
let service: ConsentService;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [ConsentModule.forRoot()],
}).compile();
service = module.get<ConsentService>(ConsentService);
});
it('should return pending status for new user', async () => {
const status = await service.getStatus(999);
expect(status.mandatoryPending).toBe(true);
expect(status.consents[0].status).toBe('PENDING');
});
it('should accept consent', async () => {
const result = await service.respond(999, 1, 'ACCEPTED');
expect(result.success).toBe(true);
expect(result.expireAt).toBeDefined();
});
});🔒 Security Considerations
What consent-core Does NOT Handle
- ❌ User authentication/login
- ❌ Authorization/permissions
- ❌ Rate limiting
- ❌ CORS configuration
- ❌ Input sanitization (beyond TypeScript validation)
Consumer Responsibilities
- ✅ Apply authentication guards to endpoints
- ✅ Validate user permissions
- ✅ Implement rate limiting
- ✅ Configure CORS properly
- ✅ Sanitize user inputs
- ✅ Set up audit logging
- ✅ Implement proper error handling
Recommended Setup
@Controller('consents')
@UseGuards(JwtAuthGuard) // Authentication
@UseInterceptors(AuditLogInterceptor) // Logging
export class ConsentController {
// ... endpoints
}📊 Performance Tips
1. Add Caching
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { ConsentService } from 'consent-core';
@Injectable()
export class CachedConsentService {
constructor(
private consentService: ConsentService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async getStatus(userId: number) {
const cacheKey = `consent:status:${userId}`;
let status = await this.cacheManager.get(cacheKey);
if (status) return status;
status = await this.consentService.getStatus(userId);
await this.cacheManager.set(cacheKey, status, 300); // 5 min TTL
return status;
}
async invalidateCache(userId: number) {
await this.cacheManager.del(`consent:status:${userId}`);
}
}2. Database Indexes
Already included in migrations.sql:
user_idindex (frequent lookups)consent_definition_idindex (joins)- Composite
(user_id, consent_definition_id)index
🌐 Environment Configuration
// config.ts
export const consentConfig = {
defaultValidityDays: 365,
suppressionMaxDays: 30,
mandatoryBlocksApp: true,
};
// Usage in custom controller
@Post('respond')
async respond(@Body() dto: any) {
// Custom business logic
if (dto.status === 'REJECTED' && isMandatory) {
throw new BadRequestException('Cannot reject mandatory consent');
}
return this.consentService.respond(...);
}📚 Additional Resources
- ARCHITECTURE.md - Detailed system architecture
- API-FLOW.md - Complete API flow documentation
- migrations.sql - Database schema
🤝 Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
MIT License - see LICENSE file for details
🆘 Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
⭐ Show Your Support
If this library helped you, please give it a ⭐️ on GitHub!
📦 NPM Publishing
# Build the project
npm run build
# Publish to npm (after authentication)
npm publishMade with ❤️ by the consent-core contributors
