nexusbert commited on
Commit
ef85da9
Β·
1 Parent(s): 78299b3
src/app.ts CHANGED
@@ -18,29 +18,23 @@ import adminRoutes from './routes/adminRoutes';
18
 
19
  const app: Application = express();
20
 
21
- // Security middleware
22
  app.use(helmet());
23
- // CORS configuration - allow Hugging Face Spaces and custom origins
24
  app.use(cors({
25
  origin: process.env.CORS_ORIGIN || process.env.FRONTEND_URL || '*',
26
  credentials: true,
27
  }));
28
 
29
- // Body parsing
30
  app.use(express.json({ limit: '10mb' }));
31
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));
32
- // Note: File uploads are handled by multer in route-specific middleware
33
 
34
- // Rate limiting
35
  const limiter = rateLimit({
36
- windowMs: 15 * 60 * 1000, // 15 minutes
37
- max: 100, // Limit each IP to 100 requests per windowMs
38
  message: 'Too many requests from this IP, please try again later.',
39
  });
40
 
41
  app.use('/api/', limiter);
42
 
43
- // Root endpoint
44
  app.get('/', (req, res) => {
45
  res.json({
46
  name: 'Zurri API',
@@ -57,20 +51,20 @@ app.get('/', (req, res) => {
57
  });
58
  });
59
 
60
- // Health check
61
  app.get('/health', (req, res) => {
62
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
63
  });
64
 
65
- // Docs - Swagger UI
66
  app.use('/docs', swaggerUi.serve);
67
  app.get('/docs', swaggerUi.setup(swaggerSpec, {
68
  explorer: true,
69
  customCss: '.swagger-ui .topbar { display: none }',
70
  customSiteTitle: 'Zurri API Documentation',
 
 
 
71
  }));
72
 
73
- // API routes
74
  app.use('/api/auth', authRoutes);
75
  app.use('/api/creator-auth', creatorAuthRoutes);
76
  app.use('/api/users', userRoutes);
@@ -81,12 +75,10 @@ app.use('/api/chat', chatRoutes);
81
  app.use('/api/subscriptions', subscriptionRoutes);
82
  app.use('/api/wallet', walletRoutes);
83
 
84
- // 404 handler
85
  app.use((req, res) => {
86
  res.status(404).json({ error: 'Route not found' });
87
  });
88
 
89
- // Error handler
90
  app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
91
  console.error('Error:', err);
92
  res.status(500).json({
 
18
 
19
  const app: Application = express();
20
 
 
21
  app.use(helmet());
 
22
  app.use(cors({
23
  origin: process.env.CORS_ORIGIN || process.env.FRONTEND_URL || '*',
24
  credentials: true,
25
  }));
26
 
 
27
  app.use(express.json({ limit: '10mb' }));
28
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));
 
29
 
 
30
  const limiter = rateLimit({
31
+ windowMs: 15 * 60 * 1000,
32
+ max: 100,
33
  message: 'Too many requests from this IP, please try again later.',
34
  });
35
 
36
  app.use('/api/', limiter);
37
 
 
38
  app.get('/', (req, res) => {
39
  res.json({
40
  name: 'Zurri API',
 
51
  });
52
  });
53
 
 
54
  app.get('/health', (req, res) => {
55
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
56
  });
57
 
 
58
  app.use('/docs', swaggerUi.serve);
59
  app.get('/docs', swaggerUi.setup(swaggerSpec, {
60
  explorer: true,
61
  customCss: '.swagger-ui .topbar { display: none }',
62
  customSiteTitle: 'Zurri API Documentation',
63
+ swaggerOptions: {
64
+ persistAuthorization: true,
65
+ },
66
  }));
67
 
 
68
  app.use('/api/auth', authRoutes);
69
  app.use('/api/creator-auth', creatorAuthRoutes);
70
  app.use('/api/users', userRoutes);
 
75
  app.use('/api/subscriptions', subscriptionRoutes);
76
  app.use('/api/wallet', walletRoutes);
77
 
 
78
  app.use((req, res) => {
79
  res.status(404).json({ error: 'Route not found' });
80
  });
81
 
 
82
  app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
83
  console.error('Error:', err);
84
  res.status(500).json({
src/config/database.ts CHANGED
@@ -12,7 +12,7 @@ export const AppDataSource = new DataSource({
12
  type: 'postgres',
13
  url: process.env.DATABASE_URL,
14
  entities: [Agent, User, Subscription, ApiKey, ChatMessage, Wallet, Transaction, CreatorProfile],
15
- synchronize: process.env.DB_SYNCHRONIZE === 'true' || process.env.NODE_ENV !== 'production', // Auto-sync if enabled or in dev
16
  logging: process.env.NODE_ENV === 'development',
17
  extra: {
18
  ssl: process.env.DATABASE_URL?.includes('render.com') || process.env.DATABASE_URL?.includes('dpg-')
 
12
  type: 'postgres',
13
  url: process.env.DATABASE_URL,
14
  entities: [Agent, User, Subscription, ApiKey, ChatMessage, Wallet, Transaction, CreatorProfile],
15
+ synchronize: process.env.DB_SYNCHRONIZE === 'true' || process.env.NODE_ENV !== 'production',
16
  logging: process.env.NODE_ENV === 'development',
17
  extra: {
18
  ssl: process.env.DATABASE_URL?.includes('render.com') || process.env.DATABASE_URL?.includes('dpg-')
src/controllers/walletController.ts CHANGED
@@ -8,14 +8,17 @@ import { TransactionStatus } from '../entities/Transaction';
8
  import crypto from 'crypto';
9
 
10
  const walletService = new WalletService();
11
- const paystackService = new PaystackService();
 
 
 
 
 
 
12
  const exchangeRateService = new ExchangeRateService();
13
  const userRepository = AppDataSource.getRepository(User);
14
 
15
  export class WalletController {
16
- /**
17
- * Get user's wallet balance
18
- */
19
  async getWallet(req: Request, res: Response) {
20
  try {
21
  const userId = (req as any).user.id;
@@ -36,14 +39,10 @@ export class WalletController {
36
  }
37
  }
38
 
39
- /**
40
- * Initiate wallet funding (purchase points)
41
- * Returns Paystack initialization data for frontend widget
42
- */
43
  async fundWallet(req: Request, res: Response) {
44
  try {
45
  const userId = (req as any).user.id;
46
- const { amount } = req.body; // Amount in Naira (NGN)
47
 
48
  if (!amount || amount <= 0) {
49
  return res.status(400).json({ error: 'Invalid amount. Must be greater than 0' });
@@ -55,19 +54,16 @@ export class WalletController {
55
  return res.status(404).json({ error: 'User not found' });
56
  }
57
 
58
- // Convert Naira to points using current exchange rate
59
  const pointValueUsd = parseFloat(process.env.POINT_VALUE_USD || '0.05');
60
- const fx = await exchangeRateService.getNGNToUSDRate(); // Dynamic rate
61
  const amountInDollars = amount / fx;
62
  const points = Math.round(amountInDollars / pointValueUsd);
63
 
64
- // Generate unique payment reference
65
  const paymentReference = `wallet_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
66
 
67
- // Initialize Paystack transaction
68
- const paymentData = await paystackService.initializeTransaction({
69
  email: user.email,
70
- amount: Math.round(amount * 100), // Convert to kobo (smallest NGN unit)
71
  reference: paymentReference,
72
  metadata: {
73
  userId: userId,
@@ -80,7 +76,7 @@ export class WalletController {
80
 
81
  // Return payment info for frontend
82
  res.json({
83
- publicKey: paystackService.getPublicKey(),
84
  reference: paymentData.reference,
85
  authorization_url: paymentData.authorization_url,
86
  access_code: paymentData.access_code,
@@ -112,7 +108,7 @@ export class WalletController {
112
  }
113
 
114
  // Verify with Paystack
115
- const verification = await paystackService.verifyTransaction(reference);
116
 
117
  // Check if transaction was successful
118
  if (verification.status !== 'success') {
@@ -226,7 +222,7 @@ export class WalletController {
226
 
227
  // Verify with Paystack API (server-to-server verification)
228
  try {
229
- const verification = await paystackService.verifyTransaction(reference);
230
  if (verification.status !== 'success') {
231
  console.error('Paystack verification failed:', verification);
232
  return res.status(400).json({ error: 'Transaction verification failed' });
@@ -291,7 +287,7 @@ export class WalletController {
291
 
292
  // Verify with Paystack
293
  try {
294
- const verification = await paystackService.verifyTransaction(reference);
295
 
296
  // Check if transaction was successful
297
  if (verification.status !== 'success') {
 
8
  import crypto from 'crypto';
9
 
10
  const walletService = new WalletService();
11
+ let paystackService: PaystackService | null = null;
12
+ const getPaystackService = () => {
13
+ if (!paystackService) {
14
+ paystackService = new PaystackService();
15
+ }
16
+ return paystackService;
17
+ };
18
  const exchangeRateService = new ExchangeRateService();
19
  const userRepository = AppDataSource.getRepository(User);
20
 
21
  export class WalletController {
 
 
 
22
  async getWallet(req: Request, res: Response) {
23
  try {
24
  const userId = (req as any).user.id;
 
39
  }
40
  }
41
 
 
 
 
 
42
  async fundWallet(req: Request, res: Response) {
43
  try {
44
  const userId = (req as any).user.id;
45
+ const { amount } = req.body;
46
 
47
  if (!amount || amount <= 0) {
48
  return res.status(400).json({ error: 'Invalid amount. Must be greater than 0' });
 
54
  return res.status(404).json({ error: 'User not found' });
55
  }
56
 
 
57
  const pointValueUsd = parseFloat(process.env.POINT_VALUE_USD || '0.05');
58
+ const fx = await exchangeRateService.getNGNToUSDRate();
59
  const amountInDollars = amount / fx;
60
  const points = Math.round(amountInDollars / pointValueUsd);
61
 
 
62
  const paymentReference = `wallet_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
63
 
64
+ const paymentData = await getPaystackService().initializeTransaction({
 
65
  email: user.email,
66
+ amount: Math.round(amount * 100),
67
  reference: paymentReference,
68
  metadata: {
69
  userId: userId,
 
76
 
77
  // Return payment info for frontend
78
  res.json({
79
+ publicKey: getPaystackService().getPublicKey(),
80
  reference: paymentData.reference,
81
  authorization_url: paymentData.authorization_url,
82
  access_code: paymentData.access_code,
 
108
  }
109
 
110
  // Verify with Paystack
111
+ const verification = await getPaystackService().verifyTransaction(reference);
112
 
113
  // Check if transaction was successful
114
  if (verification.status !== 'success') {
 
222
 
223
  // Verify with Paystack API (server-to-server verification)
224
  try {
225
+ const verification = await getPaystackService().verifyTransaction(reference);
226
  if (verification.status !== 'success') {
227
  console.error('Paystack verification failed:', verification);
228
  return res.status(400).json({ error: 'Transaction verification failed' });
 
287
 
288
  // Verify with Paystack
289
  try {
290
+ const verification = await getPaystackService().verifyTransaction(reference);
291
 
292
  // Check if transaction was successful
293
  if (verification.status !== 'success') {
src/docs/swagger.ts CHANGED
@@ -3,18 +3,37 @@ import path from 'path';
3
  import fs from 'fs';
4
 
5
  // Swagger JSDoc needs the source TypeScript files (not compiled JS) to read JSDoc comments
6
- // In production, we copy the source route files to the container
7
- const routesPath = path.join(__dirname, '../routes/*.ts');
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  // Check if route files exist (for debugging)
10
  try {
11
- const routesDir = path.join(__dirname, '../routes');
12
  if (fs.existsSync(routesDir)) {
13
  const files = fs.readdirSync(routesDir);
14
  console.log(`πŸ“ Found ${files.length} route files in ${routesDir}`);
 
 
15
  } else {
16
  console.warn(`⚠️ Routes directory not found: ${routesDir}`);
17
  }
 
 
 
 
18
  } catch (error) {
19
  console.warn('⚠️ Could not check routes directory:', error);
20
  }
@@ -41,6 +60,86 @@ export const swaggerSpec = swaggerJSDoc({
41
  bearerFormat: 'JWT',
42
  },
43
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  },
45
  security: [{ bearerAuth: [] }],
46
  },
 
3
  import fs from 'fs';
4
 
5
  // Swagger JSDoc needs the source TypeScript files (not compiled JS) to read JSDoc comments
6
+ // When running from dist/, we need to look for src/ at the project root
7
+ // When running from src/, we use relative path
8
+
9
+ // Check if we're running from dist (compiled) or src (development)
10
+ const isRunningFromDist = __dirname.includes('dist');
11
+ const projectRoot = isRunningFromDist
12
+ ? path.resolve(__dirname, '../..') // Go up from dist/docs to project root
13
+ : path.resolve(__dirname, '..'); // Go up from src/docs to project root
14
+
15
+ // Always use source TypeScript files from src/routes
16
+ const routesPath = path.join(projectRoot, 'src/routes/*.ts');
17
+
18
+ console.log(`πŸ” Swagger looking for routes at: ${routesPath}`);
19
+ console.log(`πŸ“‚ Running from: ${isRunningFromDist ? 'dist' : 'src'}`);
20
+ console.log(`πŸ“‚ Project root: ${projectRoot}`);
21
 
22
  // Check if route files exist (for debugging)
23
  try {
24
+ const routesDir = path.join(projectRoot, 'src/routes');
25
  if (fs.existsSync(routesDir)) {
26
  const files = fs.readdirSync(routesDir);
27
  console.log(`πŸ“ Found ${files.length} route files in ${routesDir}`);
28
+ const tsFiles = files.filter(f => f.endsWith('.ts'));
29
+ console.log(`πŸ“„ TypeScript route files: ${tsFiles.length}`);
30
  } else {
31
  console.warn(`⚠️ Routes directory not found: ${routesDir}`);
32
  }
33
+
34
+ // Also check if the glob pattern resolves correctly
35
+ const testPattern = path.join(projectRoot, 'src/routes/*.ts');
36
+ console.log(`πŸ” Using pattern: ${testPattern}`);
37
  } catch (error) {
38
  console.warn('⚠️ Could not check routes directory:', error);
39
  }
 
60
  bearerFormat: 'JWT',
61
  },
62
  },
63
+ schemas: {
64
+ Agent: {
65
+ type: 'object',
66
+ properties: {
67
+ id: {
68
+ type: 'string',
69
+ format: 'uuid',
70
+ },
71
+ name: {
72
+ type: 'string',
73
+ example: 'PixelPainter',
74
+ },
75
+ description: {
76
+ type: 'string',
77
+ example: 'I turn prompts into AI art!',
78
+ },
79
+ endpoint: {
80
+ type: 'string',
81
+ format: 'uri',
82
+ example: 'https://api.pixelpainter.ai/run',
83
+ },
84
+ avatar: {
85
+ type: 'string',
86
+ format: 'uri',
87
+ nullable: true,
88
+ description: 'IPFS gateway URL for avatar image',
89
+ },
90
+ category: {
91
+ type: 'string',
92
+ example: 'image generation',
93
+ },
94
+ reputation: {
95
+ type: 'number',
96
+ format: 'float',
97
+ example: 4.8,
98
+ },
99
+ capabilities: {
100
+ type: 'array',
101
+ items: {
102
+ type: 'string',
103
+ },
104
+ example: ['text-to-image', 'image-editing'],
105
+ },
106
+ pointsPerTask: {
107
+ type: 'number',
108
+ format: 'float',
109
+ example: 10,
110
+ },
111
+ status: {
112
+ type: 'string',
113
+ enum: ['pending', 'approved', 'rejected', 'suspended'],
114
+ },
115
+ usageCount: {
116
+ type: 'integer',
117
+ example: 0,
118
+ },
119
+ rating: {
120
+ type: 'number',
121
+ format: 'float',
122
+ nullable: true,
123
+ },
124
+ ratingCount: {
125
+ type: 'integer',
126
+ example: 0,
127
+ },
128
+ creatorId: {
129
+ type: 'string',
130
+ format: 'uuid',
131
+ },
132
+ createdAt: {
133
+ type: 'string',
134
+ format: 'date-time',
135
+ },
136
+ updatedAt: {
137
+ type: 'string',
138
+ format: 'date-time',
139
+ },
140
+ },
141
+ },
142
+ },
143
  },
144
  security: [{ bearerAuth: [] }],
145
  },
src/middlewares/auth.ts CHANGED
@@ -12,9 +12,6 @@ export interface AuthRequest extends Request {
12
  };
13
  }
14
 
15
- /**
16
- * Verify JWT token and attach user to request
17
- */
18
  export const authenticate = (
19
  req: AuthRequest,
20
  res: Response,
@@ -47,9 +44,6 @@ export const authenticate = (
47
  }
48
  };
49
 
50
- /**
51
- * Optional authentication - doesn't fail if no token
52
- */
53
  export const optionalAuth = (
54
  req: AuthRequest,
55
  res: Response,
@@ -73,14 +67,10 @@ export const optionalAuth = (
73
  }
74
  next();
75
  } catch (error) {
76
- // Continue without auth
77
  next();
78
  }
79
  };
80
 
81
- /**
82
- * Require admin role
83
- */
84
  export const requireAdmin = (
85
  req: AuthRequest,
86
  res: Response,
 
12
  };
13
  }
14
 
 
 
 
15
  export const authenticate = (
16
  req: AuthRequest,
17
  res: Response,
 
44
  }
45
  };
46
 
 
 
 
47
  export const optionalAuth = (
48
  req: AuthRequest,
49
  res: Response,
 
67
  }
68
  next();
69
  } catch (error) {
 
70
  next();
71
  }
72
  };
73
 
 
 
 
74
  export const requireAdmin = (
75
  req: AuthRequest,
76
  res: Response,
src/routes/agentRoutes.ts CHANGED
@@ -8,6 +8,13 @@ import { Agent, AgentStatus } from '../entities/Agent';
8
  const router = Router();
9
  const agentController = new AgentController();
10
 
 
 
 
 
 
 
 
11
  // Configure multer for avatar image uploads
12
  const avatarUpload = multer({
13
  storage: multer.memoryStorage(),
@@ -34,10 +41,162 @@ const avatarUpload = multer({
34
  },
35
  });
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  // Public routes
38
  router.get('/', optionalAuth, agentController.listAgents.bind(agentController));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  router.get('/:id', optionalAuth, agentController.getAgent.bind(agentController));
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  // Creator routes (authenticated)
42
  router.post(
43
  '/',
@@ -45,15 +204,300 @@ router.post(
45
  avatarUpload.single('avatar'), // Accept avatar as single file
46
  agentController.createAgent.bind(agentController)
47
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  router.put(
49
  '/:id',
50
  authenticate,
51
  avatarUpload.single('avatar'), // Accept avatar as single file
52
  agentController.updateAgent.bind(agentController)
53
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  router.delete('/:id', authenticate, agentController.deleteAgent.bind(agentController));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  router.get('/my/list', authenticate, agentController.getMyAgents.bind(agentController));
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  // Admin routes
58
  router.patch(
59
  '/:id/approve',
@@ -83,6 +527,36 @@ router.patch(
83
  }
84
  );
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  router.patch(
87
  '/:id/reject',
88
  authenticate,
@@ -110,6 +584,28 @@ router.patch(
110
  }
111
  );
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  router.get(
114
  '/admin/pending',
115
  authenticate,
 
8
  const router = Router();
9
  const agentController = new AgentController();
10
 
11
+ /**
12
+ * @swagger
13
+ * tags:
14
+ * name: Agents
15
+ * description: Agent marketplace and management endpoints
16
+ */
17
+
18
  // Configure multer for avatar image uploads
19
  const avatarUpload = multer({
20
  storage: multer.memoryStorage(),
 
41
  },
42
  });
43
 
44
+ /**
45
+ * @swagger
46
+ * /agents:
47
+ * get:
48
+ * summary: List agents (marketplace)
49
+ * tags: [Agents]
50
+ * description: Get a paginated list of approved agents with optional filtering
51
+ * security: []
52
+ * parameters:
53
+ * - in: query
54
+ * name: page
55
+ * schema:
56
+ * type: integer
57
+ * default: 1
58
+ * description: Page number
59
+ * - in: query
60
+ * name: limit
61
+ * schema:
62
+ * type: integer
63
+ * default: 20
64
+ * description: Items per page
65
+ * - in: query
66
+ * name: category
67
+ * schema:
68
+ * type: string
69
+ * description: Filter by category
70
+ * - in: query
71
+ * name: search
72
+ * schema:
73
+ * type: string
74
+ * description: Search by name or description
75
+ * - in: query
76
+ * name: minReputation
77
+ * schema:
78
+ * type: number
79
+ * description: Minimum reputation score
80
+ * responses:
81
+ * 200:
82
+ * description: List of agents
83
+ * content:
84
+ * application/json:
85
+ * schema:
86
+ * type: object
87
+ * properties:
88
+ * agents:
89
+ * type: array
90
+ * items:
91
+ * $ref: '#/components/schemas/Agent'
92
+ * pagination:
93
+ * type: object
94
+ * properties:
95
+ * page:
96
+ * type: integer
97
+ * limit:
98
+ * type: integer
99
+ * total:
100
+ * type: integer
101
+ * totalPages:
102
+ * type: integer
103
+ */
104
  // Public routes
105
  router.get('/', optionalAuth, agentController.listAgents.bind(agentController));
106
+
107
+ /**
108
+ * @swagger
109
+ * /agents/{id}:
110
+ * get:
111
+ * summary: Get agent details
112
+ * tags: [Agents]
113
+ * security: []
114
+ * parameters:
115
+ * - in: path
116
+ * name: id
117
+ * required: true
118
+ * schema:
119
+ * type: string
120
+ * format: uuid
121
+ * description: Agent ID
122
+ * responses:
123
+ * 200:
124
+ * description: Agent details
125
+ * content:
126
+ * application/json:
127
+ * schema:
128
+ * $ref: '#/components/schemas/Agent'
129
+ * 404:
130
+ * description: Agent not found
131
+ */
132
  router.get('/:id', optionalAuth, agentController.getAgent.bind(agentController));
133
 
134
+ /**
135
+ * @swagger
136
+ * /agents:
137
+ * post:
138
+ * summary: Create a new agent
139
+ * tags: [Agents]
140
+ * security:
141
+ * - bearerAuth: []
142
+ * requestBody:
143
+ * required: true
144
+ * content:
145
+ * multipart/form-data:
146
+ * schema:
147
+ * type: object
148
+ * required:
149
+ * - name
150
+ * - description
151
+ * - endpoint
152
+ * - category
153
+ * properties:
154
+ * name:
155
+ * type: string
156
+ * example: PixelPainter
157
+ * description:
158
+ * type: string
159
+ * example: I turn prompts into AI art!
160
+ * endpoint:
161
+ * type: string
162
+ * format: uri
163
+ * example: https://api.pixelpainter.ai/run
164
+ * category:
165
+ * type: string
166
+ * example: image generation
167
+ * avatar:
168
+ * type: string
169
+ * format: binary
170
+ * description: Agent avatar image (jpg, png, gif, webp, svg, bmp)
171
+ * reputation:
172
+ * type: number
173
+ * format: float
174
+ * default: 0
175
+ * example: 4.8
176
+ * capabilities:
177
+ * type: array
178
+ * items:
179
+ * type: string
180
+ * example: ["text-to-image", "image-editing"]
181
+ * pointsPerTask:
182
+ * type: number
183
+ * format: float
184
+ * default: 0
185
+ * example: 10
186
+ * responses:
187
+ * 201:
188
+ * description: Agent created successfully
189
+ * content:
190
+ * application/json:
191
+ * schema:
192
+ * $ref: '#/components/schemas/Agent'
193
+ * 400:
194
+ * description: Invalid input
195
+ * 401:
196
+ * description: Unauthorized
197
+ * 500:
198
+ * description: Server error
199
+ */
200
  // Creator routes (authenticated)
201
  router.post(
202
  '/',
 
204
  avatarUpload.single('avatar'), // Accept avatar as single file
205
  agentController.createAgent.bind(agentController)
206
  );
207
+
208
+ /**
209
+ * @swagger
210
+ * /agents/{id}:
211
+ * put:
212
+ * summary: Update an agent
213
+ * tags: [Agents]
214
+ * security:
215
+ * - bearerAuth: []
216
+ * parameters:
217
+ * - in: path
218
+ * name: id
219
+ * required: true
220
+ * schema:
221
+ * type: string
222
+ * format: uuid
223
+ * description: Agent ID
224
+ * requestBody:
225
+ * required: true
226
+ * content:
227
+ * multipart/form-data:
228
+ * schema:
229
+ * type: object
230
+ * properties:
231
+ * name:
232
+ * type: string
233
+ * description:
234
+ * type: string
235
+ * endpoint:
236
+ * type: string
237
+ * format: uri
238
+ * category:
239
+ * type: string
240
+ * avatar:
241
+ * type: string
242
+ * format: binary
243
+ * description: Agent avatar image
244
+ * reputation:
245
+ * type: number
246
+ * format: float
247
+ * capabilities:
248
+ * type: array
249
+ * items:
250
+ * type: string
251
+ * pointsPerTask:
252
+ * type: number
253
+ * format: float
254
+ * responses:
255
+ * 200:
256
+ * description: Agent updated successfully
257
+ * content:
258
+ * application/json:
259
+ * schema:
260
+ * $ref: '#/components/schemas/Agent'
261
+ * 400:
262
+ * description: Invalid input
263
+ * 401:
264
+ * description: Unauthorized
265
+ * 403:
266
+ * description: Not authorized to update this agent
267
+ * 404:
268
+ * description: Agent not found
269
+ */
270
  router.put(
271
  '/:id',
272
  authenticate,
273
  avatarUpload.single('avatar'), // Accept avatar as single file
274
  agentController.updateAgent.bind(agentController)
275
  );
276
+
277
+ /**
278
+ * @swagger
279
+ * /agents/{id}:
280
+ * delete:
281
+ * summary: Delete an agent
282
+ * tags: [Agents]
283
+ * security:
284
+ * - bearerAuth: []
285
+ * parameters:
286
+ * - in: path
287
+ * name: id
288
+ * required: true
289
+ * schema:
290
+ * type: string
291
+ * format: uuid
292
+ * description: Agent ID
293
+ * responses:
294
+ * 200:
295
+ * description: Agent deleted successfully
296
+ * content:
297
+ * application/json:
298
+ * schema:
299
+ * type: object
300
+ * properties:
301
+ * message:
302
+ * type: string
303
+ * example: Agent deleted successfully
304
+ * 401:
305
+ * description: Unauthorized
306
+ * 403:
307
+ * description: Not authorized to delete this agent
308
+ * 404:
309
+ * description: Agent not found
310
+ */
311
  router.delete('/:id', authenticate, agentController.deleteAgent.bind(agentController));
312
+
313
+ /**
314
+ * @swagger
315
+ * /agents/my/list:
316
+ * get:
317
+ * summary: Get my agents (creator's agents)
318
+ * tags: [Agents]
319
+ * security:
320
+ * - bearerAuth: []
321
+ * parameters:
322
+ * - in: query
323
+ * name: status
324
+ * schema:
325
+ * type: string
326
+ * enum: [pending, approved, rejected]
327
+ * description: Filter by status
328
+ * responses:
329
+ * 200:
330
+ * description: List of user's agents
331
+ * content:
332
+ * application/json:
333
+ * schema:
334
+ * type: array
335
+ * items:
336
+ * $ref: '#/components/schemas/Agent'
337
+ * 401:
338
+ * description: Unauthorized
339
+ */
340
  router.get('/my/list', authenticate, agentController.getMyAgents.bind(agentController));
341
 
342
+ /**
343
+ * @swagger
344
+ * /agents/{id}/delist:
345
+ * patch:
346
+ * summary: Delist an agent (hide from marketplace)
347
+ * tags: [Agents]
348
+ * description: Creator can suspend their own approved agent to hide it from the marketplace
349
+ * security:
350
+ * - bearerAuth: []
351
+ * parameters:
352
+ * - in: path
353
+ * name: id
354
+ * required: true
355
+ * schema:
356
+ * type: string
357
+ * format: uuid
358
+ * description: Agent ID
359
+ * responses:
360
+ * 200:
361
+ * description: Agent delisted (suspended)
362
+ * content:
363
+ * application/json:
364
+ * schema:
365
+ * $ref: '#/components/schemas/Agent'
366
+ * 401:
367
+ * description: Unauthorized
368
+ * 403:
369
+ * description: Not authorized to delist this agent
370
+ * 404:
371
+ * description: Agent not found
372
+ */
373
+ router.patch(
374
+ '/:id/delist',
375
+ authenticate,
376
+ async (req, res) => {
377
+ try {
378
+ const { id } = req.params;
379
+ const userId = (req as any).user.id;
380
+ const agentRepository = AppDataSource.getRepository(Agent);
381
+
382
+ const agent = await agentRepository.findOne({ where: { id } });
383
+ if (!agent) {
384
+ return res.status(404).json({ error: 'Agent not found' });
385
+ }
386
+
387
+ // Only creator can delist their own agent
388
+ if (agent.creatorId !== userId) {
389
+ return res.status(403).json({ error: 'Not authorized to delist this agent' });
390
+ }
391
+
392
+ agent.status = AgentStatus.SUSPENDED;
393
+ await agentRepository.save(agent);
394
+
395
+ res.json(agent);
396
+ } catch (error) {
397
+ console.error('Delist agent error:', error);
398
+ res.status(500).json({ error: 'Failed to delist agent' });
399
+ }
400
+ }
401
+ );
402
+
403
+ /**
404
+ * @swagger
405
+ * /agents/{id}/relist:
406
+ * patch:
407
+ * summary: Relist an agent (show in marketplace)
408
+ * tags: [Agents]
409
+ * description: Creator can relist their own suspended agent back to approved status (if it was previously approved)
410
+ * security:
411
+ * - bearerAuth: []
412
+ * parameters:
413
+ * - in: path
414
+ * name: id
415
+ * required: true
416
+ * schema:
417
+ * type: string
418
+ * format: uuid
419
+ * description: Agent ID
420
+ * responses:
421
+ * 200:
422
+ * description: Agent relisted (approved)
423
+ * content:
424
+ * application/json:
425
+ * schema:
426
+ * $ref: '#/components/schemas/Agent'
427
+ * 400:
428
+ * description: Agent was never approved, cannot relist
429
+ * 401:
430
+ * description: Unauthorized
431
+ * 403:
432
+ * description: Not authorized to relist this agent
433
+ * 404:
434
+ * description: Agent not found
435
+ */
436
+ router.patch(
437
+ '/:id/relist',
438
+ authenticate,
439
+ async (req, res) => {
440
+ try {
441
+ const { id } = req.params;
442
+ const userId = (req as any).user.id;
443
+ const agentRepository = AppDataSource.getRepository(Agent);
444
+
445
+ const agent = await agentRepository.findOne({ where: { id } });
446
+ if (!agent) {
447
+ return res.status(404).json({ error: 'Agent not found' });
448
+ }
449
+
450
+ // Only creator can relist their own agent
451
+ if (agent.creatorId !== userId) {
452
+ return res.status(403).json({ error: 'Not authorized to relist this agent' });
453
+ }
454
+
455
+ // Only relist if agent was previously approved (has approvedAt date)
456
+ if (!agent.approvedAt) {
457
+ return res.status(400).json({ error: 'Agent was never approved. Cannot relist. Please wait for admin approval.' });
458
+ }
459
+
460
+ agent.status = AgentStatus.APPROVED;
461
+ await agentRepository.save(agent);
462
+
463
+ res.json(agent);
464
+ } catch (error) {
465
+ console.error('Relist agent error:', error);
466
+ res.status(500).json({ error: 'Failed to relist agent' });
467
+ }
468
+ }
469
+ );
470
+
471
+ /**
472
+ * @swagger
473
+ * /agents/{id}/approve:
474
+ * patch:
475
+ * summary: Approve an agent (admin only)
476
+ * tags: [Agents]
477
+ * security:
478
+ * - bearerAuth: []
479
+ * parameters:
480
+ * - in: path
481
+ * name: id
482
+ * required: true
483
+ * schema:
484
+ * type: string
485
+ * format: uuid
486
+ * description: Agent ID
487
+ * responses:
488
+ * 200:
489
+ * description: Agent approved
490
+ * content:
491
+ * application/json:
492
+ * schema:
493
+ * $ref: '#/components/schemas/Agent'
494
+ * 401:
495
+ * description: Unauthorized
496
+ * 403:
497
+ * description: Admin access required
498
+ * 404:
499
+ * description: Agent not found
500
+ */
501
  // Admin routes
502
  router.patch(
503
  '/:id/approve',
 
527
  }
528
  );
529
 
530
+ /**
531
+ * @swagger
532
+ * /agents/{id}/reject:
533
+ * patch:
534
+ * summary: Reject an agent (admin only)
535
+ * tags: [Agents]
536
+ * security:
537
+ * - bearerAuth: []
538
+ * parameters:
539
+ * - in: path
540
+ * name: id
541
+ * required: true
542
+ * schema:
543
+ * type: string
544
+ * format: uuid
545
+ * description: Agent ID
546
+ * responses:
547
+ * 200:
548
+ * description: Agent rejected
549
+ * content:
550
+ * application/json:
551
+ * schema:
552
+ * $ref: '#/components/schemas/Agent'
553
+ * 401:
554
+ * description: Unauthorized
555
+ * 403:
556
+ * description: Admin access required
557
+ * 404:
558
+ * description: Agent not found
559
+ */
560
  router.patch(
561
  '/:id/reject',
562
  authenticate,
 
584
  }
585
  );
586
 
587
+ /**
588
+ * @swagger
589
+ * /agents/admin/pending:
590
+ * get:
591
+ * summary: Get pending agents (admin only)
592
+ * tags: [Agents]
593
+ * security:
594
+ * - bearerAuth: []
595
+ * responses:
596
+ * 200:
597
+ * description: List of pending agents
598
+ * content:
599
+ * application/json:
600
+ * schema:
601
+ * type: array
602
+ * items:
603
+ * $ref: '#/components/schemas/Agent'
604
+ * 401:
605
+ * description: Unauthorized
606
+ * 403:
607
+ * description: Admin access required
608
+ */
609
  router.get(
610
  '/admin/pending',
611
  authenticate,
src/routes/authRoutes.ts CHANGED
@@ -15,7 +15,68 @@ const messageRepository = AppDataSource.getRepository(ChatMessage);
15
  const walletService = new WalletService();
16
 
17
  /**
18
- * Register new user
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  */
20
  router.post('/register', async (req: Request, res: Response) => {
21
  try {
@@ -67,7 +128,59 @@ router.post('/register', async (req: Request, res: Response) => {
67
  });
68
 
69
  /**
70
- * Login
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  */
72
  router.post('/login', async (req: Request, res: Response) => {
73
  try {
@@ -116,7 +229,55 @@ router.post('/login', async (req: Request, res: Response) => {
116
  });
117
 
118
  /**
119
- * Get current user (profile + wallet + counts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  */
121
  router.get('/me', authenticate, async (req: Request, res: Response) => {
122
  try {
 
15
  const walletService = new WalletService();
16
 
17
  /**
18
+ * @swagger
19
+ * tags:
20
+ * name: Auth
21
+ * description: User authentication endpoints
22
+ */
23
+
24
+ /**
25
+ * @swagger
26
+ * /auth/register:
27
+ * post:
28
+ * summary: Register a new user
29
+ * tags: [Auth]
30
+ * security: []
31
+ * requestBody:
32
+ * required: true
33
+ * content:
34
+ * application/json:
35
+ * schema:
36
+ * type: object
37
+ * required:
38
+ * - email
39
+ * - password
40
+ * properties:
41
+ * email:
42
+ * type: string
43
+ * format: email
44
+ * example: user@example.com
45
+ * password:
46
+ * type: string
47
+ * format: password
48
+ * minLength: 6
49
+ * example: password123
50
+ * name:
51
+ * type: string
52
+ * example: John Doe
53
+ * responses:
54
+ * 201:
55
+ * description: User registered successfully
56
+ * content:
57
+ * application/json:
58
+ * schema:
59
+ * type: object
60
+ * properties:
61
+ * token:
62
+ * type: string
63
+ * description: JWT authentication token
64
+ * user:
65
+ * type: object
66
+ * properties:
67
+ * id:
68
+ * type: string
69
+ * format: uuid
70
+ * email:
71
+ * type: string
72
+ * name:
73
+ * type: string
74
+ * isAdmin:
75
+ * type: boolean
76
+ * 400:
77
+ * description: Invalid input or user already exists
78
+ * 500:
79
+ * description: Server error
80
  */
81
  router.post('/register', async (req: Request, res: Response) => {
82
  try {
 
128
  });
129
 
130
  /**
131
+ * @swagger
132
+ * /auth/login:
133
+ * post:
134
+ * summary: Login user
135
+ * tags: [Auth]
136
+ * security: []
137
+ * requestBody:
138
+ * required: true
139
+ * content:
140
+ * application/json:
141
+ * schema:
142
+ * type: object
143
+ * required:
144
+ * - email
145
+ * - password
146
+ * properties:
147
+ * email:
148
+ * type: string
149
+ * format: email
150
+ * example: user@example.com
151
+ * password:
152
+ * type: string
153
+ * format: password
154
+ * example: password123
155
+ * responses:
156
+ * 200:
157
+ * description: Login successful
158
+ * content:
159
+ * application/json:
160
+ * schema:
161
+ * type: object
162
+ * properties:
163
+ * token:
164
+ * type: string
165
+ * description: JWT authentication token
166
+ * user:
167
+ * type: object
168
+ * properties:
169
+ * id:
170
+ * type: string
171
+ * format: uuid
172
+ * email:
173
+ * type: string
174
+ * name:
175
+ * type: string
176
+ * isAdmin:
177
+ * type: boolean
178
+ * 401:
179
+ * description: Invalid credentials
180
+ * 403:
181
+ * description: Account disabled
182
+ * 500:
183
+ * description: Server error
184
  */
185
  router.post('/login', async (req: Request, res: Response) => {
186
  try {
 
229
  });
230
 
231
  /**
232
+ * @swagger
233
+ * /auth/me:
234
+ * get:
235
+ * summary: Get current user profile with wallet and counts
236
+ * tags: [Auth]
237
+ * security:
238
+ * - bearerAuth: []
239
+ * responses:
240
+ * 200:
241
+ * description: User profile with wallet and statistics
242
+ * content:
243
+ * application/json:
244
+ * schema:
245
+ * type: object
246
+ * properties:
247
+ * id:
248
+ * type: string
249
+ * format: uuid
250
+ * email:
251
+ * type: string
252
+ * name:
253
+ * type: string
254
+ * isAdmin:
255
+ * type: boolean
256
+ * createdAt:
257
+ * type: string
258
+ * format: date-time
259
+ * wallet:
260
+ * type: object
261
+ * properties:
262
+ * balance:
263
+ * type: number
264
+ * balanceInDollars:
265
+ * type: number
266
+ * freeTasksRemaining:
267
+ * type: integer
268
+ * counts:
269
+ * type: object
270
+ * properties:
271
+ * agentsCreated:
272
+ * type: integer
273
+ * totalMessages:
274
+ * type: integer
275
+ * 401:
276
+ * description: Unauthorized
277
+ * 404:
278
+ * description: User not found
279
+ * 500:
280
+ * description: Server error
281
  */
282
  router.get('/me', authenticate, async (req: Request, res: Response) => {
283
  try {
src/server.ts CHANGED
@@ -1,36 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import 'reflect-metadata';
2
- import dotenv from 'dotenv';
3
  import { AppDataSource } from './config/database';
4
  import app from './app';
5
 
6
- // Load environment variables
7
- dotenv.config();
8
-
9
- // Hugging Face Spaces uses port 7860 by default, but allow override
10
  const PORT = parseInt(process.env.PORT || process.env.SPACE_PORT || '7860', 10);
11
 
12
  console.log('πŸ”§ Starting server...');
13
  console.log(`πŸ“‘ Environment: ${process.env.NODE_ENV || 'development'}`);
14
  console.log(`🌐 Port: ${PORT}`);
15
- console.log(`πŸ“Š Database URL: ${process.env.DATABASE_URL ? 'Set' : 'Not set'}`);
 
 
 
 
 
 
 
 
16
 
17
- // Initialize database connection
18
  AppDataSource.initialize()
19
  .then(async () => {
20
  console.log('βœ… Database connected');
21
 
22
- // Run migrations if synchronize is disabled
23
  if (process.env.DB_SYNCHRONIZE !== 'true' && process.env.NODE_ENV === 'production') {
24
  try {
25
  console.log('πŸ”„ Running migrations...');
26
  await AppDataSource.runMigrations();
27
  console.log('βœ… Migrations completed');
28
  } catch (migrationError) {
29
- console.warn('⚠️ Migration warning (may already be up to date):', migrationError);
30
  }
31
  }
32
 
33
- // Start server
34
  app.listen(PORT, '0.0.0.0', () => {
35
  console.log(`πŸš€ Server running on port ${PORT}`);
36
  console.log(`🌍 Server accessible at http://0.0.0.0:${PORT}`);
 
1
+ const isHuggingFaceSpace = !!process.env.HF_SPACE_ID || !!process.env.SPACE_ID;
2
+ const isProduction = process.env.NODE_ENV === 'production';
3
+
4
+ if (!isHuggingFaceSpace && !isProduction) {
5
+ const dotenv = require('dotenv');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+
9
+ const envPath = path.join(__dirname, '../.env');
10
+ if (fs.existsSync(envPath)) {
11
+ dotenv.config({ path: envPath });
12
+ console.log(`πŸ“„ Development: Loaded .env from: ${envPath}`);
13
+ } else {
14
+ dotenv.config();
15
+ console.log('πŸ“„ Development: Using default .env location');
16
+ }
17
+ } else {
18
+ console.log('πŸ“„ Production: Using environment variables from platform secrets');
19
+ }
20
+
21
  import 'reflect-metadata';
 
22
  import { AppDataSource } from './config/database';
23
  import app from './app';
24
 
 
 
 
 
25
  const PORT = parseInt(process.env.PORT || process.env.SPACE_PORT || '7860', 10);
26
 
27
  console.log('πŸ”§ Starting server...');
28
  console.log(`πŸ“‘ Environment: ${process.env.NODE_ENV || 'development'}`);
29
  console.log(`🌐 Port: ${PORT}`);
30
+ if (process.env.DATABASE_URL) {
31
+ const dbUrl = process.env.DATABASE_URL;
32
+ const masked = dbUrl.length > 60
33
+ ? `${dbUrl.substring(0, 30)}...${dbUrl.substring(dbUrl.length - 10)}`
34
+ : dbUrl;
35
+ console.log(`πŸ“Š Database URL: Set (${masked})`);
36
+ } else {
37
+ console.log(`πŸ“Š Database URL: ❌ NOT SET`);
38
+ }
39
 
 
40
  AppDataSource.initialize()
41
  .then(async () => {
42
  console.log('βœ… Database connected');
43
 
 
44
  if (process.env.DB_SYNCHRONIZE !== 'true' && process.env.NODE_ENV === 'production') {
45
  try {
46
  console.log('πŸ”„ Running migrations...');
47
  await AppDataSource.runMigrations();
48
  console.log('βœ… Migrations completed');
49
  } catch (migrationError) {
50
+ console.warn('⚠️ Migration warning:', migrationError);
51
  }
52
  }
53
 
 
54
  app.listen(PORT, '0.0.0.0', () => {
55
  console.log(`πŸš€ Server running on port ${PORT}`);
56
  console.log(`🌍 Server accessible at http://0.0.0.0:${PORT}`);
src/services/exchangeRateService.ts CHANGED
@@ -1,32 +1,21 @@
1
  import axios from 'axios';
2
 
3
- /**
4
- * Exchange Rate Service
5
- * Fetches current NGN to USD exchange rate from various APIs
6
- * Falls back to fixed rate if API fails
7
- */
8
  export class ExchangeRateService {
9
  private cachedRate: number | null = null;
10
  private cacheExpiry: Date | null = null;
11
- private readonly CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour cache
12
  private readonly FALLBACK_RATE: number;
13
 
14
  constructor() {
15
  this.FALLBACK_RATE = parseFloat(process.env.NGN_PER_USD || '750');
16
  }
17
 
18
- /**
19
- * Get current NGN to USD exchange rate
20
- * Uses cache if available and not expired
21
- */
22
  async getNGNToUSDRate(): Promise<number> {
23
- // Check cache first
24
  if (this.cachedRate && this.cacheExpiry && new Date() < this.cacheExpiry) {
25
  console.log('Using cached exchange rate:', this.cachedRate);
26
  return this.cachedRate;
27
  }
28
 
29
- // Try to fetch from API
30
  try {
31
  const rate = await this.fetchExchangeRate();
32
  this.cachedRate = rate;
@@ -39,12 +28,7 @@ export class ExchangeRateService {
39
  }
40
  }
41
 
42
- /**
43
- * Fetch exchange rate from external API
44
- * Tries multiple sources for reliability
45
- */
46
  private async fetchExchangeRate(): Promise<number> {
47
- // Option 1: ExchangeRate-API (free, no API key needed)
48
  try {
49
  const response = await axios.get('https://api.exchangerate-api.com/v4/latest/USD', {
50
  timeout: 5000,
 
1
  import axios from 'axios';
2
 
 
 
 
 
 
3
  export class ExchangeRateService {
4
  private cachedRate: number | null = null;
5
  private cacheExpiry: Date | null = null;
6
+ private readonly CACHE_DURATION_MS = 60 * 60 * 1000;
7
  private readonly FALLBACK_RATE: number;
8
 
9
  constructor() {
10
  this.FALLBACK_RATE = parseFloat(process.env.NGN_PER_USD || '750');
11
  }
12
 
 
 
 
 
13
  async getNGNToUSDRate(): Promise<number> {
 
14
  if (this.cachedRate && this.cacheExpiry && new Date() < this.cacheExpiry) {
15
  console.log('Using cached exchange rate:', this.cachedRate);
16
  return this.cachedRate;
17
  }
18
 
 
19
  try {
20
  const rate = await this.fetchExchangeRate();
21
  this.cachedRate = rate;
 
28
  }
29
  }
30
 
 
 
 
 
31
  private async fetchExchangeRate(): Promise<number> {
 
32
  try {
33
  const response = await axios.get('https://api.exchangerate-api.com/v4/latest/USD', {
34
  timeout: 5000,
src/services/ipfsService.ts CHANGED
@@ -1,9 +1,5 @@
1
  import { PinataSDK } from 'pinata';
2
 
3
- /**
4
- * IPFS Service via Pinata SDK
5
- * Handles storing and retrieving agent metadata from IPFS
6
- */
7
  export class IpfsService {
8
  private pinata: PinataSDK | null;
9
  private gatewayUrl: string;
@@ -17,13 +13,10 @@ export class IpfsService {
17
  this.pinata = null;
18
  this.gatewayUrl = '';
19
  } else {
20
- // Gateway URL should be in format "fun-llama-300.mypinata.cloud" (no https://)
21
- // Remove protocol if present, SDK expects just the domain
22
  const gatewayDomain = gatewayUrl
23
  ? gatewayUrl.replace(/^https?:\/\//, '').replace(/\/ipfs\/?$/, '')
24
  : 'gateway.pinata.cloud';
25
 
26
- // Store full URL for direct access
27
  this.gatewayUrl = `https://${gatewayDomain}/ipfs/`;
28
 
29
  if (!gatewayUrl) {
@@ -37,9 +30,6 @@ export class IpfsService {
37
  }
38
  }
39
 
40
- /**
41
- * Upload agent metadata to IPFS
42
- */
43
  async uploadMetadata(metadata: {
44
  name: string;
45
  description: string;
 
1
  import { PinataSDK } from 'pinata';
2
 
 
 
 
 
3
  export class IpfsService {
4
  private pinata: PinataSDK | null;
5
  private gatewayUrl: string;
 
13
  this.pinata = null;
14
  this.gatewayUrl = '';
15
  } else {
 
 
16
  const gatewayDomain = gatewayUrl
17
  ? gatewayUrl.replace(/^https?:\/\//, '').replace(/\/ipfs\/?$/, '')
18
  : 'gateway.pinata.cloud';
19
 
 
20
  this.gatewayUrl = `https://${gatewayDomain}/ipfs/`;
21
 
22
  if (!gatewayUrl) {
 
30
  }
31
  }
32
 
 
 
 
33
  async uploadMetadata(metadata: {
34
  name: string;
35
  description: string;
src/services/paystackService.ts CHANGED
@@ -1,13 +1,14 @@
1
  const Paystack = require('paystack-node');
2
 
3
- /**
4
- * Paystack Service
5
- * Handles payment initialization and verification using paystack-node
6
- */
7
  export class PaystackService {
8
  private paystack: any;
 
 
 
 
 
 
9
 
10
- constructor() {
11
  const secretKey = process.env.PAYSTACK_SECRET_KEY;
12
  if (!secretKey) {
13
  throw new Error('PAYSTACK_SECRET_KEY not configured');
@@ -15,12 +16,9 @@ export class PaystackService {
15
 
16
  const environment = process.env.NODE_ENV === 'production' ? 'production' : 'development';
17
  this.paystack = new Paystack(secretKey, environment);
 
18
  }
19
 
20
- /**
21
- * Initialize transaction
22
- * Returns authorization_url for redirect or frontend widget
23
- */
24
  async initializeTransaction(data: {
25
  email: string;
26
  amount: number; // Amount in kobo (NGN) or smallest currency unit
@@ -28,11 +26,11 @@ export class PaystackService {
28
  metadata?: Record<string, any>;
29
  callback_url?: string;
30
  }) {
 
31
  try {
32
- // paystack-node expects metadata as JSON string, not object
33
  const payload: any = {
34
  email: data.email,
35
- amount: data.amount, // Already in kobo/smallest unit
36
  reference: data.reference,
37
  };
38
 
@@ -64,6 +62,7 @@ export class PaystackService {
64
  * Verify transaction by reference
65
  */
66
  async verifyTransaction(reference: string) {
 
67
  try {
68
  const response = await this.paystack.verifyTransaction({
69
  reference: reference,
@@ -109,5 +108,12 @@ export class PaystackService {
109
  }
110
  return publicKey;
111
  }
 
 
 
 
 
 
 
112
  }
113
 
 
1
  const Paystack = require('paystack-node');
2
 
 
 
 
 
3
  export class PaystackService {
4
  private paystack: any;
5
+ private initialized: boolean = false;
6
+
7
+ private initialize() {
8
+ if (this.initialized) {
9
+ return;
10
+ }
11
 
 
12
  const secretKey = process.env.PAYSTACK_SECRET_KEY;
13
  if (!secretKey) {
14
  throw new Error('PAYSTACK_SECRET_KEY not configured');
 
16
 
17
  const environment = process.env.NODE_ENV === 'production' ? 'production' : 'development';
18
  this.paystack = new Paystack(secretKey, environment);
19
+ this.initialized = true;
20
  }
21
 
 
 
 
 
22
  async initializeTransaction(data: {
23
  email: string;
24
  amount: number; // Amount in kobo (NGN) or smallest currency unit
 
26
  metadata?: Record<string, any>;
27
  callback_url?: string;
28
  }) {
29
+ this.initialize();
30
  try {
 
31
  const payload: any = {
32
  email: data.email,
33
+ amount: data.amount,
34
  reference: data.reference,
35
  };
36
 
 
62
  * Verify transaction by reference
63
  */
64
  async verifyTransaction(reference: string) {
65
+ this.initialize();
66
  try {
67
  const response = await this.paystack.verifyTransaction({
68
  reference: reference,
 
108
  }
109
  return publicKey;
110
  }
111
+
112
+ /**
113
+ * Check if Paystack is configured
114
+ */
115
+ isConfigured(): boolean {
116
+ return !!(process.env.PAYSTACK_SECRET_KEY && process.env.PAYSTACK_PUBLIC_KEY);
117
+ }
118
  }
119