push
Browse files- src/controllers/agentController.ts +18 -0
- src/controllers/walletController.ts +9 -1
- src/docs/swagger.ts +45 -0
- src/routes/agentRoutes.ts +22 -0
- src/routes/authRoutes.ts +14 -21
- src/routes/userRoutes.ts +92 -1
src/controllers/agentController.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import { Request, Response } from 'express';
|
| 2 |
import { AppDataSource } from '../config/database';
|
| 3 |
import { Agent, AgentStatus } from '../entities/Agent';
|
|
|
|
| 4 |
import { IpfsService } from '../services/ipfsService';
|
| 5 |
|
| 6 |
const agentRepository = AppDataSource.getRepository(Agent);
|
|
@@ -119,6 +120,23 @@ export class AgentController {
|
|
| 119 |
async createAgent(req: Request, res: Response) {
|
| 120 |
try {
|
| 121 |
const userId = (req as any).user.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
const avatarFile = (req as any).file as MulterFile | undefined;
|
| 123 |
const {
|
| 124 |
name,
|
|
|
|
| 1 |
import { Request, Response } from 'express';
|
| 2 |
import { AppDataSource } from '../config/database';
|
| 3 |
import { Agent, AgentStatus } from '../entities/Agent';
|
| 4 |
+
import { User } from '../entities/User';
|
| 5 |
import { IpfsService } from '../services/ipfsService';
|
| 6 |
|
| 7 |
const agentRepository = AppDataSource.getRepository(Agent);
|
|
|
|
| 120 |
async createAgent(req: Request, res: Response) {
|
| 121 |
try {
|
| 122 |
const userId = (req as any).user.id;
|
| 123 |
+
const isAdmin = (req as any).user.isAdmin;
|
| 124 |
+
|
| 125 |
+
// Check if user is a creator or admin
|
| 126 |
+
const userRepository = AppDataSource.getRepository(User);
|
| 127 |
+
const user = await userRepository.findOne({ where: { id: userId } });
|
| 128 |
+
|
| 129 |
+
if (!user) {
|
| 130 |
+
return res.status(404).json({ error: 'User not found' });
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (!user.isCreator && !isAdmin) {
|
| 134 |
+
return res.status(403).json({
|
| 135 |
+
error: 'You must be a creator to create agents',
|
| 136 |
+
message: 'Please become a creator first by calling POST /api/users/become-creator',
|
| 137 |
+
becomeCreatorEndpoint: '/api/users/become-creator',
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
const avatarFile = (req as any).file as MulterFile | undefined;
|
| 141 |
const {
|
| 142 |
name,
|
src/controllers/walletController.ts
CHANGED
|
@@ -61,6 +61,14 @@ export class WalletController {
|
|
| 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),
|
|
@@ -71,7 +79,7 @@ export class WalletController {
|
|
| 71 |
amountInNaira: amount,
|
| 72 |
amountInDollars: amountInDollars,
|
| 73 |
},
|
| 74 |
-
callback_url:
|
| 75 |
});
|
| 76 |
|
| 77 |
// Return payment info for frontend
|
|
|
|
| 61 |
|
| 62 |
const paymentReference = `wallet_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
| 63 |
|
| 64 |
+
// Paystack callback should point to backend endpoint, which then redirects to frontend
|
| 65 |
+
// For Hugging Face Spaces: https://nexusbert-zurri.hf.space/api/wallet/callback
|
| 66 |
+
// For local: http://localhost:7860/api/wallet/callback
|
| 67 |
+
const backendUrl = process.env.PAYSTACK_CALLBACK_URL
|
| 68 |
+
|| (process.env.HF_SPACE_ID || process.env.SPACE_ID
|
| 69 |
+
? `https://nexusbert-zurri.hf.space/api/wallet/callback`
|
| 70 |
+
: `${process.env.API_BASE_URL || process.env.BACKEND_URL || 'http://localhost:7860'}/api/wallet/callback`);
|
| 71 |
+
|
| 72 |
const paymentData = await getPaystackService().initializeTransaction({
|
| 73 |
email: user.email,
|
| 74 |
amount: Math.round(amount * 100),
|
|
|
|
| 79 |
amountInNaira: amount,
|
| 80 |
amountInDollars: amountInDollars,
|
| 81 |
},
|
| 82 |
+
callback_url: backendUrl,
|
| 83 |
});
|
| 84 |
|
| 85 |
// Return payment info for frontend
|
src/docs/swagger.ts
CHANGED
|
@@ -198,6 +198,51 @@ export const swaggerSpec = swaggerJSDoc({
|
|
| 198 |
},
|
| 199 |
},
|
| 200 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
},
|
| 202 |
},
|
| 203 |
security: [{ bearerAuth: [] }],
|
|
|
|
| 198 |
},
|
| 199 |
},
|
| 200 |
},
|
| 201 |
+
User: {
|
| 202 |
+
type: 'object',
|
| 203 |
+
properties: {
|
| 204 |
+
id: {
|
| 205 |
+
type: 'string',
|
| 206 |
+
format: 'uuid',
|
| 207 |
+
description: 'User ID',
|
| 208 |
+
},
|
| 209 |
+
email: {
|
| 210 |
+
type: 'string',
|
| 211 |
+
format: 'email',
|
| 212 |
+
description: 'User email address',
|
| 213 |
+
},
|
| 214 |
+
name: {
|
| 215 |
+
type: 'string',
|
| 216 |
+
nullable: true,
|
| 217 |
+
description: 'User display name',
|
| 218 |
+
},
|
| 219 |
+
isAdmin: {
|
| 220 |
+
type: 'boolean',
|
| 221 |
+
default: false,
|
| 222 |
+
description: 'Whether user has admin privileges',
|
| 223 |
+
},
|
| 224 |
+
isCreator: {
|
| 225 |
+
type: 'boolean',
|
| 226 |
+
default: false,
|
| 227 |
+
description: 'Whether user can create and manage agents',
|
| 228 |
+
},
|
| 229 |
+
isActive: {
|
| 230 |
+
type: 'boolean',
|
| 231 |
+
default: true,
|
| 232 |
+
description: 'Whether user account is active',
|
| 233 |
+
},
|
| 234 |
+
createdAt: {
|
| 235 |
+
type: 'string',
|
| 236 |
+
format: 'date-time',
|
| 237 |
+
description: 'Account creation timestamp',
|
| 238 |
+
},
|
| 239 |
+
updatedAt: {
|
| 240 |
+
type: 'string',
|
| 241 |
+
format: 'date-time',
|
| 242 |
+
description: 'Last update timestamp',
|
| 243 |
+
},
|
| 244 |
+
},
|
| 245 |
+
},
|
| 246 |
},
|
| 247 |
},
|
| 248 |
security: [{ bearerAuth: [] }],
|
src/routes/agentRoutes.ts
CHANGED
|
@@ -107,6 +107,28 @@ router.get('/', optionalAuth, agentController.listAgents.bind(agentController));
|
|
| 107 |
// Specific routes must come BEFORE /:id to avoid route conflicts
|
| 108 |
// These routes are matched first to prevent conflicts with /:id
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
/**
|
| 111 |
* @swagger
|
| 112 |
* /agents/my/list:
|
|
|
|
| 107 |
// Specific routes must come BEFORE /:id to avoid route conflicts
|
| 108 |
// These routes are matched first to prevent conflicts with /:id
|
| 109 |
|
| 110 |
+
/**
|
| 111 |
+
* @swagger
|
| 112 |
+
* /agents/new:
|
| 113 |
+
* get:
|
| 114 |
+
* summary: Get agent creation form info (frontend route)
|
| 115 |
+
* tags: [Agents]
|
| 116 |
+
* description: This endpoint exists to prevent /new from being caught by /:id route
|
| 117 |
+
* security:
|
| 118 |
+
* - bearerAuth: []
|
| 119 |
+
* responses:
|
| 120 |
+
* 200:
|
| 121 |
+
* description: Agent creation info
|
| 122 |
+
*/
|
| 123 |
+
router.get('/new', authenticate, (req, res) => {
|
| 124 |
+
res.json({
|
| 125 |
+
message: 'Use POST /api/agents to create an agent',
|
| 126 |
+
requiresCreator: true,
|
| 127 |
+
endpoint: '/api/agents',
|
| 128 |
+
method: 'POST',
|
| 129 |
+
});
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
/**
|
| 133 |
* @swagger
|
| 134 |
* /agents/my/list:
|
src/routes/authRoutes.ts
CHANGED
|
@@ -134,15 +134,16 @@ router.post('/register', authLimiter, async (req: Request, res: Response) => {
|
|
| 134 |
{ expiresIn: '7d' }
|
| 135 |
);
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
} catch (error) {
|
| 147 |
console.error('Register error:', error);
|
| 148 |
res.status(500).json({ error: 'Failed to register user' });
|
|
@@ -186,17 +187,7 @@ router.post('/register', authLimiter, async (req: Request, res: Response) => {
|
|
| 186 |
* type: string
|
| 187 |
* description: JWT authentication token
|
| 188 |
* user:
|
| 189 |
-
*
|
| 190 |
-
* properties:
|
| 191 |
-
* id:
|
| 192 |
-
* type: string
|
| 193 |
-
* format: uuid
|
| 194 |
-
* email:
|
| 195 |
-
* type: string
|
| 196 |
-
* name:
|
| 197 |
-
* type: string
|
| 198 |
-
* isAdmin:
|
| 199 |
-
* type: boolean
|
| 200 |
* 401:
|
| 201 |
* description: Invalid credentials
|
| 202 |
* 403:
|
|
@@ -272,6 +263,7 @@ router.post('/login', authLimiter, async (req: Request, res: Response) => {
|
|
| 272 |
email: user.email,
|
| 273 |
name: user.name,
|
| 274 |
isAdmin: user.isAdmin,
|
|
|
|
| 275 |
},
|
| 276 |
});
|
| 277 |
} catch (error) {
|
|
@@ -336,7 +328,7 @@ router.get('/me', authenticate, async (req: Request, res: Response) => {
|
|
| 336 |
const userId = (req as any).user.id;
|
| 337 |
const user = await userRepository.findOne({
|
| 338 |
where: { id: userId },
|
| 339 |
-
select: ['id', 'email', 'name', 'isAdmin', 'createdAt'],
|
| 340 |
});
|
| 341 |
|
| 342 |
if (!user) {
|
|
@@ -355,6 +347,7 @@ router.get('/me', authenticate, async (req: Request, res: Response) => {
|
|
| 355 |
email: user.email,
|
| 356 |
name: user.name,
|
| 357 |
isAdmin: user.isAdmin,
|
|
|
|
| 358 |
createdAt: user.createdAt,
|
| 359 |
wallet: {
|
| 360 |
balance: Number(wallet.balance),
|
|
|
|
| 134 |
{ expiresIn: '7d' }
|
| 135 |
);
|
| 136 |
|
| 137 |
+
res.status(201).json({
|
| 138 |
+
token,
|
| 139 |
+
user: {
|
| 140 |
+
id: savedUser.id,
|
| 141 |
+
email: savedUser.email,
|
| 142 |
+
name: savedUser.name,
|
| 143 |
+
isAdmin: savedUser.isAdmin,
|
| 144 |
+
isCreator: savedUser.isCreator,
|
| 145 |
+
},
|
| 146 |
+
});
|
| 147 |
} catch (error) {
|
| 148 |
console.error('Register error:', error);
|
| 149 |
res.status(500).json({ error: 'Failed to register user' });
|
|
|
|
| 187 |
* type: string
|
| 188 |
* description: JWT authentication token
|
| 189 |
* user:
|
| 190 |
+
* $ref: '#/components/schemas/User'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
* 401:
|
| 192 |
* description: Invalid credentials
|
| 193 |
* 403:
|
|
|
|
| 263 |
email: user.email,
|
| 264 |
name: user.name,
|
| 265 |
isAdmin: user.isAdmin,
|
| 266 |
+
isCreator: user.isCreator,
|
| 267 |
},
|
| 268 |
});
|
| 269 |
} catch (error) {
|
|
|
|
| 328 |
const userId = (req as any).user.id;
|
| 329 |
const user = await userRepository.findOne({
|
| 330 |
where: { id: userId },
|
| 331 |
+
select: ['id', 'email', 'name', 'isAdmin', 'isCreator', 'createdAt'],
|
| 332 |
});
|
| 333 |
|
| 334 |
if (!user) {
|
|
|
|
| 347 |
email: user.email,
|
| 348 |
name: user.name,
|
| 349 |
isAdmin: user.isAdmin,
|
| 350 |
+
isCreator: user.isCreator,
|
| 351 |
createdAt: user.createdAt,
|
| 352 |
wallet: {
|
| 353 |
balance: Number(wallet.balance),
|
src/routes/userRoutes.ts
CHANGED
|
@@ -1,10 +1,101 @@
|
|
| 1 |
-
import { Router } from 'express';
|
| 2 |
import { authenticate } from '../middlewares/auth';
|
| 3 |
import { UserController } from '../controllers/userController';
|
|
|
|
|
|
|
| 4 |
|
| 5 |
const router = Router();
|
| 6 |
const userController = new UserController();
|
|
|
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
router.get('/me/history', authenticate, userController.getMyHistory.bind(userController));
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
export default router;
|
|
|
|
| 1 |
+
import { Router, Request, Response } from 'express';
|
| 2 |
import { authenticate } from '../middlewares/auth';
|
| 3 |
import { UserController } from '../controllers/userController';
|
| 4 |
+
import { AppDataSource } from '../config/database';
|
| 5 |
+
import { User } from '../entities/User';
|
| 6 |
|
| 7 |
const router = Router();
|
| 8 |
const userController = new UserController();
|
| 9 |
+
const userRepository = AppDataSource.getRepository(User);
|
| 10 |
|
| 11 |
+
/**
|
| 12 |
+
* @swagger
|
| 13 |
+
* /users/me/history:
|
| 14 |
+
* get:
|
| 15 |
+
* summary: Get user's chat history across all agents
|
| 16 |
+
* tags: [Users]
|
| 17 |
+
* security:
|
| 18 |
+
* - bearerAuth: []
|
| 19 |
+
* responses:
|
| 20 |
+
* 200:
|
| 21 |
+
* description: Chat history
|
| 22 |
+
*/
|
| 23 |
router.get('/me/history', authenticate, userController.getMyHistory.bind(userController));
|
| 24 |
|
| 25 |
+
/**
|
| 26 |
+
* @swagger
|
| 27 |
+
* /users/become-creator:
|
| 28 |
+
* post:
|
| 29 |
+
* summary: Become a creator (enable agent creation)
|
| 30 |
+
* tags: [Users]
|
| 31 |
+
* description: |
|
| 32 |
+
* Allows a regular user to become a creator and start creating agents.
|
| 33 |
+
* This sets the `isCreator` field to `true` in the database, enabling the user to:
|
| 34 |
+
* - Create new agent listings
|
| 35 |
+
* - Manage their own agents (edit, delete, delist, relist)
|
| 36 |
+
* - View creator dashboard and earnings
|
| 37 |
+
* security:
|
| 38 |
+
* - bearerAuth: []
|
| 39 |
+
* responses:
|
| 40 |
+
* 200:
|
| 41 |
+
* description: Successfully became a creator
|
| 42 |
+
* content:
|
| 43 |
+
* application/json:
|
| 44 |
+
* schema:
|
| 45 |
+
* type: object
|
| 46 |
+
* properties:
|
| 47 |
+
* message:
|
| 48 |
+
* type: string
|
| 49 |
+
* example: "You are now a creator! You can create and manage agents."
|
| 50 |
+
* user:
|
| 51 |
+
* $ref: '#/components/schemas/User'
|
| 52 |
+
* 400:
|
| 53 |
+
* description: User is already a creator
|
| 54 |
+
* content:
|
| 55 |
+
* application/json:
|
| 56 |
+
* schema:
|
| 57 |
+
* type: object
|
| 58 |
+
* properties:
|
| 59 |
+
* error:
|
| 60 |
+
* type: string
|
| 61 |
+
* example: "You are already a creator"
|
| 62 |
+
* 401:
|
| 63 |
+
* description: Unauthorized - authentication required
|
| 64 |
+
* 404:
|
| 65 |
+
* description: User not found
|
| 66 |
+
* 500:
|
| 67 |
+
* description: Server error
|
| 68 |
+
*/
|
| 69 |
+
router.post('/become-creator', authenticate, async (req: Request, res: Response) => {
|
| 70 |
+
try {
|
| 71 |
+
const userId = (req as any).user.id;
|
| 72 |
+
const user = await userRepository.findOne({ where: { id: userId } });
|
| 73 |
+
|
| 74 |
+
if (!user) {
|
| 75 |
+
return res.status(404).json({ error: 'User not found' });
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if (user.isCreator) {
|
| 79 |
+
return res.status(400).json({ error: 'You are already a creator' });
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
user.isCreator = true;
|
| 83 |
+
await userRepository.save(user);
|
| 84 |
+
|
| 85 |
+
res.json({
|
| 86 |
+
message: 'You are now a creator! You can create and manage agents.',
|
| 87 |
+
user: {
|
| 88 |
+
id: user.id,
|
| 89 |
+
email: user.email,
|
| 90 |
+
name: user.name,
|
| 91 |
+
isCreator: user.isCreator,
|
| 92 |
+
isAdmin: user.isAdmin,
|
| 93 |
+
},
|
| 94 |
+
});
|
| 95 |
+
} catch (error) {
|
| 96 |
+
console.error('Become creator error:', error);
|
| 97 |
+
res.status(500).json({ error: 'Failed to become creator' });
|
| 98 |
+
}
|
| 99 |
+
});
|
| 100 |
+
|
| 101 |
export default router;
|