Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at dev 299 lines 8.7 kB view raw
1import { Router } from 'express'; 2import pool from '../utils/pgClient'; 3import logger from '../utils/logger'; 4import { authenticateToken } from '../middlewares/auth'; 5import { body, validationResult } from 'express-validator'; 6import { encrypt as encryptApiKey } from '../utils/encrypt'; 7import OpenAI from 'openai'; 8 9const ALLOWED_API_HOSTS = [ 10 'api.openai.com', 11 'openrouter.ai', 12 'generativelanguage.googleapis.com', 13 'api.anthropic.com', 14]; 15 16function getOpenAIClient(apiKey: string, baseURL?: string): OpenAI { 17 return new OpenAI({ 18 apiKey, 19 baseURL: baseURL || 'https://api.openai.com/v1', 20 defaultHeaders: 21 new URL(baseURL || '').hostname === 'openrouter.ai' 22 ? { 23 'HTTP-Referer': 'https://aethel.xyz', 24 'X-Title': 'Aethel Discord Bot', 25 } 26 : {}, 27 }); 28} 29 30const router = Router(); 31 32router.use(authenticateToken); 33 34router.get('/', async (req, res) => { 35 try { 36 const userId = req.user?.userId; 37 if (!userId) { 38 return res.status(401).json({ error: 'User not authenticated' }); 39 } 40 41 const query = ` 42 SELECT custom_model, custom_api_url, 43 CASE WHEN api_key_encrypted IS NOT NULL THEN TRUE ELSE FALSE END as has_api_key 44 FROM users 45 WHERE user_id = $1 46 `; 47 const result = await pool.query(query, [userId]); 48 49 if (result.rows.length === 0) { 50 return res.status(404).json({ error: 'User not found' }); 51 } 52 53 const user = result.rows[0]; 54 res.json({ 55 hasApiKey: user.has_api_key, 56 model: user.custom_model, 57 apiUrl: user.custom_api_url, 58 }); 59 } catch (error) { 60 logger.error('Error fetching API key info:', error); 61 res.status(500).json({ error: 'Internal server error' }); 62 } 63}); 64 65router.post( 66 '/', 67 body('apiKey') 68 .trim() 69 .isLength({ min: 1, max: 1000 }) 70 .withMessage('API key is required and must be less than 1000 characters'), 71 body('model') 72 .optional() 73 .trim() 74 .isLength({ max: 100 }) 75 .withMessage('Model name must be less than 100 characters'), 76 body('apiUrl') 77 .optional() 78 .trim() 79 .isURL({ require_protocol: true }) 80 .withMessage('API URL must be a valid URL with protocol'), 81 async (req, res) => { 82 try { 83 const errors = validationResult(req); 84 if (!errors.isEmpty()) { 85 return res.status(400).json({ error: errors.array()[0].msg }); 86 } 87 88 const userId = req.user?.userId; 89 if (!userId) { 90 return res.status(401).json({ error: 'User not authenticated' }); 91 } 92 93 const { apiKey, model, apiUrl } = req.body; 94 95 const encryptedApiKey = encryptApiKey(apiKey); 96 97 const query = ` 98 UPDATE users 99 SET api_key_encrypted = $1, 100 custom_model = $2, 101 custom_api_url = $3 102 WHERE user_id = $4 103 RETURNING user_id 104 `; 105 const result = await pool.query(query, [ 106 encryptedApiKey, 107 model || null, 108 apiUrl || null, 109 userId, 110 ]); 111 112 if (result.rows.length === 0) { 113 return res.status(404).json({ error: 'User not found' }); 114 } 115 116 logger.info(`API key updated for user ${userId}`); 117 res.json({ message: 'API key updated successfully' }); 118 } catch (error) { 119 logger.error('Error updating API key:', error); 120 res.status(500).json({ error: 'Internal server error' }); 121 } 122 }, 123); 124 125router.put( 126 '/', 127 body('apiKey') 128 .trim() 129 .isLength({ min: 1, max: 1000 }) 130 .withMessage('API key is required and must be less than 1000 characters'), 131 body('model') 132 .optional() 133 .trim() 134 .isLength({ max: 100 }) 135 .withMessage('Model name must be less than 100 characters'), 136 body('apiUrl') 137 .optional() 138 .trim() 139 .isURL({ require_protocol: true }) 140 .withMessage('API URL must be a valid URL with protocol'), 141 async (req, res) => { 142 try { 143 const errors = validationResult(req); 144 if (!errors.isEmpty()) { 145 return res.status(400).json({ error: errors.array()[0].msg }); 146 } 147 148 const userId = req.user?.userId; 149 if (!userId) { 150 return res.status(401).json({ error: 'User not authenticated' }); 151 } 152 153 const { apiKey, model, apiUrl } = req.body; 154 155 const encryptedApiKey = encryptApiKey(apiKey); 156 157 const query = ` 158 UPDATE users 159 SET api_key_encrypted = $1, 160 custom_model = $2, 161 custom_api_url = $3 162 WHERE user_id = $4 163 RETURNING user_id 164 `; 165 const result = await pool.query(query, [ 166 encryptedApiKey, 167 model || null, 168 apiUrl || null, 169 userId, 170 ]); 171 172 if (result.rows.length === 0) { 173 return res.status(404).json({ error: 'User not found' }); 174 } 175 176 logger.info(`API key updated for user ${userId}`); 177 res.json({ message: 'API key updated successfully' }); 178 } catch (error) { 179 logger.error('Error updating API key:', error); 180 res.status(500).json({ error: 'Internal server error' }); 181 } 182 }, 183); 184 185router.delete('/', async (req, res) => { 186 try { 187 const userId = req.user?.userId; 188 if (!userId) { 189 return res.status(401).json({ error: 'User not authenticated' }); 190 } 191 192 const query = ` 193 UPDATE users 194 SET api_key_encrypted = NULL, 195 custom_model = NULL, 196 custom_api_url = NULL 197 WHERE user_id = $1 198 RETURNING user_id 199 `; 200 const result = await pool.query(query, [userId]); 201 202 if (result.rows.length === 0) { 203 return res.status(404).json({ error: 'User not found' }); 204 } 205 206 logger.info(`API key deleted for user ${userId}`); 207 res.json({ message: 'API key deleted successfully' }); 208 } catch (error) { 209 logger.error('Error deleting API key:', error); 210 res.status(500).json({ error: 'Internal server error' }); 211 } 212}); 213 214router.post( 215 '/test', 216 body('apiKey').trim().isLength({ min: 1 }).withMessage('API key is required'), 217 body('model').optional().trim(), 218 body('apiUrl') 219 .optional() 220 .trim() 221 .isURL({ require_protocol: true }) 222 .withMessage('API URL must be a valid URL with protocol'), 223 async (req, res) => { 224 try { 225 const errors = validationResult(req); 226 if (!errors.isEmpty()) { 227 return res.status(400).json({ error: errors.array()[0].msg }); 228 } 229 230 const { apiKey, model, apiUrl } = req.body; 231 const userId = req.user?.userId; 232 233 const fullApiUrl = apiUrl || 'https://openrouter.ai/api/v1'; 234 235 let parsedUrl; 236 try { 237 parsedUrl = new URL(fullApiUrl); 238 } catch { 239 logger.warn(`Blocked invalid API URL for user ${userId}: ${fullApiUrl}`); 240 return res.status(400).json({ 241 error: 242 'API URL is invalid. Please use a supported API endpoint (OpenAI, OpenRouter, Anthropic, or Google Gemini).', 243 }); 244 } 245 if (!ALLOWED_API_HOSTS.includes(parsedUrl.hostname)) { 246 logger.warn(`Blocked potentially malicious API URL for user ${userId}: ${fullApiUrl}`); 247 return res.status(400).json({ 248 error: 249 'API URL not allowed. Please use a supported API endpoint (OpenAI, OpenRouter, Anthropic, or Google Gemini).', 250 }); 251 } 252 253 const testModel = model || 'openai/gpt-4o-mini'; 254 const client = getOpenAIClient(apiKey, fullApiUrl); 255 256 try { 257 const response = await client.chat.completions.create({ 258 model: testModel, 259 messages: [ 260 { 261 role: 'user', 262 content: 263 'Hello! This is a test message. Please respond with "API key test successful!"', 264 }, 265 ], 266 max_tokens: 50, 267 temperature: 0.1, 268 }); 269 270 const testMessage = response.choices?.[0]?.message?.content || 'Test completed'; 271 272 logger.info(`API key test successful for user ${userId}`); 273 res.json({ 274 success: true, 275 message: 'API key is valid and working!', 276 testResponse: testMessage, 277 }); 278 } catch (error: unknown) { 279 const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 280 logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); 281 return res.status(400).json({ 282 error: `API key test failed: ${errorMessage}`, 283 }); 284 } 285 } catch (error) { 286 logger.error('Error testing API key:', error); 287 288 if (error instanceof TypeError && error.message.includes('fetch')) { 289 return res 290 .status(400) 291 .json({ error: 'Failed to connect to API endpoint. Please check the URL.' }); 292 } 293 294 res.status(500).json({ error: 'API key test failed due to server error' }); 295 } 296 }, 297); 298 299export default router;