Some checks failed
CI/CD Pipeline / Backend - Lint & Test (push) Has been cancelled
CI/CD Pipeline / Frontend - Lint & Test (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Build Backend (push) Has been cancelled
CI/CD Pipeline / Build Frontend (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Tests (push) Has been cancelled
CI/CD Pipeline / Dependency Updates (push) Has been cancelled
- Updated Anthropic API to latest version (2024-01-01) - Set Claude 3.7 Sonnet Latest as primary model - Removed deprecated Opus 3.5 references - Fixed LLM response validation and JSON parsing - Improved error handling and logging - Updated model configurations and pricing - Enhanced document processing reliability - Fixed TypeScript type issues - Updated environment configuration
391 lines
10 KiB
TypeScript
391 lines
10 KiB
TypeScript
import { getSupabaseServiceClient } from '../config/supabase';
|
|
import { User, CreateUserInput } from './types';
|
|
import logger from '../utils/logger';
|
|
import { redisCacheService } from '../services/redisCacheService';
|
|
|
|
export class UserModel {
|
|
/**
|
|
* Create a new user
|
|
*/
|
|
static async create(userData: CreateUserInput): Promise<User> {
|
|
const { email, name, password, role = 'user' } = userData;
|
|
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.insert({
|
|
email,
|
|
name,
|
|
password_hash: password, // Note: In production, this should be hashed
|
|
role
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
logger.error('Error creating user:', error);
|
|
throw error;
|
|
}
|
|
|
|
logger.info(`Created user: ${email}`);
|
|
return data;
|
|
} catch (error) {
|
|
logger.error('Error creating user:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find user by ID
|
|
*/
|
|
static async findById(id: string): Promise<User | null> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('*')
|
|
.eq('id', id)
|
|
.eq('is_active', true)
|
|
.single();
|
|
|
|
if (error) {
|
|
if (error.code === 'PGRST116') {
|
|
return null; // No rows returned
|
|
}
|
|
logger.error('Error finding user by ID:', error);
|
|
throw error;
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
logger.error('Error finding user by ID:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find user by email
|
|
*/
|
|
static async findByEmail(email: string): Promise<User | null> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('*')
|
|
.eq('email', email)
|
|
.eq('is_active', true)
|
|
.single();
|
|
|
|
if (error) {
|
|
if (error.code === 'PGRST116') {
|
|
return null; // No rows returned
|
|
}
|
|
logger.error('Error finding user by email:', error);
|
|
throw error;
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
logger.error('Error finding user by email:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all users (for admin)
|
|
*/
|
|
static async findAll(limit = 100, offset = 0): Promise<User[]> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('*')
|
|
.eq('is_active', true)
|
|
.order('created_at', { ascending: false })
|
|
.range(offset, offset + limit - 1);
|
|
|
|
if (error) {
|
|
logger.error('Error finding all users:', error);
|
|
throw error;
|
|
}
|
|
|
|
return data || [];
|
|
} catch (error) {
|
|
logger.error('Error finding all users:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update user
|
|
*/
|
|
static async update(id: string, updates: Partial<User>): Promise<User | null> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.update({
|
|
...updates,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
if (error.code === 'PGRST116') {
|
|
return null; // No rows returned
|
|
}
|
|
logger.error('Error updating user:', error);
|
|
throw error;
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
logger.error('Error updating user:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update last login
|
|
*/
|
|
static async updateLastLogin(id: string): Promise<void> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('users')
|
|
.update({
|
|
last_login: new Date().toISOString(),
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', id);
|
|
|
|
if (error) {
|
|
logger.error('Error updating last login:', error);
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error updating last login:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete user (soft delete)
|
|
*/
|
|
static async delete(id: string): Promise<boolean> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('users')
|
|
.update({
|
|
is_active: false,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', id);
|
|
|
|
if (error) {
|
|
logger.error('Error deleting user:', error);
|
|
throw error;
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Error deleting user:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Count users
|
|
*/
|
|
static async count(): Promise<number> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { count, error } = await supabase
|
|
.from('users')
|
|
.select('*', { count: 'exact', head: true })
|
|
.eq('is_active', true);
|
|
|
|
if (error) {
|
|
logger.error('Error counting users:', error);
|
|
throw error;
|
|
}
|
|
|
|
return count || 0;
|
|
} catch (error) {
|
|
logger.error('Error counting users:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if email exists
|
|
*/
|
|
static async emailExists(email: string): Promise<boolean> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('id')
|
|
.eq('email', email)
|
|
.eq('is_active', true)
|
|
.limit(1);
|
|
|
|
if (error) {
|
|
logger.error('Error checking if email exists:', error);
|
|
throw error;
|
|
}
|
|
|
|
return (data && data.length > 0);
|
|
} catch (error) {
|
|
logger.error('Error checking if email exists:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all users (admin only)
|
|
*/
|
|
static async getAllUsers(): Promise<any[]> {
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select('*')
|
|
.eq('is_active', true)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
logger.error('Error getting all users:', error);
|
|
throw error;
|
|
}
|
|
|
|
return data || [];
|
|
} catch (error) {
|
|
logger.error('Error getting all users:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user activity statistics (admin only) with Redis caching
|
|
*/
|
|
static async getUserActivityStats(): Promise<any[]> {
|
|
const cacheKey = 'user_activity_stats';
|
|
|
|
try {
|
|
// Try to get from cache first
|
|
const cachedData = await redisCacheService.get<any[]>(cacheKey, { prefix: 'analytics' });
|
|
if (cachedData) {
|
|
logger.info('User activity stats retrieved from cache');
|
|
return cachedData;
|
|
}
|
|
|
|
const supabase = getSupabaseServiceClient();
|
|
|
|
// Get users with their document counts and last activity
|
|
const { data, error } = await supabase
|
|
.from('users')
|
|
.select(`
|
|
id,
|
|
email,
|
|
name,
|
|
created_at,
|
|
last_login,
|
|
is_active
|
|
`)
|
|
.eq('is_active', true)
|
|
.order('last_login', { ascending: false });
|
|
|
|
if (error) {
|
|
logger.error('Error getting user activity stats:', error);
|
|
throw error;
|
|
}
|
|
|
|
// Get document statistics for each user
|
|
const usersWithStats = await Promise.all(
|
|
(data || []).map(async (user) => {
|
|
try {
|
|
// Get document count for user
|
|
const { data: docData, error: docError } = await supabase
|
|
.from('documents')
|
|
.select('id, processing_time_ms, created_at')
|
|
.eq('user_id', user.id)
|
|
.eq('deleted_at', null);
|
|
|
|
if (docError) {
|
|
logger.error('Error getting document stats for user:', docError);
|
|
return {
|
|
userId: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
loginCount: 1,
|
|
lastLogin: user.last_login,
|
|
documentsProcessed: 0,
|
|
totalProcessingTime: 0,
|
|
averageProcessingTime: 0
|
|
};
|
|
}
|
|
|
|
const documents = docData || [];
|
|
const totalProcessingTime = documents.reduce((sum, doc) => sum + (doc.processing_time_ms || 0), 0);
|
|
const averageProcessingTime = documents.length > 0 ? totalProcessingTime / documents.length : 0;
|
|
|
|
// Get login count from processing jobs (as a proxy for activity)
|
|
const { data: jobData, error: jobError } = await supabase
|
|
.from('processing_jobs')
|
|
.select('id')
|
|
.eq('user_id', user.id);
|
|
|
|
const loginCount = jobError ? 1 : (jobData?.length || 1);
|
|
|
|
return {
|
|
userId: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
loginCount,
|
|
lastLogin: user.last_login,
|
|
documentsProcessed: documents.length,
|
|
totalProcessingTime,
|
|
averageProcessingTime: Math.round(averageProcessingTime)
|
|
};
|
|
} catch (error) {
|
|
logger.error('Error calculating stats for user:', error);
|
|
return {
|
|
userId: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
loginCount: 1,
|
|
lastLogin: user.last_login,
|
|
documentsProcessed: 0,
|
|
totalProcessingTime: 0,
|
|
averageProcessingTime: 0
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
// Cache the results for 30 minutes
|
|
await redisCacheService.set(cacheKey, usersWithStats, { ttl: 1800, prefix: 'analytics' });
|
|
logger.info('User activity stats cached successfully');
|
|
|
|
return usersWithStats;
|
|
} catch (error) {
|
|
logger.error('Error getting user activity stats:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|