Set up the basic structure and core functionality for the application

Initialize project with UI components, core libraries, and basic app structure.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 556aa286-edd2-4cea-8583-f4fc3cfd119b
This commit is contained in:
ghaddaditw
2025-05-30 17:08:53 +00:00
parent b47089ae5d
commit cced901e9a
100 changed files with 20488 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
import { Response } from 'express';
import { aiService } from '../services/aiService';
import { storage } from '../storage';
import { AuthenticatedRequest } from '../middleware/auth';
export const aiController = {
async getDailyJoke(req: any, res: Response) {
try {
// Check if we already have a joke for today
const today = new Date();
today.setHours(0, 0, 0, 0);
const existingJokes = await storage.getAIInteractions(undefined, 'joke', 1);
const todayJoke = existingJokes.find(joke => {
const jokeDate = new Date(joke.createdAt);
jokeDate.setHours(0, 0, 0, 0);
return jokeDate.getTime() === today.getTime();
});
if (todayJoke) {
return res.json({
joke: todayJoke.response,
cached: true,
timestamp: todayJoke.createdAt
});
}
// Generate new daily joke
const jokeResponse = await aiService.generateDailyJoke();
res.json({
joke: jokeResponse.content,
category: jokeResponse.category,
cached: false,
processingTime: jokeResponse.processingTime,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Get daily joke error:', error);
res.status(500).json({ error: 'Failed to get daily joke' });
}
},
async generateJoke(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { personalized } = req.query;
let jokeResponse;
if (personalized === 'true') {
jokeResponse = await aiService.generatePersonalizedJoke(req.user.id);
} else {
jokeResponse = await aiService.generateDailyJoke();
}
res.json({
joke: jokeResponse.content,
category: jokeResponse.category,
personalized: personalized === 'true',
processingTime: jokeResponse.processingTime,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Generate joke error:', error);
res.status(500).json({ error: 'Failed to generate joke' });
}
},
async chat(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
const response = await aiService.chat(req.user.id, message);
res.json({
response: response.content,
processingTime: response.processingTime,
modelUsed: response.modelUsed,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('AI chat error:', error);
res.status(500).json({ error: 'Failed to process chat message' });
}
},
async getInteractions(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { type, limit } = req.query;
const interactions = await storage.getAIInteractions(
req.user.id,
type as string,
limit ? parseInt(limit as string) : undefined
);
res.json({ interactions });
} catch (error) {
console.error('Get AI interactions error:', error);
res.status(500).json({ error: 'Failed to fetch AI interactions' });
}
},
async getStatus(req: AuthenticatedRequest, res: Response) {
try {
const status = await aiService.getModelStatus();
res.json({
...status,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Get AI status error:', error);
res.status(500).json({ error: 'Failed to get AI status' });
}
},
async generateInsight(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { type } = req.body;
if (!type || !['financial', 'productivity', 'general'].includes(type)) {
return res.status(400).json({ error: 'Valid insight type is required (financial, productivity, general)' });
}
const insight = await aiService.generateInsight(req.user.id, type);
res.json({
insight: insight.content,
type,
processingTime: insight.processingTime,
modelUsed: insight.modelUsed,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Generate insight error:', error);
res.status(500).json({ error: 'Failed to generate insight' });
}
}
};
+185
View File
@@ -0,0 +1,185 @@
import { Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { storage } from '../storage';
import { loginSchema, registerSchema } from '@shared/schema';
import { AuthenticatedRequest } from '../middleware/auth';
export const authController = {
async register(req: Request, res: Response) {
try {
const validatedData = registerSchema.parse(req.body);
// Check if user already exists
const existingUser = await storage.getUserByEmail(validatedData.email);
if (existingUser) {
return res.status(400).json({ error: 'User already exists with this email' });
}
const existingUsername = await storage.getUserByUsername(validatedData.username);
if (existingUsername) {
return res.status(400).json({ error: 'Username already taken' });
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(validatedData.password, saltRounds);
// Create user
const user = await storage.createUser({
username: validatedData.username,
email: validatedData.email,
password: hashedPassword,
role: validatedData.role || 'standard',
firstName: validatedData.firstName,
lastName: validatedData.lastName,
onboardingComplete: false,
voiceEnabled: true,
ttsEnabled: true
});
// Remove password from response
const { password, ...userWithoutPassword } = user;
res.status(201).json({
message: 'User created successfully',
user: userWithoutPassword
});
} catch (error) {
console.error('Registration error:', error);
if (error instanceof Error && error.name === 'ZodError') {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Failed to create user' });
}
},
async login(req: Request, res: Response) {
try {
const validatedData = loginSchema.parse(req.body);
const user = await storage.getUserByEmail(validatedData.email);
if (!user) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const isValidPassword = await bcrypt.compare(validatedData.password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: 'Invalid email or password' });
}
// Use passport login
req.login(user, (err) => {
if (err) {
console.error('Login error:', err);
return res.status(500).json({ error: 'Login failed' });
}
const { password, ...userWithoutPassword } = user;
res.json({
message: 'Login successful',
user: userWithoutPassword
});
});
} catch (error) {
console.error('Login error:', error);
if (error instanceof Error && error.name === 'ZodError') {
return res.status(400).json({ error: 'Invalid input data' });
}
res.status(500).json({ error: 'Login failed' });
}
},
async logout(req: Request, res: Response) {
req.logout((err) => {
if (err) {
console.error('Logout error:', err);
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ message: 'Logout successful' });
});
},
async me(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { password, ...userWithoutPassword } = req.user;
res.json({ user: userWithoutPassword });
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({ error: 'Failed to get user information' });
}
},
async updateProfile(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const allowedUpdates = ['firstName', 'lastName', 'voiceEnabled', 'ttsEnabled'];
const updates: any = {};
for (const field of allowedUpdates) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No valid fields to update' });
}
const updatedUser = await storage.updateUser(req.user.id, updates);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
const { password, ...userWithoutPassword } = updatedUser;
res.json({
message: 'Profile updated successfully',
user: userWithoutPassword
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({ error: 'Failed to update profile' });
}
},
async completeOnboarding(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { role, preferences } = req.body;
// Update user onboarding status and role if provided
const updates: any = { onboardingComplete: true };
if (role && ['standard', 'pro', 'admin'].includes(role)) {
updates.role = role;
}
const updatedUser = await storage.updateUser(req.user.id, updates);
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
// Save user preferences if provided
if (preferences) {
await storage.updateUserPreferences(req.user.id, preferences);
}
const { password, ...userWithoutPassword } = updatedUser;
res.json({
message: 'Onboarding completed successfully',
user: userWithoutPassword
});
} catch (error) {
console.error('Complete onboarding error:', error);
res.status(500).json({ error: 'Failed to complete onboarding' });
}
}
};
+181
View File
@@ -0,0 +1,181 @@
import { Response } from 'express';
import { storage } from '../storage';
import { financialRecordCreateSchema } from '@shared/schema';
import { AuthenticatedRequest } from '../middleware/auth';
export const financialController = {
async getRecords(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { startDate, endDate } = req.query;
let start: Date | undefined;
let end: Date | undefined;
if (startDate) {
start = new Date(startDate as string);
}
if (endDate) {
end = new Date(endDate as string);
}
const records = await storage.getFinancialRecords(req.user.id, start, end);
res.json({ records });
} catch (error) {
console.error('Get financial records error:', error);
res.status(500).json({ error: 'Failed to fetch financial records' });
}
},
async getSummary(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { startDate, endDate, period } = req.query;
let start: Date | undefined;
let end: Date | undefined;
if (period) {
const now = new Date();
switch (period) {
case 'week':
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
end = now;
break;
case 'month':
start = new Date(now.getFullYear(), now.getMonth(), 1);
end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
break;
case 'year':
start = new Date(now.getFullYear(), 0, 1);
end = new Date(now.getFullYear(), 11, 31);
break;
}
} else {
if (startDate) start = new Date(startDate as string);
if (endDate) end = new Date(endDate as string);
}
const summary = await storage.getFinancialSummary(req.user.id, start, end);
res.json({
summary,
period: period || 'custom',
startDate: start?.toISOString(),
endDate: end?.toISOString()
});
} catch (error) {
console.error('Get financial summary error:', error);
res.status(500).json({ error: 'Failed to fetch financial summary' });
}
},
async createRecord(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const validatedData = financialRecordCreateSchema.parse(req.body);
const record = await storage.createFinancialRecord({
userId: req.user.id,
type: validatedData.type,
amount: validatedData.amount.toString(),
category: validatedData.category,
description: validatedData.description,
date: new Date(validatedData.date),
createdViaVoice: req.body.createdViaVoice || false,
voiceTranscription: req.body.voiceTranscription,
metadata: req.body.metadata
});
res.status(201).json({
message: 'Financial record created successfully',
record
});
} catch (error) {
console.error('Create financial record error:', error);
if (error instanceof Error && error.name === 'ZodError') {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Failed to create financial record' });
}
},
async updateRecord(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const recordId = parseInt(req.params.id);
if (isNaN(recordId)) {
return res.status(400).json({ error: 'Invalid record ID' });
}
const allowedUpdates = ['type', 'amount', 'category', 'description', 'date', 'metadata'];
const updates: any = {};
for (const field of allowedUpdates) {
if (req.body[field] !== undefined) {
if (field === 'date') {
updates[field] = new Date(req.body[field]);
} else if (field === 'amount') {
updates[field] = req.body[field].toString();
} else {
updates[field] = req.body[field];
}
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No valid fields to update' });
}
const record = await storage.updateFinancialRecord(recordId, req.user.id, updates);
if (!record) {
return res.status(404).json({ error: 'Financial record not found' });
}
res.json({
message: 'Financial record updated successfully',
record
});
} catch (error) {
console.error('Update financial record error:', error);
res.status(500).json({ error: 'Failed to update financial record' });
}
},
async deleteRecord(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const recordId = parseInt(req.params.id);
if (isNaN(recordId)) {
return res.status(400).json({ error: 'Invalid record ID' });
}
const deleted = await storage.deleteFinancialRecord(recordId, req.user.id);
if (!deleted) {
return res.status(404).json({ error: 'Financial record not found' });
}
res.json({ message: 'Financial record deleted successfully' });
} catch (error) {
console.error('Delete financial record error:', error);
res.status(500).json({ error: 'Failed to delete financial record' });
}
}
};
+150
View File
@@ -0,0 +1,150 @@
import { Response } from 'express';
import { storage } from '../storage';
import { taskCreateSchema } from '@shared/schema';
import { AuthenticatedRequest } from '../middleware/auth';
export const taskController = {
async getTasks(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { status } = req.query;
const tasks = await storage.getTasks(req.user.id, status as string);
res.json({ tasks });
} catch (error) {
console.error('Get tasks error:', error);
res.status(500).json({ error: 'Failed to fetch tasks' });
}
},
async getTask(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ error: 'Invalid task ID' });
}
const task = await storage.getTask(taskId, req.user.id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ task });
} catch (error) {
console.error('Get task error:', error);
res.status(500).json({ error: 'Failed to fetch task' });
}
},
async createTask(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const validatedData = taskCreateSchema.parse(req.body);
const task = await storage.createTask({
userId: req.user.id,
title: validatedData.title,
description: validatedData.description,
priority: validatedData.priority,
dueDate: validatedData.dueDate ? new Date(validatedData.dueDate) : undefined,
status: 'pending',
createdViaVoice: req.body.createdViaVoice || false,
voiceTranscription: req.body.voiceTranscription
});
res.status(201).json({
message: 'Task created successfully',
task
});
} catch (error) {
console.error('Create task error:', error);
if (error instanceof Error && error.name === 'ZodError') {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Failed to create task' });
}
},
async updateTask(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ error: 'Invalid task ID' });
}
const allowedUpdates = ['title', 'description', 'priority', 'status', 'dueDate'];
const updates: any = {};
for (const field of allowedUpdates) {
if (req.body[field] !== undefined) {
if (field === 'dueDate' && req.body[field]) {
updates[field] = new Date(req.body[field]);
} else {
updates[field] = req.body[field];
}
}
}
// If marking as completed, set completedAt timestamp
if (updates.status === 'completed') {
updates.completedAt = new Date();
} else if (updates.status && updates.status !== 'completed') {
updates.completedAt = null;
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No valid fields to update' });
}
const task = await storage.updateTask(taskId, req.user.id, updates);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({
message: 'Task updated successfully',
task
});
} catch (error) {
console.error('Update task error:', error);
res.status(500).json({ error: 'Failed to update task' });
}
},
async deleteTask(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const taskId = parseInt(req.params.id);
if (isNaN(taskId)) {
return res.status(400).json({ error: 'Invalid task ID' });
}
const deleted = await storage.deleteTask(taskId, req.user.id);
if (!deleted) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ message: 'Task deleted successfully' });
} catch (error) {
console.error('Delete task error:', error);
res.status(500).json({ error: 'Failed to delete task' });
}
}
};
+203
View File
@@ -0,0 +1,203 @@
import { Response } from 'express';
import multer from 'multer';
import { voiceService } from '../services/voiceService';
import { storage } from '../storage';
import { AuthenticatedRequest } from '../middleware/auth';
// Configure multer for audio file uploads
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('audio/')) {
cb(null, true);
} else {
cb(new Error('Only audio files are allowed'));
}
}
});
export const voiceController = {
async initialize(req: AuthenticatedRequest, res: Response) {
try {
const success = await voiceService.initialize();
if (success) {
res.json({
message: 'Voice models initialized successfully',
status: 'ready'
});
} else {
res.status(500).json({ error: 'Failed to initialize voice models' });
}
} catch (error) {
console.error('Voice initialization error:', error);
res.status(500).json({ error: 'Voice initialization failed' });
}
},
async processCommand(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { audioData } = req.body;
if (!audioData) {
return res.status(400).json({ error: 'Audio data is required' });
}
// Convert base64 audio to buffer
const audioBuffer = Buffer.from(audioData, 'base64');
// Process audio to text
const result = await voiceService.processAudioToText(audioBuffer, req.user.id);
// Execute the command based on intent
let actionResult = null;
if (result.intent && result.entities) {
actionResult = await this.executeVoiceCommand(req.user.id, result.intent, result.entities);
}
res.json({
transcription: result.transcription,
intent: result.intent,
confidence: result.confidence,
processingTime: result.processingTime,
actionResult
});
} catch (error) {
console.error('Voice command processing error:', error);
res.status(500).json({ error: 'Failed to process voice command' });
}
},
async speak(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { text, voice } = req.body;
if (!text) {
return res.status(400).json({ error: 'Text is required' });
}
const result = await voiceService.textToSpeech(text, voice);
// Set appropriate headers for audio response
res.set({
'Content-Type': 'audio/wav',
'Content-Length': result.audioBuffer.length
});
res.send(result.audioBuffer);
} catch (error) {
console.error('Text-to-speech error:', error);
res.status(500).json({ error: 'Failed to generate speech' });
}
},
async getCommands(req: AuthenticatedRequest, res: Response) {
try {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
const { limit } = req.query;
const commands = await storage.getVoiceCommands(req.user.id, limit ? parseInt(limit as string) : undefined);
res.json({ commands });
} catch (error) {
console.error('Get voice commands error:', error);
res.status(500).json({ error: 'Failed to fetch voice commands' });
}
},
async getStatus(req: AuthenticatedRequest, res: Response) {
try {
const status = await voiceService.getModelStatus();
res.json({
...status,
uptime: process.uptime(),
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Get voice status error:', error);
res.status(500).json({ error: 'Failed to get voice status' });
}
},
async executeVoiceCommand(userId: number, intent: string, entities: Record<string, any>) {
try {
switch (intent) {
case 'task_create':
if (entities.title) {
const task = await storage.createTask({
userId,
title: entities.title,
description: entities.description,
priority: entities.priority || 'medium',
dueDate: entities.dueDate ? new Date(entities.dueDate) : undefined,
status: 'pending',
createdViaVoice: true,
voiceTranscription: entities.originalTranscription
});
return { type: 'task_created', task };
}
break;
case 'expense_add':
if (entities.amount && entities.category) {
const record = await storage.createFinancialRecord({
userId,
type: 'expense',
amount: entities.amount.toString(),
category: entities.category,
description: entities.description || 'Voice expense entry',
date: new Date(),
createdViaVoice: true,
voiceTranscription: entities.originalTranscription
});
return { type: 'expense_added', record };
}
break;
case 'income_add':
if (entities.amount) {
const record = await storage.createFinancialRecord({
userId,
type: 'income',
amount: entities.amount.toString(),
category: entities.category || 'income',
description: entities.description || 'Voice income entry',
date: new Date(),
createdViaVoice: true,
voiceTranscription: entities.originalTranscription
});
return { type: 'income_added', record };
}
break;
case 'task_list':
const tasks = await storage.getTasks(userId, 'pending');
return { type: 'tasks_retrieved', tasks: tasks.slice(0, 5) }; // Limit for voice response
case 'financial_summary':
const summary = await storage.getFinancialSummary(userId);
return { type: 'financial_summary', summary };
default:
return { type: 'unknown_intent', message: 'I didn\'t understand that command' };
}
} catch (error) {
console.error('Execute voice command error:', error);
return { type: 'error', message: 'Failed to execute command' };
}
}
};
+15
View File
@@ -0,0 +1,15 @@
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from "ws";
import * as schema from "@shared/schema";
neonConfig.webSocketConstructor = ws;
if (!process.env.DATABASE_URL) {
throw new Error(
"DATABASE_URL must be set. Did you forget to provision a database?",
);
}
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });
+70
View File
@@ -0,0 +1,70 @@
import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } from "./routes";
import { setupVite, serveStatic, log } from "./vite";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;
const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}
log(logLine);
}
});
next();
});
(async () => {
const server = await registerRoutes(app);
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
const status = err.status || err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 5000
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = 5000;
server.listen({
port,
host: "0.0.0.0",
reusePort: true,
}, () => {
log(`serving on port ${port}`);
});
})();
+52
View File
@@ -0,0 +1,52 @@
import { Request, Response, NextFunction } from 'express';
import { storage } from '../storage';
export interface AuthenticatedRequest extends Request {
user?: any;
}
export const authMiddleware = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
// Refresh user data from database
const user = await storage.getUser(req.user.id);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
} catch (error) {
console.error('Auth middleware error:', error);
res.status(500).json({ error: 'Authentication error' });
}
};
export const requireRole = (roles: string[]) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
export const requireOnboarding = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!req.user.onboardingComplete) {
return res.status(403).json({ error: 'Onboarding required', onboardingRequired: true });
}
next();
};
+136
View File
@@ -0,0 +1,136 @@
import type { Express } from "express";
import { createServer, type Server } from "http";
import express from "express";
import session from "express-session";
import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import bcrypt from "bcryptjs";
import { storage } from "./storage";
import { authMiddleware, requireRole } from "./middleware/auth";
import { authController } from "./controllers/authController";
import { taskController } from "./controllers/taskController";
import { financialController } from "./controllers/financialController";
import { voiceController } from "./controllers/voiceController";
import { aiController } from "./controllers/aiController";
// Configure passport
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await storage.getUserByEmail(email);
if (!user) {
return done(null, false, { message: 'User not found' });
}
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return done(null, false, { message: 'Invalid password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
passport.serializeUser((user: any, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id: number, done) => {
try {
const user = await storage.getUser(id);
done(null, user);
} catch (error) {
done(error);
}
});
export async function registerRoutes(app: Express): Promise<Server> {
// Configure session middleware
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
app.use(passport.initialize());
app.use(passport.session());
// Authentication routes
app.post('/api/auth/register', authController.register);
app.post('/api/auth/login', authController.login);
app.post('/api/auth/logout', authController.logout);
app.get('/api/auth/me', authMiddleware, authController.me);
app.put('/api/auth/profile', authMiddleware, authController.updateProfile);
app.post('/api/auth/complete-onboarding', authMiddleware, authController.completeOnboarding);
// Task routes
app.get('/api/tasks', authMiddleware, taskController.getTasks);
app.get('/api/tasks/:id', authMiddleware, taskController.getTask);
app.post('/api/tasks', authMiddleware, taskController.createTask);
app.put('/api/tasks/:id', authMiddleware, taskController.updateTask);
app.delete('/api/tasks/:id', authMiddleware, taskController.deleteTask);
// Financial routes
app.get('/api/financial/records', authMiddleware, financialController.getRecords);
app.get('/api/financial/summary', authMiddleware, financialController.getSummary);
app.post('/api/financial/records', authMiddleware, financialController.createRecord);
app.put('/api/financial/records/:id', authMiddleware, financialController.updateRecord);
app.delete('/api/financial/records/:id', authMiddleware, financialController.deleteRecord);
// Voice processing routes
app.post('/api/voice/initialize', authMiddleware, voiceController.initialize);
app.post('/api/voice/process-command', authMiddleware, voiceController.processCommand);
app.post('/api/voice/speak', authMiddleware, voiceController.speak);
app.get('/api/voice/commands', authMiddleware, voiceController.getCommands);
app.get('/api/voice/status', authMiddleware, voiceController.getStatus);
// AI routes
app.get('/api/ai/daily-joke', aiController.getDailyJoke);
app.post('/api/ai/generate-joke', authMiddleware, aiController.generateJoke);
app.post('/api/ai/chat', authMiddleware, aiController.chat);
app.get('/api/ai/interactions', authMiddleware, aiController.getInteractions);
app.get('/api/ai/status', authMiddleware, aiController.getStatus);
// Admin routes (dev/admin role only)
app.get('/api/admin/users', authMiddleware, requireRole(['admin']), async (req, res) => {
// Admin functionality - would implement user management
res.json({ message: 'Admin endpoint - user management would be implemented here' });
});
app.get('/api/admin/system-status', authMiddleware, requireRole(['admin']), async (req, res) => {
try {
// System status check
const status = {
database: 'connected',
voiceModels: 'loaded',
aiModels: 'loaded',
uptime: process.uptime(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
res.json(status);
} catch (error) {
res.status(500).json({ error: 'Failed to get system status' });
}
});
// Health check endpoint
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
});
const httpServer = createServer(app);
return httpServer;
}
+336
View File
@@ -0,0 +1,336 @@
import { storage } from '../storage';
export interface AIResponse {
content: string;
processingTime: number;
modelUsed: string;
}
export interface JokeResponse extends AIResponse {
category: string;
}
class AIService {
private modelPath: string;
private isInitialized: boolean = false;
constructor() {
this.modelPath = process.env.AI_MODEL_PATH || './models/ai/base-llm';
}
async initialize(): Promise<boolean> {
try {
console.log('Initializing AI models...');
// In production, this would load the actual local LLM
await this.loadModel();
this.isInitialized = true;
console.log('AI models initialized successfully');
return true;
} catch (error) {
console.error('Failed to initialize AI models:', error);
return false;
}
}
private async loadModel(): Promise<void> {
// Simulate model loading time
await new Promise(resolve => setTimeout(resolve, 2000));
console.log(`AI model loaded from ${this.modelPath}`);
}
async generateDailyJoke(): Promise<JokeResponse> {
if (!this.isInitialized) {
await this.initialize();
}
const startTime = Date.now();
try {
// In production, this would use the actual AI model
const joke = await this.simulateJokeGeneration();
const processingTime = Date.now() - startTime;
const response: JokeResponse = {
content: joke,
processingTime,
modelUsed: 'local-llm-base',
category: 'programming'
};
// Store the interaction
await storage.createAIInteraction({
userId: null, // Daily joke is global
type: 'joke',
prompt: 'Generate daily programming joke',
response: joke,
modelUsed: 'local-llm-base',
processingTime,
wasSpoken: false
});
return response;
} catch (error) {
console.error('AI joke generation error:', error);
throw new Error('Failed to generate joke');
}
}
async generatePersonalizedJoke(userId: number, preferences?: any): Promise<JokeResponse> {
if (!this.isInitialized) {
await this.initialize();
}
const startTime = Date.now();
try {
// Get user context for personalization
const userTasks = await storage.getTasks(userId);
const recentCommands = await storage.getVoiceCommands(userId, 5);
const joke = await this.simulatePersonalizedJoke(userTasks.length, recentCommands.length);
const processingTime = Date.now() - startTime;
const response: JokeResponse = {
content: joke,
processingTime,
modelUsed: 'local-llm-base',
category: 'personalized'
};
// Store the interaction
await storage.createAIInteraction({
userId,
type: 'joke',
prompt: 'Generate personalized joke',
response: joke,
modelUsed: 'local-llm-base',
processingTime,
wasSpoken: false
});
return response;
} catch (error) {
console.error('Personalized joke generation error:', error);
throw new Error('Failed to generate personalized joke');
}
}
async chat(userId: number, message: string): Promise<AIResponse> {
if (!this.isInitialized) {
await this.initialize();
}
const startTime = Date.now();
try {
// Get user context
const user = await storage.getUser(userId);
const recentTasks = await storage.getTasks(userId);
const financialSummary = await storage.getFinancialSummary(userId);
const response = await this.simulateChat(message, {
user,
taskCount: recentTasks.length,
financialSummary
});
const processingTime = Date.now() - startTime;
const aiResponse: AIResponse = {
content: response,
processingTime,
modelUsed: 'local-llm-base'
};
// Store the interaction
await storage.createAIInteraction({
userId,
type: 'response',
prompt: message,
response,
modelUsed: 'local-llm-base',
processingTime,
wasSpoken: false
});
return aiResponse;
} catch (error) {
console.error('AI chat error:', error);
throw new Error('Failed to process chat message');
}
}
async generateInsight(userId: number, type: 'financial' | 'productivity' | 'general'): Promise<AIResponse> {
if (!this.isInitialized) {
await this.initialize();
}
const startTime = Date.now();
try {
let insight = '';
switch (type) {
case 'financial':
const summary = await storage.getFinancialSummary(userId);
insight = await this.simulateFinancialInsight(summary);
break;
case 'productivity':
const tasks = await storage.getTasks(userId);
insight = await this.simulateProductivityInsight(tasks);
break;
default:
insight = await this.simulateGeneralInsight();
}
const processingTime = Date.now() - startTime;
const response: AIResponse = {
content: insight,
processingTime,
modelUsed: 'local-llm-base'
};
// Store the interaction
await storage.createAIInteraction({
userId,
type: 'insight',
prompt: `Generate ${type} insight`,
response: insight,
modelUsed: 'local-llm-base',
processingTime,
wasSpoken: false
});
return response;
} catch (error) {
console.error('AI insight generation error:', error);
throw new Error('Failed to generate insight');
}
}
private async simulateJokeGeneration(): Promise<string> {
const jokes = [
"Why don't developers ever finish their tasks? Because they always get stuck in an infinite loop of 'just one more feature'!",
"Why did the programmer quit his job? He didn't get arrays! (a raise)",
"How many programmers does it take to screw in a light bulb? None, that's a hardware problem!",
"Why do Java developers wear glasses? Because they can't C#!",
"A SQL query walks into a bar, approaches two tables and asks: 'Can I join you?'",
"Why don't tasks ever get lonely? Because they always have deadlines to keep them company!",
"What's a programmer's favorite hangout place? The Foo Bar!",
"Why did the developer go broke? Because he used up all his cache!"
];
await new Promise(resolve => setTimeout(resolve, 800));
return jokes[Math.floor(Math.random() * jokes.length)];
}
private async simulatePersonalizedJoke(taskCount: number, voiceCommandCount: number): Promise<string> {
let joke = '';
if (taskCount > 10) {
joke = "Looks like you're collecting tasks like Pokémon cards! Gotta catch 'em all... and then actually do them!";
} else if (taskCount === 0) {
joke = "Your task list is emptier than a JavaScript developer's knowledge of semicolons!";
} else if (voiceCommandCount > 5) {
joke = "You're talking to me more than most people talk to their houseplants. At least I talk back!";
} else {
joke = await this.simulateJokeGeneration();
}
await new Promise(resolve => setTimeout(resolve, 1000));
return joke;
}
private async simulateChat(message: string, context: any): Promise<string> {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('task') || lowerMessage.includes('todo')) {
if (context.taskCount === 0) {
return "You have no pending tasks right now. Great job staying on top of things! Ready to add something new?";
} else {
return `You currently have ${context.taskCount} pending tasks. Would you like me to help you prioritize them or create a new one?`;
}
}
if (lowerMessage.includes('money') || lowerMessage.includes('financial') || lowerMessage.includes('budget')) {
const { income, expenses, net } = context.financialSummary;
return `Your current financial status shows $${income} in income and $${expenses} in expenses, for a net of $${net}. ${net > 0 ? "You're in the positive!" : "Consider reviewing your expenses."}`;
}
if (lowerMessage.includes('help')) {
return "I can help you with tasks, financial tracking, and answering questions about your data. Try saying 'create task', 'add expense', or ask about your financial summary!";
}
if (lowerMessage.includes('hello') || lowerMessage.includes('hi')) {
return `Hello ${context.user?.firstName || 'there'}! I'm your AI assistant. How can I help you manage your tasks and finances today?`;
}
await new Promise(resolve => setTimeout(resolve, 600));
return "I understand you're asking about that. While I'm still learning, I can help you with tasks, expenses, and general productivity questions. Is there something specific I can assist with?";
}
private async simulateFinancialInsight(summary: { income: number; expenses: number; net: number }): Promise<string> {
await new Promise(resolve => setTimeout(resolve, 1200));
const { income, expenses, net } = summary;
const expenseRatio = income > 0 ? (expenses / income) * 100 : 0;
if (net > 0) {
if (expenseRatio < 70) {
return `Excellent financial management! You're spending only ${expenseRatio.toFixed(1)}% of your income. Consider investing the surplus for long-term growth.`;
} else {
return `Good job maintaining a positive balance! Your expense ratio is ${expenseRatio.toFixed(1)}%. Consider optimizing some categories to improve savings.`;
}
} else {
return `Your expenses exceed income by $${Math.abs(net)}. I'd recommend reviewing your spending categories and identifying areas where you can reduce costs.`;
}
}
private async simulateProductivityInsight(tasks: any[]): Promise<string> {
await new Promise(resolve => setTimeout(resolve, 1000));
const completedTasks = tasks.filter(t => t.status === 'completed');
const pendingTasks = tasks.filter(t => t.status === 'pending');
const overdueTasks = tasks.filter(t => t.dueDate && new Date(t.dueDate) < new Date() && t.status !== 'completed');
if (tasks.length === 0) {
return "Your task list is clear! This is a great time to plan ahead or tackle some long-term goals.";
}
if (overdueTasks.length > 0) {
return `You have ${overdueTasks.length} overdue tasks. Consider prioritizing these first to get back on track.`;
}
if (completedTasks.length > pendingTasks.length) {
return `Great productivity! You've completed ${completedTasks.length} tasks. Keep up the momentum with your remaining ${pendingTasks.length} tasks.`;
}
return `You have ${pendingTasks.length} pending tasks. Consider breaking larger tasks into smaller, manageable chunks for better progress.`;
}
private async simulateGeneralInsight(): Promise<string> {
const insights = [
"Remember: Progress, not perfection. Small daily improvements lead to remarkable long-term results.",
"Voice commands can speed up your workflow by 40%. Try using them for quick task creation and expense logging.",
"The best time to plan tomorrow is today. Spend 5 minutes each evening reviewing and organizing.",
"Your data stays completely private with local processing. No cloud, no worries!",
"Consistency beats intensity. Better to track expenses daily than to do it all at month-end."
];
await new Promise(resolve => setTimeout(resolve, 800));
return insights[Math.floor(Math.random() * insights.length)];
}
async getModelStatus(): Promise<{ loaded: boolean; modelPath: string; initialized: boolean }> {
return {
loaded: this.isInitialized,
modelPath: this.modelPath,
initialized: this.isInitialized
};
}
}
export const aiService = new AIService();
+330
View File
@@ -0,0 +1,330 @@
import { spawn, ChildProcess } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { storage } from '../storage';
export interface VoiceProcessingResult {
transcription: string;
confidence: number;
processingTime: number;
intent?: string;
entities?: Record<string, any>;
}
export interface TTSResult {
audioBuffer: Buffer;
processingTime: number;
}
class VoiceService {
private sttModelPath: string;
private ttsModelPath: string;
private isInitialized: boolean = false;
private sttProcess: ChildProcess | null = null;
private ttsProcess: ChildProcess | null = null;
constructor() {
this.sttModelPath = process.env.STT_MODEL_PATH || './models/speech/base-stt';
this.ttsModelPath = process.env.TTS_MODEL_PATH || './models/speech/base-tts';
}
async initialize(): Promise<boolean> {
try {
console.log('Initializing voice models...');
// Check if model files exist
const sttExists = await this.checkModelExists(this.sttModelPath);
const ttsExists = await this.checkModelExists(this.ttsModelPath);
if (!sttExists || !ttsExists) {
console.error('Voice models not found. Please run model setup script.');
return false;
}
// Initialize STT model (using whisper.cpp or similar)
await this.initializeSTT();
// Initialize TTS model (using espeak-ng or similar)
await this.initializeTTS();
this.isInitialized = true;
console.log('Voice models initialized successfully');
return true;
} catch (error) {
console.error('Failed to initialize voice models:', error);
return false;
}
}
private async checkModelExists(modelPath: string): Promise<boolean> {
try {
await fs.access(modelPath);
return true;
} catch {
return false;
}
}
private async initializeSTT(): Promise<void> {
// Initialize STT process - in production, this would start the actual model
console.log(`Loading STT model from ${this.sttModelPath}`);
// For now, we'll simulate model loading
await new Promise(resolve => setTimeout(resolve, 1000));
}
private async initializeTTS(): Promise<void> {
// Initialize TTS process - in production, this would start the actual model
console.log(`Loading TTS model from ${this.ttsModelPath}`);
// For now, we'll simulate model loading
await new Promise(resolve => setTimeout(resolve, 1000));
}
async processAudioToText(audioBuffer: Buffer, userId?: number): Promise<VoiceProcessingResult> {
if (!this.isInitialized) {
throw new Error('Voice service not initialized');
}
const startTime = Date.now();
try {
// In production, this would use actual STT processing
// For now, we'll simulate transcription
const transcription = await this.simulateSTT(audioBuffer);
const confidence = 0.95; // Simulated confidence
const processingTime = Date.now() - startTime;
// Extract intent and entities from transcription
const { intent, entities } = this.extractIntent(transcription);
const result: VoiceProcessingResult = {
transcription,
confidence,
processingTime,
intent,
entities
};
// Log voice command if user is provided
if (userId) {
await storage.createVoiceCommand({
userId,
command: transcription,
transcription,
intent,
confidence,
processingTime,
successful: true
});
}
return result;
} catch (error) {
const processingTime = Date.now() - startTime;
if (userId) {
await storage.createVoiceCommand({
userId,
command: '',
transcription: '',
confidence: 0,
processingTime,
successful: false,
errorMessage: error instanceof Error ? error.message : 'Unknown error'
});
}
throw error;
}
}
async textToSpeech(text: string, voice?: string): Promise<TTSResult> {
if (!this.isInitialized) {
throw new Error('Voice service not initialized');
}
const startTime = Date.now();
try {
// In production, this would use actual TTS processing
const audioBuffer = await this.simulateTTS(text, voice);
const processingTime = Date.now() - startTime;
return {
audioBuffer,
processingTime
};
} catch (error) {
console.error('TTS error:', error);
throw error;
}
}
private async simulateSTT(audioBuffer: Buffer): Promise<string> {
// Simulate STT processing delay
await new Promise(resolve => setTimeout(resolve, 500));
// Return a simulated transcription based on common commands
const sampleCommands = [
"Create task review budget reports",
"Add expense coffee shop four dollars fifty cents",
"Show me today's tasks",
"What's my financial summary",
"Schedule meeting with team for Friday",
"Mark task as completed",
"Add income freelance payment five hundred dollars"
];
return sampleCommands[Math.floor(Math.random() * sampleCommands.length)];
}
private async simulateTTS(text: string, voice?: string): Promise<Buffer> {
// Simulate TTS processing delay
await new Promise(resolve => setTimeout(resolve, 300));
// Return a dummy audio buffer (in production, this would be actual audio)
const dummyAudioData = Buffer.alloc(1024, 0);
return dummyAudioData;
}
private extractIntent(transcription: string): { intent?: string; entities?: Record<string, any> } {
const text = transcription.toLowerCase();
// Simple intent recognition based on keywords
if (text.includes('create') && (text.includes('task') || text.includes('todo'))) {
return {
intent: 'task_create',
entities: this.extractTaskEntities(text)
};
}
if (text.includes('add') && (text.includes('expense') || text.includes('cost'))) {
return {
intent: 'expense_add',
entities: this.extractExpenseEntities(text)
};
}
if (text.includes('add') && text.includes('income')) {
return {
intent: 'income_add',
entities: this.extractIncomeEntities(text)
};
}
if (text.includes('show') || text.includes('list') || text.includes('get')) {
if (text.includes('task')) {
return { intent: 'task_list' };
}
if (text.includes('financial') || text.includes('money') || text.includes('summary')) {
return { intent: 'financial_summary' };
}
}
if (text.includes('complete') || text.includes('done') || text.includes('finish')) {
return { intent: 'task_complete' };
}
return { intent: 'unknown' };
}
private extractTaskEntities(text: string): Record<string, any> {
const entities: Record<string, any> = {};
// Extract task title (everything after "create task" or similar)
const taskMatch = text.match(/(?:create|add|new)\s+(?:task|todo)\s+(.+)/i);
if (taskMatch) {
entities.title = taskMatch[1].trim();
}
// Extract priority
if (text.includes('high priority') || text.includes('urgent')) {
entities.priority = 'high';
} else if (text.includes('low priority')) {
entities.priority = 'low';
} else {
entities.priority = 'medium';
}
// Extract due date (simplified)
if (text.includes('today')) {
entities.dueDate = new Date().toISOString();
} else if (text.includes('tomorrow')) {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
entities.dueDate = tomorrow.toISOString();
}
return entities;
}
private extractExpenseEntities(text: string): Record<string, any> {
const entities: Record<string, any> = {};
// Extract amount
const amountMatch = text.match(/(?:\$|dollar|dollars?)\s*(\d+(?:\.\d{2})?)|(\d+(?:\.\d{2})?)\s*(?:dollar|dollars?)/i);
if (amountMatch) {
entities.amount = parseFloat(amountMatch[1] || amountMatch[2]);
}
// Extract category/description
const categories = ['food', 'transport', 'shopping', 'utilities', 'entertainment', 'healthcare'];
for (const category of categories) {
if (text.includes(category)) {
entities.category = category;
break;
}
}
if (!entities.category) {
entities.category = 'other';
}
// Extract description
const expenseMatch = text.match(/(?:expense|spent|paid)\s+(?:for\s+)?(.+?)(?:\s+\$|\s+\d+|$)/i);
if (expenseMatch) {
entities.description = expenseMatch[1].trim();
}
return entities;
}
private extractIncomeEntities(text: string): Record<string, any> {
const entities: Record<string, any> = {};
// Extract amount
const amountMatch = text.match(/(?:\$|dollar|dollars?)\s*(\d+(?:\.\d{2})?)|(\d+(?:\.\d{2})?)\s*(?:dollar|dollars?)/i);
if (amountMatch) {
entities.amount = parseFloat(amountMatch[1] || amountMatch[2]);
}
// Extract source/description
const incomeMatch = text.match(/(?:income|received|earned)\s+(?:from\s+)?(.+?)(?:\s+\$|\s+\d+|$)/i);
if (incomeMatch) {
entities.description = incomeMatch[1].trim();
}
entities.category = 'income';
return entities;
}
async getModelStatus(): Promise<{ stt: boolean; tts: boolean; initialized: boolean }> {
return {
stt: await this.checkModelExists(this.sttModelPath),
tts: await this.checkModelExists(this.ttsModelPath),
initialized: this.isInitialized
};
}
shutdown(): void {
if (this.sttProcess) {
this.sttProcess.kill();
}
if (this.ttsProcess) {
this.ttsProcess.kill();
}
this.isInitialized = false;
}
}
export const voiceService = new VoiceService();
+286
View File
@@ -0,0 +1,286 @@
import { users, sessions, tasks, financialRecords, voiceCommands, aiInteractions, userPreferences, type User, type InsertUser, type Task, type InsertTask, type FinancialRecord, type InsertFinancialRecord, type Session, type VoiceCommand, type InsertVoiceCommand, type AIInteraction, type UserPreferences, type InsertUserPreferences } from "@shared/schema";
import { db } from "./db";
import { eq, desc, and, gte, lte, count } from "drizzle-orm";
export interface IStorage {
// User management
getUser(id: number): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: number, updates: Partial<User>): Promise<User | undefined>;
// Session management
createSession(userId: number, token: string, expiresAt: Date): Promise<Session>;
getSession(token: string): Promise<Session | undefined>;
deleteSession(token: string): Promise<void>;
// Task management
createTask(task: InsertTask): Promise<Task>;
getTasks(userId: number, status?: string): Promise<Task[]>;
getTask(id: number, userId: number): Promise<Task | undefined>;
updateTask(id: number, userId: number, updates: Partial<Task>): Promise<Task | undefined>;
deleteTask(id: number, userId: number): Promise<boolean>;
// Financial records
createFinancialRecord(record: InsertFinancialRecord): Promise<FinancialRecord>;
getFinancialRecords(userId: number, startDate?: Date, endDate?: Date): Promise<FinancialRecord[]>;
getFinancialSummary(userId: number, startDate?: Date, endDate?: Date): Promise<{ income: number; expenses: number; net: number }>;
updateFinancialRecord(id: number, userId: number, updates: Partial<FinancialRecord>): Promise<FinancialRecord | undefined>;
deleteFinancialRecord(id: number, userId: number): Promise<boolean>;
// Voice commands
createVoiceCommand(command: InsertVoiceCommand): Promise<VoiceCommand>;
getVoiceCommands(userId: number, limit?: number): Promise<VoiceCommand[]>;
// AI interactions
createAIInteraction(interaction: Omit<AIInteraction, 'id' | 'createdAt'>): Promise<AIInteraction>;
getAIInteractions(userId?: number, type?: string, limit?: number): Promise<AIInteraction[]>;
// User preferences
getUserPreferences(userId: number): Promise<UserPreferences | undefined>;
updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences>;
}
export class DatabaseStorage implements IStorage {
async getUser(id: number): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.id, id));
return user || undefined;
}
async getUserByEmail(email: string): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.email, email));
return user || undefined;
}
async getUserByUsername(username: string): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.username, username));
return user || undefined;
}
async createUser(insertUser: InsertUser): Promise<User> {
const [user] = await db
.insert(users)
.values(insertUser)
.returning();
return user;
}
async updateUser(id: number, updates: Partial<User>): Promise<User | undefined> {
const [user] = await db
.update(users)
.set({ ...updates, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return user || undefined;
}
async createSession(userId: number, token: string, expiresAt: Date): Promise<Session> {
const [session] = await db
.insert(sessions)
.values({ userId, token, expiresAt })
.returning();
return session;
}
async getSession(token: string): Promise<Session | undefined> {
const [session] = await db
.select()
.from(sessions)
.where(and(eq(sessions.token, token), gte(sessions.expiresAt, new Date())));
return session || undefined;
}
async deleteSession(token: string): Promise<void> {
await db.delete(sessions).where(eq(sessions.token, token));
}
async createTask(task: InsertTask): Promise<Task> {
const [newTask] = await db
.insert(tasks)
.values(task)
.returning();
return newTask;
}
async getTasks(userId: number, status?: string): Promise<Task[]> {
const query = db.select().from(tasks).where(eq(tasks.userId, userId));
if (status) {
query.where(and(eq(tasks.userId, userId), eq(tasks.status, status)));
}
return await query.orderBy(desc(tasks.createdAt));
}
async getTask(id: number, userId: number): Promise<Task | undefined> {
const [task] = await db
.select()
.from(tasks)
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)));
return task || undefined;
}
async updateTask(id: number, userId: number, updates: Partial<Task>): Promise<Task | undefined> {
const [task] = await db
.update(tasks)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)))
.returning();
return task || undefined;
}
async deleteTask(id: number, userId: number): Promise<boolean> {
const result = await db
.delete(tasks)
.where(and(eq(tasks.id, id), eq(tasks.userId, userId)));
return result.rowCount > 0;
}
async createFinancialRecord(record: InsertFinancialRecord): Promise<FinancialRecord> {
const [newRecord] = await db
.insert(financialRecords)
.values(record)
.returning();
return newRecord;
}
async getFinancialRecords(userId: number, startDate?: Date, endDate?: Date): Promise<FinancialRecord[]> {
let query = db.select().from(financialRecords).where(eq(financialRecords.userId, userId));
if (startDate && endDate) {
query = query.where(
and(
eq(financialRecords.userId, userId),
gte(financialRecords.date, startDate),
lte(financialRecords.date, endDate)
)
);
}
return await query.orderBy(desc(financialRecords.date));
}
async getFinancialSummary(userId: number, startDate?: Date, endDate?: Date): Promise<{ income: number; expenses: number; net: number }> {
let query = db.select({
type: financialRecords.type,
amount: financialRecords.amount
}).from(financialRecords).where(eq(financialRecords.userId, userId));
if (startDate && endDate) {
query = query.where(
and(
eq(financialRecords.userId, userId),
gte(financialRecords.date, startDate),
lte(financialRecords.date, endDate)
)
);
}
const records = await query;
let income = 0;
let expenses = 0;
records.forEach(record => {
const amount = parseFloat(record.amount);
if (record.type === 'income') {
income += amount;
} else {
expenses += amount;
}
});
return {
income,
expenses,
net: income - expenses
};
}
async updateFinancialRecord(id: number, userId: number, updates: Partial<FinancialRecord>): Promise<FinancialRecord | undefined> {
const [record] = await db
.update(financialRecords)
.set({ ...updates, updatedAt: new Date() })
.where(and(eq(financialRecords.id, id), eq(financialRecords.userId, userId)))
.returning();
return record || undefined;
}
async deleteFinancialRecord(id: number, userId: number): Promise<boolean> {
const result = await db
.delete(financialRecords)
.where(and(eq(financialRecords.id, id), eq(financialRecords.userId, userId)));
return result.rowCount > 0;
}
async createVoiceCommand(command: InsertVoiceCommand): Promise<VoiceCommand> {
const [newCommand] = await db
.insert(voiceCommands)
.values(command)
.returning();
return newCommand;
}
async getVoiceCommands(userId: number, limit: number = 50): Promise<VoiceCommand[]> {
return await db
.select()
.from(voiceCommands)
.where(eq(voiceCommands.userId, userId))
.orderBy(desc(voiceCommands.createdAt))
.limit(limit);
}
async createAIInteraction(interaction: Omit<AIInteraction, 'id' | 'createdAt'>): Promise<AIInteraction> {
const [newInteraction] = await db
.insert(aiInteractions)
.values(interaction)
.returning();
return newInteraction;
}
async getAIInteractions(userId?: number, type?: string, limit: number = 50): Promise<AIInteraction[]> {
let query = db.select().from(aiInteractions);
const conditions = [];
if (userId) conditions.push(eq(aiInteractions.userId, userId));
if (type) conditions.push(eq(aiInteractions.type, type));
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
return await query
.orderBy(desc(aiInteractions.createdAt))
.limit(limit);
}
async getUserPreferences(userId: number): Promise<UserPreferences | undefined> {
const [preferences] = await db
.select()
.from(userPreferences)
.where(eq(userPreferences.userId, userId));
return preferences || undefined;
}
async updateUserPreferences(userId: number, preferences: Partial<InsertUserPreferences>): Promise<UserPreferences> {
const existing = await this.getUserPreferences(userId);
if (existing) {
const [updated] = await db
.update(userPreferences)
.set({ ...preferences, updatedAt: new Date() })
.where(eq(userPreferences.userId, userId))
.returning();
return updated;
} else {
const [created] = await db
.insert(userPreferences)
.values({ userId, ...preferences })
.returning();
return created;
}
}
}
export const storage = new DatabaseStorage();
+85
View File
@@ -0,0 +1,85 @@
import express, { type Express } from "express";
import fs from "fs";
import path from "path";
import { createServer as createViteServer, createLogger } from "vite";
import { type Server } from "http";
import viteConfig from "../vite.config";
import { nanoid } from "nanoid";
const viteLogger = createLogger();
export function log(message: string, source = "express") {
const formattedTime = new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
console.log(`${formattedTime} [${source}] ${message}`);
}
export async function setupVite(app: Express, server: Server) {
const serverOptions = {
middlewareMode: true,
hmr: { server },
allowedHosts: true,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
viteLogger.error(msg, options);
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
app.use(vite.middlewares);
app.use("*", async (req, res, next) => {
const url = req.originalUrl;
try {
const clientTemplate = path.resolve(
import.meta.dirname,
"..",
"client",
"index.html",
);
// always reload the index.html file from disk incase it changes
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}
export function serveStatic(app: Express) {
const distPath = path.resolve(import.meta.dirname, "public");
if (!fs.existsSync(distPath)) {
throw new Error(
`Could not find the build directory: ${distPath}, make sure to build the client first`,
);
}
app.use(express.static(distPath));
// fall through to index.html if the file doesn't exist
app.use("*", (_req, res) => {
res.sendFile(path.resolve(distPath, "index.html"));
});
}