Auto-commit 2026-04-29 16:31
This commit is contained in:
19
packages/shared-analytics/package.json
Normal file
19
packages/shared-analytics/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@shieldsai/shared-analytics",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@segment/analytics-node": "^1.0.0",
|
||||
"googleapis": "^128.0.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
132
packages/shared-analytics/src/config/analytics.config.ts
Normal file
132
packages/shared-analytics/src/config/analytics.config.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Environment variables for analytics
|
||||
const envSchema = z.object({
|
||||
MIXPANEL_TOKEN: z.string(),
|
||||
MIXPANEL_API_SECRET: z.string().optional(),
|
||||
GA4_MEASUREMENT_ID: z.string(),
|
||||
GA4_API_SECRET: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SECRET: z.string(),
|
||||
ANALYTICS_ENV: z.enum(['development', 'production', 'staging']).default('development'),
|
||||
});
|
||||
|
||||
export const analyticsEnv = envSchema.parse({
|
||||
MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN,
|
||||
MIXPANEL_API_SECRET: process.env.MIXPANEL_API_SECRET,
|
||||
GA4_MEASUREMENT_ID: process.env.GA4_MEASUREMENT_ID,
|
||||
GA4_API_SECRET: process.env.GA4_API_SECRET,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
ANALYTICS_ENV: process.env.ANALYTICS_ENV,
|
||||
});
|
||||
|
||||
// Event taxonomy
|
||||
export enum EventType {
|
||||
// User events
|
||||
USER_SIGNED_UP = 'user_signed_up',
|
||||
USER_LOGGED_IN = 'user_logged_in',
|
||||
USER_LOGGED_OUT = 'user_logged_out',
|
||||
USER_UPGRADED = 'user_upgraded',
|
||||
USER_DOWNGRADED = 'user_downgraded',
|
||||
|
||||
// Subscription events
|
||||
SUBSCRIPTION_CREATED = 'subscription_created',
|
||||
SUBSCRIPTION_UPDATED = 'subscription_updated',
|
||||
SUBSCRIPTION_CANCELLED = 'subscription_cancelled',
|
||||
SUBSCRIPTION_RENEWED = 'subscription_renewed',
|
||||
|
||||
// DarkWatch events
|
||||
DARK_WEB_SCAN_STARTED = 'dark_web_scan_started',
|
||||
DARK_WEB_SCAN_COMPLETED = 'dark_web_scan_completed',
|
||||
EXPOSURE_DETECTED = 'exposure_detected',
|
||||
EXPOSURE_RESOLVED = 'exposure_resolved',
|
||||
WATCHLIST_ITEM_ADDED = 'watchlist_item_added',
|
||||
WATCHLIST_ITEM_REMOVED = 'watchlist_item_removed',
|
||||
|
||||
// VoicePrint events
|
||||
VOICE_ENROLLED = 'voice_enrolled',
|
||||
VOICE_ANALYZED = 'voice_analyzed',
|
||||
VOICE_MATCH_FOUND = 'voice_match_found',
|
||||
SYNTHETIC_VOICE_DETECTED = 'synthetic_voice_detected',
|
||||
|
||||
// SpamShield events
|
||||
CALL_ANALYZED = 'call_analyzed',
|
||||
SMS_ANALYZED = 'sms_analyzed',
|
||||
SPAM_BLOCKED = 'spam_blocked',
|
||||
SPAM_FLAGGED = 'spam_flagged',
|
||||
SPAM_FEEDBACK_SUBMITTED = 'spam_feedback_submitted',
|
||||
|
||||
// KPI events
|
||||
MRR_UPDATED = 'mrr_updated',
|
||||
CONVERSION_OCCURRED = 'conversion_occurred',
|
||||
CHURN_OCCURRED = 'churn_occurred',
|
||||
REFERRAL_SENT = 'referral_sent',
|
||||
REFERRAL_CONVERTED = 'referral_converted',
|
||||
}
|
||||
|
||||
// Event properties schema
|
||||
export const eventPropertiesSchema = z.object({
|
||||
userId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
timestamp: z.date().optional(),
|
||||
platform: z.enum(['web', 'mobile', 'desktop', 'api']).optional(),
|
||||
version: z.string().optional(),
|
||||
environment: z.string().optional(),
|
||||
});
|
||||
|
||||
// KPI definitions
|
||||
export const kpiDefinitions = {
|
||||
mau: {
|
||||
name: 'Monthly Active Users',
|
||||
description: 'Unique users who performed an action in the last 30 days',
|
||||
calculation: 'COUNT(DISTINCT userId) WHERE timestamp > NOW() - INTERVAL 30 DAYS',
|
||||
},
|
||||
payingUsers: {
|
||||
name: 'Paying Users',
|
||||
description: 'Users with active subscriptions',
|
||||
calculation: 'COUNT(DISTINCT userId) WHERE subscription.status = "active"',
|
||||
},
|
||||
mrr: {
|
||||
name: 'Monthly Recurring Revenue',
|
||||
description: 'Total monthly subscription revenue',
|
||||
calculation: 'SUM(subscription.amount) WHERE subscription.status = "active"',
|
||||
},
|
||||
conversionRate: {
|
||||
name: 'Conversion Rate',
|
||||
description: 'Percentage of free users who upgrade to paid',
|
||||
calculation: 'COUNT(upgrade events) / COUNT(signup events)',
|
||||
},
|
||||
churn: {
|
||||
name: 'Churn Rate',
|
||||
description: 'Percentage of paying users who cancel',
|
||||
calculation: 'COUNT(cancel events) / COUNT(active subscriptions)',
|
||||
},
|
||||
cac: {
|
||||
name: 'Customer Acquisition Cost',
|
||||
description: 'Average cost to acquire a new paying user',
|
||||
calculation: 'Total marketing spend / COUNT(new paying users)',
|
||||
},
|
||||
ltv: {
|
||||
name: 'Lifetime Value',
|
||||
description: 'Average revenue per user over their lifetime',
|
||||
calculation: 'Average subscription amount / Churn rate',
|
||||
},
|
||||
nps: {
|
||||
name: 'Net Promoter Score',
|
||||
description: 'Customer satisfaction metric (-100 to 100)',
|
||||
calculation: '% Promoters - % Detractors',
|
||||
},
|
||||
viralCoefficient: {
|
||||
name: 'Viral Coefficient',
|
||||
description: 'Average number of referrals per user',
|
||||
calculation: 'COUNT(referral events) / COUNT(users)',
|
||||
},
|
||||
};
|
||||
|
||||
// Alert thresholds
|
||||
export const alertThresholds = {
|
||||
churn: { warning: 0.05, critical: 0.10 },
|
||||
conversionRate: { warning: 0.02, critical: 0.01 },
|
||||
mrr: { warning: 0.90, critical: 0.80 }, // Percentage of target
|
||||
nps: { warning: 50, critical: 40 },
|
||||
viralCoefficient: { warning: 0.4, critical: 0.3 },
|
||||
};
|
||||
18
packages/shared-analytics/src/index.ts
Normal file
18
packages/shared-analytics/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Config
|
||||
export {
|
||||
analyticsEnv,
|
||||
EventType,
|
||||
eventPropertiesSchema,
|
||||
kpiDefinitions,
|
||||
alertThresholds,
|
||||
} from './config/analytics.config';
|
||||
|
||||
// Services
|
||||
export {
|
||||
MixpanelService,
|
||||
mixpanelService,
|
||||
} from './services/mixpanel.service';
|
||||
export {
|
||||
GA4Service,
|
||||
ga4Service,
|
||||
} from './services/ga4.service';
|
||||
104
packages/shared-analytics/src/services/ga4.service.ts
Normal file
104
packages/shared-analytics/src/services/ga4.service.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { google } from 'googleapis';
|
||||
import { analyticsEnv, EventType } from '../config/analytics.config';
|
||||
|
||||
// GA4 service
|
||||
export class GA4Service {
|
||||
private auth: any;
|
||||
|
||||
constructor() {
|
||||
this.auth = google.auth.fromAPIKey(analyticsEnv.GA4_API_SECRET || 'placeholder');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize GA4 client
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// TODO: Initialize GA4 client with measurement ID
|
||||
console.log('GA4 client initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send event to GA4
|
||||
*/
|
||||
async sendEvent(
|
||||
eventName: string,
|
||||
params: {
|
||||
client_id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
): Promise<void> {
|
||||
// TODO: Implement GA4 event tracking
|
||||
// const measurementId = analyticsEnv.GA4_MEASUREMENT_ID;
|
||||
// await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${analyticsEnv.GA4_API_SECRET}`, {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify({
|
||||
// events: [{ name: eventName, params }],
|
||||
// }),
|
||||
// });
|
||||
|
||||
console.log('GA4 event:', eventName, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track page view
|
||||
*/
|
||||
async trackPageView(clientId: string, path: string, title?: string): Promise<void> {
|
||||
await this.sendEvent('page_view', {
|
||||
client_id: clientId,
|
||||
page_path: path,
|
||||
page_title: title,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track e-commerce purchase
|
||||
*/
|
||||
async trackPurchase(
|
||||
clientId: string,
|
||||
transactionId: string,
|
||||
value: number,
|
||||
currency: string,
|
||||
items: Array<{ name: string; price: number; quantity: number }>
|
||||
): Promise<void> {
|
||||
await this.sendEvent('purchase', {
|
||||
client_id: clientId,
|
||||
transaction_id: transactionId,
|
||||
value,
|
||||
currency,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track conversion
|
||||
*/
|
||||
async trackConversion(
|
||||
clientId: string,
|
||||
conversionName: string,
|
||||
metadata?: Record<string, any>
|
||||
): Promise<void> {
|
||||
await this.sendEvent('conversion', {
|
||||
client_id: clientId,
|
||||
conversion_name: conversionName,
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analytics data (for dashboards)
|
||||
*/
|
||||
async getMetrics(
|
||||
dateRange: { startDate: string; endDate: string },
|
||||
metrics: string[],
|
||||
dimensions?: string[]
|
||||
): Promise<any> {
|
||||
// TODO: Implement GA4 Analytics Data API
|
||||
return {
|
||||
rows: [],
|
||||
totals: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export instance
|
||||
export const ga4Service = new GA4Service();
|
||||
116
packages/shared-analytics/src/services/mixpanel.service.ts
Normal file
116
packages/shared-analytics/src/services/mixpanel.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Analytics } from '@segment/analytics-node';
|
||||
import { analyticsEnv, EventType, eventPropertiesSchema } from '../config/analytics.config';
|
||||
|
||||
// Mixpanel service
|
||||
export class MixpanelService {
|
||||
private client: Analytics;
|
||||
|
||||
constructor() {
|
||||
this.client = new Analytics({
|
||||
apiKey: analyticsEnv.MIXPANEL_TOKEN,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track an event in Mixpanel
|
||||
*/
|
||||
async track(
|
||||
event: EventType,
|
||||
distinctId: string,
|
||||
properties?: Record<string, any>
|
||||
): Promise<void> {
|
||||
const validatedProperties = eventPropertiesSchema.parse(properties);
|
||||
|
||||
this.client.track({
|
||||
event,
|
||||
distinctId,
|
||||
properties: {
|
||||
...validatedProperties,
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify a user
|
||||
*/
|
||||
async identify(userId: string, traits?: Record<string, any>): Promise<void> {
|
||||
this.client.identify({
|
||||
distinctId: userId,
|
||||
traits,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Group users by subscription tier
|
||||
*/
|
||||
async group(groupId: string, groupKey: string, traits?: Record<string, any>): Promise<void> {
|
||||
this.client.group({
|
||||
groupKey,
|
||||
groupId,
|
||||
traits,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track user sign-up
|
||||
*/
|
||||
async userSignedUp(userId: string, plan?: string, referrer?: string): Promise<void> {
|
||||
await this.track(EventType.USER_SIGNED_UP, userId, {
|
||||
plan,
|
||||
referrer,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track subscription upgrade
|
||||
*/
|
||||
async userUpgraded(userId: string, fromTier: string, toTier: string, mrr: number): Promise<void> {
|
||||
await this.track(EventType.USER_UPGRADED, userId, {
|
||||
fromTier,
|
||||
toTier,
|
||||
mrr,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track exposure detection
|
||||
*/
|
||||
async exposureDetected(
|
||||
userId: string,
|
||||
exposureType: string,
|
||||
severity: string,
|
||||
source: string
|
||||
): Promise<void> {
|
||||
await this.track(EventType.EXPOSURE_DETECTED, userId, {
|
||||
exposureType,
|
||||
severity,
|
||||
source,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Track spam detection
|
||||
*/
|
||||
async spamBlocked(userId: string, phoneNumber: string, confidence: number, method: string): Promise<void> {
|
||||
await this.track(EventType.SPAM_BLOCKED, userId, {
|
||||
phoneNumber,
|
||||
confidence,
|
||||
method,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush pending events
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
await this.client.flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Export instance
|
||||
export const mixpanelService = new MixpanelService();
|
||||
12
packages/shared-analytics/tsconfig.json
Normal file
12
packages/shared-analytics/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
18
packages/shared-auth/package.json
Normal file
18
packages/shared-auth/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@shieldsai/shared-auth",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"next-auth": "^4.24.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
114
packages/shared-auth/src/config/auth.config.ts
Normal file
114
packages/shared-auth/src/config/auth.config.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { NextAuthOptions } from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import GoogleProvider from 'next-auth/providers/google';
|
||||
import AppleProvider from 'next-auth/providers/apple';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Environment variables
|
||||
const envSchema = z.object({
|
||||
NEXTAUTH_URL: z.string().url(),
|
||||
NEXTAUTH_SECRET: z.string().min(32),
|
||||
GOOGLE_CLIENT_ID: z.string(),
|
||||
GOOGLE_CLIENT_SECRET: z.string(),
|
||||
APPLE_CLIENT_ID: z.string(),
|
||||
APPLE_CLIENT_SECRET: z.string(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
});
|
||||
|
||||
export const authEnv = envSchema.parse({
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
||||
APPLE_CLIENT_ID: process.env.APPLE_CLIENT_ID,
|
||||
APPLE_CLIENT_SECRET: process.env.APPLE_CLIENT_SECRET,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
// Role-based access control
|
||||
export type UserRole = 'user' | 'family_admin' | 'family_member' | 'support';
|
||||
|
||||
export const userRoles: UserRole[] = ['user', 'family_admin', 'family_member', 'support'];
|
||||
|
||||
// Family group types
|
||||
export type FamilyGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
members: string[]; // user IDs
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
// NextAuth options
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error('Email and password required');
|
||||
}
|
||||
|
||||
// TODO: Validate against database
|
||||
const user = {
|
||||
id: '1',
|
||||
email: credentials.email,
|
||||
name: credentials.email.split('@')[0],
|
||||
role: 'user' as UserRole,
|
||||
};
|
||||
|
||||
return user;
|
||||
},
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: authEnv.GOOGLE_CLIENT_ID,
|
||||
clientSecret: authEnv.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
AppleProvider({
|
||||
clientId: authEnv.APPLE_CLIENT_ID,
|
||||
clientSecret: authEnv.APPLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: 'jwt',
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
},
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error',
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.role = (user as any).role;
|
||||
}
|
||||
|
||||
if (account) {
|
||||
token.provider = account.provider;
|
||||
token.accessToken = account.access_token;
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.role = token.role as UserRole;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async createUser({ user }) {
|
||||
// TODO: Create default family group
|
||||
console.log('New user created:', user.email);
|
||||
},
|
||||
},
|
||||
};
|
||||
25
packages/shared-auth/src/index.ts
Normal file
25
packages/shared-auth/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Config
|
||||
export { authOptions, authEnv, userRoles } from './config/auth.config';
|
||||
export type { UserRole, FamilyGroup } from './config/auth.config';
|
||||
|
||||
// Middleware
|
||||
export { withAuth, withRole, protectApiRoute } from './middleware/auth.middleware';
|
||||
|
||||
// Models
|
||||
export {
|
||||
userSchema,
|
||||
familyGroupSchema,
|
||||
familyMemberSchema,
|
||||
sessionSchema,
|
||||
accountSchema,
|
||||
createUserSchema,
|
||||
createFamilyGroupSchema,
|
||||
addFamilyMemberSchema,
|
||||
} from './models/auth.models';
|
||||
export type {
|
||||
User,
|
||||
FamilyGroup as AuthFamilyGroup,
|
||||
FamilyMember,
|
||||
Session,
|
||||
Account,
|
||||
} from './models/auth.models';
|
||||
62
packages/shared-auth/src/middleware/auth.middleware.ts
Normal file
62
packages/shared-auth/src/middleware/auth.middleware.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from 'next-auth/react';
|
||||
import { UserRole } from '../config/auth.config';
|
||||
|
||||
/**
|
||||
* Middleware to protect routes that require authentication
|
||||
*/
|
||||
export function withAuth(
|
||||
request: NextRequest,
|
||||
options?: {
|
||||
signInPath?: string;
|
||||
}
|
||||
): NextResponse {
|
||||
const token = request.cookies.get('next-auth.session-token')?.value;
|
||||
const signInPath = options?.signInPath ?? '/auth/signin';
|
||||
|
||||
if (!token) {
|
||||
const signInUrl = new URL(signInPath, request.url);
|
||||
signInUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
|
||||
return NextResponse.redirect(signInUrl);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to check if user has required role
|
||||
*/
|
||||
export function withRole(
|
||||
response: NextResponse,
|
||||
request: NextRequest,
|
||||
requiredRoles: UserRole[]
|
||||
): NextResponse {
|
||||
const token = request.cookies.get('next-auth.session-token')?.value;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// TODO: Decode JWT and check role
|
||||
// For now, allow all authenticated users
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to protect API routes
|
||||
*/
|
||||
export function protectApiRoute(request: NextRequest): NextResponse {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return NextResponse.json({ error: 'Missing or invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
// TODO: Verify JWT token
|
||||
return NextResponse.next();
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
81
packages/shared-auth/src/models/auth.models.ts
Normal file
81
packages/shared-auth/src/models/auth.models.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// User schema
|
||||
export const userSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
image: z.string().url().optional(),
|
||||
role: z.enum(['user', 'family_admin', 'family_member', 'support']),
|
||||
emailVerified: z.date().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
// Family group schema
|
||||
export const familyGroupSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
ownerId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type FamilyGroup = z.infer<typeof familyGroupSchema>;
|
||||
|
||||
// Family member schema
|
||||
export const familyMemberSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
groupId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
role: z.enum(['owner', 'admin', 'member']),
|
||||
joinedAt: z.date(),
|
||||
});
|
||||
|
||||
export type FamilyMember = z.infer<typeof familyMemberSchema>;
|
||||
|
||||
// Session schema
|
||||
export const sessionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
sessionToken: z.string(),
|
||||
expires: z.date(),
|
||||
createdAt: z.date(),
|
||||
});
|
||||
|
||||
export type Session = z.infer<typeof sessionSchema>;
|
||||
|
||||
// Account schema (for OAuth)
|
||||
export const accountSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
provider: z.string(),
|
||||
providerAccountId: z.string(),
|
||||
access_token: z.string().optional(),
|
||||
refresh_token: z.string().optional(),
|
||||
expires_at: z.number().optional(),
|
||||
token_type: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Account = z.infer<typeof accountSchema>;
|
||||
|
||||
// Validation schemas for API
|
||||
export const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
name: z.string().min(1),
|
||||
});
|
||||
|
||||
export const createFamilyGroupSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
ownerId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const addFamilyMemberSchema = z.object({
|
||||
groupId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
role: z.enum(['admin', 'member']).default('member'),
|
||||
});
|
||||
12
packages/shared-auth/tsconfig.json
Normal file
12
packages/shared-auth/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
90
packages/shared-billing/src/config/billing.config.ts
Normal file
90
packages/shared-billing/src/config/billing.config.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Environment variables
|
||||
const envSchema = z.object({
|
||||
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
|
||||
STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
|
||||
STRIPE_PRICE_ID_BASIC: z.string().startsWith('price_'),
|
||||
STRIPE_PRICE_ID_PLUS: z.string().startsWith('price_'),
|
||||
STRIPE_PRICE_ID_PREMIUM: z.string().startsWith('price_'),
|
||||
});
|
||||
|
||||
export const billingEnv = envSchema.parse({
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
STRIPE_PRICE_ID_BASIC: process.env.STRIPE_PRICE_ID_BASIC,
|
||||
STRIPE_PRICE_ID_PLUS: process.env.STRIPE_PRICE_ID_PLUS,
|
||||
STRIPE_PRICE_ID_PREMIUM: process.env.STRIPE_PRICE_ID_PREMIUM,
|
||||
});
|
||||
|
||||
// Initialize Stripe
|
||||
export const stripe = new Stripe(billingEnv.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-06-20',
|
||||
typescript: true,
|
||||
});
|
||||
|
||||
// Subscription tiers
|
||||
export enum SubscriptionTier {
|
||||
BASIC = 'basic',
|
||||
PLUS = 'plus',
|
||||
PREMIUM = 'premium',
|
||||
}
|
||||
|
||||
// Tier configuration
|
||||
export const tierConfig: Record<SubscriptionTier, {
|
||||
name: string;
|
||||
priceId: string;
|
||||
features: {
|
||||
maxWatchlistItems: number;
|
||||
maxFamilyMembers: number;
|
||||
darkWebScanFrequency: 'daily' | 'hourly' | 'realtime';
|
||||
exposureRetentionMonths: number;
|
||||
alertChannels: string[];
|
||||
};
|
||||
}> = {
|
||||
[SubscriptionTier.BASIC]: {
|
||||
name: 'Basic',
|
||||
priceId: billingEnv.STRIPE_PRICE_ID_BASIC,
|
||||
features: {
|
||||
maxWatchlistItems: 2,
|
||||
maxFamilyMembers: 1,
|
||||
darkWebScanFrequency: 'daily',
|
||||
exposureRetentionMonths: 12,
|
||||
alertChannels: ['email'],
|
||||
},
|
||||
},
|
||||
[SubscriptionTier.PLUS]: {
|
||||
name: 'Plus',
|
||||
priceId: billingEnv.STRIPE_PRICE_ID_PLUS,
|
||||
features: {
|
||||
maxWatchlistItems: 10,
|
||||
maxFamilyMembers: 3,
|
||||
darkWebScanFrequency: 'hourly',
|
||||
exposureRetentionMonths: 24,
|
||||
alertChannels: ['email', 'push'],
|
||||
},
|
||||
},
|
||||
[SubscriptionTier.PREMIUM]: {
|
||||
name: 'Premium',
|
||||
priceId: billingEnv.STRIPE_PRICE_ID_PREMIUM,
|
||||
features: {
|
||||
maxWatchlistItems: Infinity,
|
||||
maxFamilyMembers: Infinity,
|
||||
darkWebScanFrequency: 'realtime',
|
||||
exposureRetentionMonths: 60,
|
||||
alertChannels: ['email', 'push', 'sms'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Feature gating middleware
|
||||
export function getTierFeatures(tier: SubscriptionTier) {
|
||||
return tierConfig[tier].features;
|
||||
}
|
||||
|
||||
export function checkFeatureAccess(tier: SubscriptionTier, feature: string, value: number): boolean {
|
||||
const features = tierConfig[tier].features;
|
||||
const featureValue = features[feature as keyof typeof features] as number;
|
||||
return value <= featureValue;
|
||||
}
|
||||
22
packages/shared-billing/src/index.ts
Normal file
22
packages/shared-billing/src/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Config
|
||||
export {
|
||||
stripe,
|
||||
billingEnv,
|
||||
SubscriptionTier,
|
||||
tierConfig,
|
||||
getTierFeatures,
|
||||
checkFeatureAccess,
|
||||
} from './config/billing.config';
|
||||
|
||||
// Services
|
||||
export {
|
||||
SubscriptionService,
|
||||
CustomerService,
|
||||
WebhookService,
|
||||
subscriptionService,
|
||||
customerService,
|
||||
webhookService,
|
||||
} from './services/billing.services';
|
||||
|
||||
// Middleware
|
||||
export { requireTier, checkFeatureLimit } from './middleware/billing.middleware';
|
||||
68
packages/shared-billing/src/middleware/billing.middleware.ts
Normal file
68
packages/shared-billing/src/middleware/billing.middleware.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { SubscriptionTier, getTierFeatures } from '../config/billing.config';
|
||||
|
||||
/**
|
||||
* Middleware to check if user has access to required tier features
|
||||
*/
|
||||
export function requireTier(
|
||||
request: NextRequest,
|
||||
requiredTier: SubscriptionTier
|
||||
): NextResponse | null {
|
||||
const userTier = request.headers.get('x-user-tier') as SubscriptionTier;
|
||||
|
||||
if (!userTier) {
|
||||
return NextResponse.json({ error: 'User tier not found' }, { status: 401 });
|
||||
}
|
||||
|
||||
const tierOrder: Record<SubscriptionTier, number> = {
|
||||
[SubscriptionTier.BASIC]: 1,
|
||||
[SubscriptionTier.PLUS]: 2,
|
||||
[SubscriptionTier.PREMIUM]: 3,
|
||||
};
|
||||
|
||||
if (tierOrder[userTier] < tierOrder[requiredTier]) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Feature requires ${requiredTier} tier or higher`,
|
||||
currentTier: userTier,
|
||||
requiredTier,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to check feature limits
|
||||
*/
|
||||
export function checkFeatureLimit(
|
||||
request: NextRequest,
|
||||
feature: string,
|
||||
currentValue: number
|
||||
): NextResponse | null {
|
||||
const userTier = request.headers.get('x-user-tier') as SubscriptionTier;
|
||||
|
||||
if (!userTier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const features = getTierFeatures(userTier);
|
||||
const featureLimit = features[feature as keyof typeof features] as number;
|
||||
|
||||
if (currentValue > featureLimit) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: `Feature limit exceeded`,
|
||||
feature,
|
||||
current: currentValue,
|
||||
limit: featureLimit,
|
||||
tier: userTier,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
223
packages/shared-billing/src/services/billing.services.ts
Normal file
223
packages/shared-billing/src/services/billing.services.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { stripe, SubscriptionTier, tierConfig } from '../config/billing.config';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Subscription service
|
||||
export class SubscriptionService {
|
||||
/**
|
||||
* Create a new subscription for a customer
|
||||
*/
|
||||
async createSubscription(
|
||||
customerId: string,
|
||||
tier: SubscriptionTier,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<Stripe.Subscription> {
|
||||
const priceId = tierConfig[tier].priceId;
|
||||
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: customerId,
|
||||
items: [{ price: priceId }],
|
||||
metadata: metadata,
|
||||
proration_behavior: 'create_prorations',
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a customer's subscription tier
|
||||
*/
|
||||
async updateSubscriptionTier(
|
||||
subscriptionId: string,
|
||||
newTier: SubscriptionTier
|
||||
): Promise<Stripe.Subscription> {
|
||||
const newPriceId = tierConfig[newTier].priceId;
|
||||
|
||||
const subscription = await stripe.subscriptions.update(subscriptionId, {
|
||||
items: [
|
||||
{
|
||||
price: newPriceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
proration_behavior: 'create_prorations',
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a subscription
|
||||
*/
|
||||
async cancelSubscription(
|
||||
subscriptionId: string,
|
||||
atPeriodEnd: boolean = true
|
||||
): Promise<Stripe.Subscription> {
|
||||
const subscription = await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: atPeriodEnd,
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription by ID
|
||||
*/
|
||||
async getSubscription(subscriptionId: string): Promise<Stripe.Subscription | null> {
|
||||
try {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
if (error instanceof Stripe.errors.StripeInvalidRequestError) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer's current subscription
|
||||
*/
|
||||
async getCustomerSubscription(customerId: string): Promise<Stripe.Subscription | null> {
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
status: 'active',
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
return subscriptions.data[0] || null;
|
||||
}
|
||||
}
|
||||
|
||||
// Customer service
|
||||
export class CustomerService {
|
||||
/**
|
||||
* Create a new Stripe customer
|
||||
*/
|
||||
async createCustomer(
|
||||
email: string,
|
||||
name?: string,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<Stripe.Customer> {
|
||||
const customer = await stripe.customers.create({
|
||||
email,
|
||||
name,
|
||||
metadata,
|
||||
});
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create customer by email
|
||||
*/
|
||||
async getOrCreateCustomer(
|
||||
email: string,
|
||||
name?: string
|
||||
): Promise<Stripe.Customer> {
|
||||
const existingCustomers = await stripe.customers.list({
|
||||
email,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (existingCustomers.data.length > 0) {
|
||||
return existingCustomers.data[0];
|
||||
}
|
||||
|
||||
return this.createCustomer(email, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a billing portal session
|
||||
*/
|
||||
async createBillingPortalSession(
|
||||
customerId: string,
|
||||
returnUrl: string
|
||||
): Promise<Stripe.BillingPortal.Session> {
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url: returnUrl,
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer by ID
|
||||
*/
|
||||
async getCustomer(customerId: string): Promise<Stripe.Customer | null> {
|
||||
try {
|
||||
const customer = await stripe.customers.retrieve(customerId);
|
||||
return customer as Stripe.Customer;
|
||||
} catch (error) {
|
||||
if (error instanceof Stripe.errors.StripeInvalidRequestError) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook service
|
||||
export class WebhookService {
|
||||
/**
|
||||
* Construct webhook event from raw body
|
||||
*/
|
||||
constructEvent(
|
||||
rawBody: Buffer | string,
|
||||
signature: string
|
||||
): Stripe.Event {
|
||||
return stripe.webhooks.constructEvent(
|
||||
rawBody,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle webhook event
|
||||
*/
|
||||
async handleWebhook(event: Stripe.Event): Promise<void> {
|
||||
switch (event.type) {
|
||||
case 'customer.subscription.created':
|
||||
case 'customer.subscription.updated':
|
||||
await this.handleSubscriptionChange(event.data.object);
|
||||
break;
|
||||
case 'customer.subscription.deleted':
|
||||
await this.handleSubscriptionDeleted(event.data.object);
|
||||
break;
|
||||
case 'invoice.payment_succeeded':
|
||||
await this.handlePaymentSucceeded(event.data.object);
|
||||
break;
|
||||
case 'invoice.payment_failed':
|
||||
await this.handlePaymentFailed(event.data.object);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSubscriptionChange(subscription: Stripe.Subscription) {
|
||||
console.log(`Subscription ${subscription.id} changed to ${subscription.status}`);
|
||||
// TODO: Update local database
|
||||
}
|
||||
|
||||
private async handleSubscriptionDeleted(subscription: Stripe.Subscription) {
|
||||
console.log(`Subscription ${subscription.id} deleted`);
|
||||
// TODO: Update local database
|
||||
}
|
||||
|
||||
private async handlePaymentSucceeded(invoice: Stripe.Invoice) {
|
||||
console.log(`Payment succeeded for invoice ${invoice.id}`);
|
||||
// TODO: Update usage tracking
|
||||
}
|
||||
|
||||
private async handlePaymentFailed(invoice: Stripe.Invoice) {
|
||||
console.log(`Payment failed for invoice ${invoice.id}`);
|
||||
// TODO: Send notification to customer
|
||||
}
|
||||
}
|
||||
|
||||
// Export instances
|
||||
export const subscriptionService = new SubscriptionService();
|
||||
export const customerService = new CustomerService();
|
||||
export const webhookService = new WebhookService();
|
||||
12
packages/shared-billing/tsconfig.json
Normal file
12
packages/shared-billing/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
12
packages/shared-db/drizzle.config.ts
Normal file
12
packages/shared-db/drizzle.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './prisma/schema.prisma',
|
||||
out: './migrations',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
23
packages/shared-db/package.json
Normal file
23
packages/shared-db/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@shieldsai/shared-db",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate deploy",
|
||||
"db:studio": "prisma studio",
|
||||
"db:format": "prisma format"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.14.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^5.14.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
436
packages/shared-db/prisma/schema.prisma
Normal file
436
packages/shared-db/prisma/schema.prisma
Normal file
@@ -0,0 +1,436 @@
|
||||
// Prisma schema for ShieldAI
|
||||
// All models for the multi-service SaaS platform
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// User & Authentication Models
|
||||
// ============================================
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
name String?
|
||||
image String?
|
||||
role UserRole @default(user)
|
||||
|
||||
// Relationships
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
familyGroups FamilyGroupMember[]
|
||||
subscriptions Subscription[]
|
||||
watchlist WatchlistItem[]
|
||||
exposures Exposure[]
|
||||
alerts Alert[]
|
||||
voiceEnrollments VoiceEnrollment[]
|
||||
spamFeedback SpamFeedback[]
|
||||
|
||||
// Audit
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([email])
|
||||
@@index([role])
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
user
|
||||
family_admin
|
||||
family_member
|
||||
support
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
provider String
|
||||
providerAccountId String
|
||||
access_token String?
|
||||
refresh_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
sessionToken String @unique
|
||||
expires DateTime
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([sessionToken])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Family & Subscription Models
|
||||
// ============================================
|
||||
|
||||
model FamilyGroup {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
ownerId String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
owner User @relation("FamilyGroupOwner", fields: [ownerId], references: [id])
|
||||
members FamilyGroupMember[]
|
||||
subscriptions Subscription[]
|
||||
|
||||
@@index([ownerId])
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
model FamilyGroupMember {
|
||||
id String @id @default(uuid())
|
||||
groupId String
|
||||
userId String
|
||||
role FamilyMemberRole @default(member)
|
||||
joinedAt DateTime @default(now())
|
||||
|
||||
group FamilyGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([groupId, userId])
|
||||
@@index([groupId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum FamilyMemberRole {
|
||||
owner
|
||||
admin
|
||||
member
|
||||
}
|
||||
|
||||
model Subscription {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
familyGroupId String?
|
||||
stripeId String? @unique
|
||||
tier SubscriptionTier @default(basic)
|
||||
status SubscriptionStatus @default(active)
|
||||
currentPeriodStart DateTime
|
||||
currentPeriodEnd DateTime
|
||||
cancelAtPeriodEnd Boolean @default(false)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
familyGroup FamilyGroup? @relation(fields: [familyGroupId], references: [id])
|
||||
|
||||
watchlistItems WatchlistItem[]
|
||||
exposures Exposure[]
|
||||
alerts Alert[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([familyGroupId])
|
||||
@@index([stripeId])
|
||||
@@index([tier])
|
||||
}
|
||||
|
||||
enum SubscriptionTier {
|
||||
basic
|
||||
plus
|
||||
premium
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
active
|
||||
past_due
|
||||
canceled
|
||||
unpaid
|
||||
trialing
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DarkWatch Models (Dark Web Monitoring)
|
||||
// ============================================
|
||||
|
||||
model WatchlistItem {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
type WatchlistType
|
||||
value String
|
||||
hash String // SHA-256 hash for deduplication
|
||||
isActive Boolean @default(true)
|
||||
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
exposures Exposure[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([subscriptionId, type, hash])
|
||||
@@index([subscriptionId])
|
||||
@@index([type])
|
||||
@@index([hash])
|
||||
}
|
||||
|
||||
enum WatchlistType {
|
||||
email
|
||||
phoneNumber
|
||||
ssn
|
||||
address
|
||||
domain
|
||||
}
|
||||
|
||||
model Exposure {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
watchlistItemId String?
|
||||
source ExposureSource
|
||||
dataType WatchlistType
|
||||
identifier String
|
||||
identifierHash String
|
||||
severity ExposureSeverity @default(info)
|
||||
metadata Json? // Additional source-specific data
|
||||
isFirstTime Boolean @default(false)
|
||||
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
watchlistItem WatchlistItem? @relation(fields: [watchlistItemId], references: [id])
|
||||
alerts Alert[]
|
||||
|
||||
detectedAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([subscriptionId])
|
||||
@@index([watchlistItemId])
|
||||
@@index([source])
|
||||
@@index([severity])
|
||||
@@index([detectedAt])
|
||||
}
|
||||
|
||||
enum ExposureSource {
|
||||
hibp // Have I Been Pwned
|
||||
securityTrails
|
||||
censys
|
||||
darkWebForum
|
||||
shodan
|
||||
honeypot
|
||||
}
|
||||
|
||||
enum ExposureSeverity {
|
||||
info
|
||||
warning
|
||||
critical
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Notification & Alert Models
|
||||
// ============================================
|
||||
|
||||
model Alert {
|
||||
id String @id @default(uuid())
|
||||
subscriptionId String
|
||||
userId String
|
||||
exposureId String?
|
||||
type AlertType
|
||||
title String
|
||||
message String
|
||||
severity AlertSeverity @default(info)
|
||||
isRead Boolean @default(false)
|
||||
readAt DateTime?
|
||||
channel AlertChannel[] // Array of notification channels
|
||||
|
||||
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
exposure Exposure? @relation(fields: [exposureId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([subscriptionId])
|
||||
@@index([userId])
|
||||
@@index([isRead])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
enum AlertType {
|
||||
exposure_detected
|
||||
exposure_resolved
|
||||
scan_complete
|
||||
subscription_changed
|
||||
system_warning
|
||||
}
|
||||
|
||||
enum AlertSeverity {
|
||||
info
|
||||
warning
|
||||
critical
|
||||
}
|
||||
|
||||
enum AlertChannel {
|
||||
email
|
||||
push
|
||||
sms
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VoicePrint Models (Voice Cloning Detection)
|
||||
// ============================================
|
||||
|
||||
model VoiceEnrollment {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
name String
|
||||
voiceHash String // FAISS embedding hash
|
||||
audioMetadata Json? // Sample rate, duration, etc.
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
analyses VoiceAnalysis[]
|
||||
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([voiceHash])
|
||||
}
|
||||
|
||||
model VoiceAnalysis {
|
||||
id String @id @default(uuid())
|
||||
enrollmentId String?
|
||||
userId String
|
||||
audioHash String // Content hash of audio file
|
||||
isSynthetic Boolean
|
||||
confidence Float // 0.0 to 1.0
|
||||
analysisResult Json // Full ML analysis results
|
||||
audioUrl String // S3 storage URL
|
||||
|
||||
enrollment VoiceEnrollment? @relation(fields: [enrollmentId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([enrollmentId])
|
||||
@@index([audioHash])
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SpamShield Models (Spam Detection)
|
||||
// ============================================
|
||||
|
||||
model SpamFeedback {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
phoneNumber String
|
||||
phoneNumberHash String // SHA-256 hash
|
||||
isSpam Boolean
|
||||
confidence Float? // ML model confidence
|
||||
feedbackType FeedbackType
|
||||
metadata Json? // Call duration, time, etc.
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([phoneNumberHash])
|
||||
@@index([isSpam])
|
||||
}
|
||||
|
||||
enum FeedbackType {
|
||||
initial_detection
|
||||
user_confirmation
|
||||
user_rejection
|
||||
auto_learned
|
||||
}
|
||||
|
||||
model SpamRule {
|
||||
id String @id @default(uuid())
|
||||
userId String?
|
||||
isGlobal Boolean @default(false)
|
||||
ruleType RuleType
|
||||
pattern String
|
||||
action RuleAction
|
||||
priority Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([isGlobal])
|
||||
@@index([ruleType])
|
||||
}
|
||||
|
||||
enum RuleType {
|
||||
phoneNumber
|
||||
areaCode
|
||||
prefix
|
||||
pattern
|
||||
reputation
|
||||
}
|
||||
|
||||
enum RuleAction {
|
||||
block
|
||||
flag
|
||||
allow
|
||||
challenge
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Audit & Analytics Models
|
||||
// ============================================
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
userId String?
|
||||
action String
|
||||
resource String
|
||||
resourceId String?
|
||||
changes Json? // Before/after values
|
||||
metadata Json?
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([action])
|
||||
@@index([resource])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model KPISnapshot {
|
||||
id String @id @default(uuid())
|
||||
date DateTime @unique
|
||||
metricName String
|
||||
metricValue Float
|
||||
metadata Json?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([metricName])
|
||||
@@index([date])
|
||||
}
|
||||
50
packages/shared-db/src/client.ts
Normal file
50
packages/shared-db/src/client.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { PrismaClient } from './generated/client';
|
||||
|
||||
// Singleton pattern for Prisma Client
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
|
||||
// Export types from generated client
|
||||
export type {
|
||||
User,
|
||||
Account,
|
||||
Session,
|
||||
FamilyGroup,
|
||||
FamilyGroupMember,
|
||||
Subscription,
|
||||
WatchlistItem,
|
||||
Exposure,
|
||||
Alert,
|
||||
VoiceEnrollment,
|
||||
VoiceAnalysis,
|
||||
SpamFeedback,
|
||||
SpamRule,
|
||||
AuditLog,
|
||||
KPISnapshot,
|
||||
UserRole,
|
||||
FamilyMemberRole,
|
||||
SubscriptionTier,
|
||||
SubscriptionStatus,
|
||||
WatchlistType,
|
||||
ExposureSource,
|
||||
ExposureSeverity,
|
||||
AlertType,
|
||||
AlertSeverity,
|
||||
AlertChannel,
|
||||
FeedbackType,
|
||||
RuleType,
|
||||
RuleAction,
|
||||
} from './generated/client';
|
||||
|
||||
export * as PrismaModels from './generated/client';
|
||||
21
packages/shared-db/src/index.ts
Normal file
21
packages/shared-db/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Re-export Prisma client
|
||||
export { prisma } from './client';
|
||||
|
||||
// Export types
|
||||
export type {
|
||||
User,
|
||||
Account,
|
||||
Session,
|
||||
FamilyGroup,
|
||||
FamilyGroupMember,
|
||||
Subscription,
|
||||
WatchlistItem,
|
||||
Exposure,
|
||||
Alert,
|
||||
VoiceEnrollment,
|
||||
VoiceAnalysis,
|
||||
SpamFeedback,
|
||||
SpamRule,
|
||||
AuditLog,
|
||||
KPISnapshot,
|
||||
} from './client';
|
||||
12
packages/shared-db/tsconfig.json
Normal file
12
packages/shared-db/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "prisma"]
|
||||
}
|
||||
17
packages/shared-ui/package.json
Normal file
17
packages/shared-ui/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@shieldsai/shared-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.tsx",
|
||||
"types": "src/index.tsx",
|
||||
"scripts": {
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"solid-js": "^1.8.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
16
packages/shared-utils/package.json
Normal file
16
packages/shared-utils/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@shieldsai/shared-utils",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/",
|
||||
"test": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.3.1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user