Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
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;