eletrotupi / tcc/ commit / 3bcf802

api: introduce routes and error handling middleware

Pedro Lucas Porcellis porcellis@eletrotupi.com · 7 months ago 3bcf802a5ee2d0a9efe349a6250e92f62e9f27ee Browse files
Parents a6b6444
Changed files
5
Additions
+54
Deletions
-728
/dev/null → api/src/index.ts
1 +
import express from 'express';
2 +
import userRouter from '@app/routes/users'
3 +
import { errorHandler } from '@app/middleware/errorHandler';
4 +

5 +
const port = process.env.PORT || 3000;
6 +

7 +
const app = express();
8 +
app.use(express.json())
9 +

10 +
app.use("/users", userRouter);
11 +

12 +
app.use(errorHandler); // always last
13 +

14 +
app.listen(port, () => {
15 +
  console.log(`Server booted on port ${port}`)
16 +
})
17 +

/dev/null → api/src/middleware/errorHandler.ts
1 +
import { Request, Response, NextFunction } from 'express';
2 +
import { Prisma } from '@prisma/client' // Bleh
3 +
import { isDomainError } from "@app/lib/errors/base";
4 +

5 +
export const errorHandler = (
6 +
  err: unknown,
7 +
  _req: Request,
8 +
  res: Response,
9 +
  _next: NextFunction
10 +
) => {
11 +
  // TODO: Rig some improved, centralized error logging here
12 +
  console.error(`[ErrorHandler]: ${err}`);
13 +

14 +
  if (isDomainError(err)) {
15 +
    return res.status(err.status)
16 +
      .json({ error: err.message });
17 +
  }
18 +

19 +
  // Prisma errors as a fallback (or map these to domain errors in the repo)
20 +
  if (err instanceof Prisma.PrismaClientKnownRequestError) {
21 +
    if (err.code === "P2025") return res.status(404)
22 +
      .json({ error: "Record not found" });
23 +

24 +
    if (err.code === "P2002") return res.status(409)
25 +
      .json({ error: "Unique constraint violation" });
26 +
  }
27 +
  
28 +
  res.status(500).json({ error: "Internal server error" });
29 +
}
api/src/routes/mood.js → /dev/null
1 -
import express from "express";
2 -

3 -
import {
4 -
  validateMoodComponents,
5 -
  isValidLevel,
6 -
  isValidRating,
7 -
  calculateEmotionStats
8 -
} from "../utils/emotions.js";
9 -

10 -
import {
11 -
  validateRequiredFields,
12 -
  isValidDate,
13 -
  isNotFutureDate,
14 -
  isValidDateRange,
15 -
  sanitizeString,
16 -
  validatePagination
17 -
} from "../utils/validators.js";
18 -

19 -
const router = express.Router();
20 -

21 -
router.get('/', (req, res) => {
22 -
  
23 -
});
24 -

25 -
// Create new mood entry with components
26 -
router.post("/", (req, res) => {
27 -
  const {
28 -
    user_id,
29 -
    rating,
30 -
    stress_level,
31 -
    anxiety_level,
32 -
    energy_level,
33 -
    title,
34 -
    description,
35 -
    recorded_at,
36 -
    mood_components
37 -
  } = req.body;
38 -

39 -
  // Validate required fields
40 -
  const requiredValidation = validateRequiredFields(req.body, [
41 -
    'user_id', 'stress_level', 'anxiety_level', 'energy_level', 'recorded_at'
42 -
  ]);
43 -

44 -
  if (!requiredValidation.valid) {
45 -
    return res.status(400).json({
46 -
      error: "Campos faltantes",
47 -
      missing: requiredValidation.missing
48 -
    });
49 -
  }
50 -

51 -
  // Validate levels
52 -
  if (!isValidLevel(stress_level)) {
53 -
    return res.status(400).json({ error: "stress_level deve ser entre 1 e 10" });
54 -
  }
55 -
  if (!isValidLevel(anxiety_level)) {
56 -
    return res.status(400).json({ error: "anxiety_level deve ser entre 1 e 10" });
57 -
  }
58 -
  if (!isValidLevel(energy_level)) {
59 -
    return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
60 -
  }
61 -

62 -
  // Validate optional rating
63 -
  if (rating && !isValidRating(rating)) {
64 -
    return res.status(400).json({ error: "rating deve ser entre 1 e 10" });
65 -
  }
66 -

67 -
  // Validate date
68 -
  if (!isValidDate(recorded_at)) {
69 -
    return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
70 -
  }
71 -

72 -
  if (!isNotFutureDate(recorded_at)) {
73 -
    return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
74 -
  }
75 -

76 -
  // Validate mood components if provided
77 -
  if (mood_components) {
78 -
    const componentValidation = validateMoodComponents(mood_components);
79 -
    if (!componentValidation.valid) {
80 -
      return res.status(400).json({ error: componentValidation.error });
81 -
    }
82 -
  }
83 -

84 -
  const db = req.db;
85 -

86 -
  try {
87 -
    // Start transaction
88 -
    db.prepare("BEGIN").run();
89 -

90 -
    // Insert mood entry
91 -
    const moodStmt = db.prepare(`
92 -
      INSERT INTO mood (
93 -
        user_id, rating, stress_level, anxiety_level, energy_level,
94 -
        title, description, recorded_at
95 -
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
96 -
    `);
97 -

98 -
    const moodResult = moodStmt.run(
99 -
      user_id,
100 -
      rating || null,
101 -
      stress_level,
102 -
      anxiety_level,
103 -
      energy_level,
104 -
      sanitizeString(title) || null,
105 -
      sanitizeString(description) || null,
106 -
      recorded_at
107 -
    );
108 -

109 -
    const moodId = moodResult.lastInsertRowid;
110 -

111 -
    // Insert mood components if provided
112 -
    if (mood_components && mood_components.length > 0) {
113 -
      const componentStmt = db.prepare(`
114 -
        INSERT INTO mood_components (mood_id, emotion, intensity)
115 -
        VALUES (?, ?, ?)
116 -
      `);
117 -

118 -
      for (const component of mood_components) {
119 -
        componentStmt.run(
120 -
          moodId,
121 -
          component.emotion.toLowerCase(),
122 -
          component.intensity
123 -
        );
124 -
      }
125 -
    }
126 -

127 -
    // Commit transaction
128 -
    db.prepare("COMMIT").run();
129 -

130 -
    res.status(201).json({ id: moodId });
131 -
  } catch (err) {
132 -
    // Rollback on error
133 -
    db.prepare("ROLLBACK").run();
134 -
    console.error('Error creating mood:', err);
135 -
    res.status(500).json({ error: "Erro ao criar registro de humor" });
136 -
  }
137 -
});
138 -

139 -
// Get single mood entry with components
140 -
router.get("/:mood_id", (req, res) => {
141 -
  const moodId = req.params.mood_id;
142 -
  const db = req.db;
143 -

144 -
  const moodStmt = db.prepare(`
145 -
    SELECT m.*, u.first_name, u.last_name
146 -
    FROM mood m
147 -
    JOIN users u ON m.user_id = u.id
148 -
    WHERE m.id = ?
149 -
  `);
150 -

151 -
  const mood = moodStmt.get(moodId);
152 -

153 -
  if (!mood) {
154 -
    return res.status(404).json({ error: "Humor não encontrado" });
155 -
  }
156 -

157 -
  // Get mood components
158 -
  const componentsStmt = db.prepare(`
159 -
    SELECT emotion, intensity
160 -
    FROM mood_components
161 -
    WHERE mood_id = ?
162 -
  `);
163 -

164 -
  const components = componentsStmt.all(moodId);
165 -
  mood.mood_components = components;
166 -

167 -
  // Calculate emotion stats
168 -
  if (components.length > 0) {
169 -
    mood.emotion_stats = calculateEmotionStats(components);
170 -
  }
171 -

172 -
  res.json(mood);
173 -
});
174 -

175 -
// Update mood entry
176 -
router.put("/:mood_id", (req, res) => {
177 -
  const moodId = req.params.mood_id;
178 -
  const {
179 -
    rating,
180 -
    stress_level,
181 -
    anxiety_level,
182 -
    energy_level,
183 -
    title,
184 -
    description,
185 -
    recorded_at,
186 -
    mood_components
187 -
  } = req.body;
188 -

189 -
  const db = req.db;
190 -

191 -
  // Check if mood exists
192 -
  const existingMood = db.prepare("SELECT id FROM mood WHERE id = ?").get(moodId);
193 -
  if (!existingMood) {
194 -
    return res.status(404).json({ error: "Humor não encontrado" });
195 -
  }
196 -

197 -
  // Build update query dynamically
198 -
  const updates = [];
199 -
  const values = [];
200 -

201 -
  if (rating !== undefined) {
202 -
    if (!isValidRating(rating)) {
203 -
      return res.status(400).json({ error: "rating deve ser entre 1 e 10" });
204 -
    }
205 -
    updates.push("rating = ?");
206 -
    values.push(rating);
207 -
  }
208 -

209 -
  if (stress_level !== undefined) {
210 -
    if (!isValidLevel(stress_level)) {
211 -
      return res.status(400).json({ error: "stress_level deve ser entre 1 e 10" });
212 -
    }
213 -
    updates.push("stress_level = ?");
214 -
    values.push(stress_level);
215 -
  }
216 -

217 -
  if (anxiety_level !== undefined) {
218 -
    if (!isValidLevel(anxiety_level)) {
219 -
      return res.status(400).json({ error: "anxiety_level deve ser entre 1 e 10" });
220 -
    }
221 -
    updates.push("anxiety_level = ?");
222 -
    values.push(anxiety_level);
223 -
  }
224 -

225 -
  if (energy_level !== undefined) {
226 -
    if (!isValidLevel(energy_level)) {
227 -
      return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
228 -
    }
229 -
    updates.push("energy_level = ?");
230 -
    values.push(energy_level);
231 -
  }
232 -

233 -
  if (title !== undefined) {
234 -
    updates.push("title = ?");
235 -
    values.push(sanitizeString(title) || null);
236 -
  }
237 -

238 -
  if (description !== undefined) {
239 -
    updates.push("description = ?");
240 -
    values.push(sanitizeString(description) || null);
241 -
  }
242 -

243 -
  if (recorded_at !== undefined) {
244 -
    if (!isValidDate(recorded_at)) {
245 -
      return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
246 -
    }
247 -
    if (!isNotFutureDate(recorded_at)) {
248 -
      return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
249 -
    }
250 -
    updates.push("recorded_at = ?");
251 -
    values.push(recorded_at);
252 -
  }
253 -

254 -
  updates.push("updated_at = CURRENT_TIMESTAMP");
255 -

256 -
  try {
257 -
    db.prepare("BEGIN").run();
258 -

259 -
    // Update mood if there are changes
260 -
    if (updates.length > 0) {
261 -
      const updateQuery = `UPDATE mood SET ${updates.join(', ')} WHERE id = ?`;
262 -
      values.push(moodId);
263 -
      db.prepare(updateQuery).run(...values);
264 -
    }
265 -

266 -
    // Update mood components if provided
267 -
    if (mood_components !== undefined) {
268 -
      const componentValidation = validateMoodComponents(mood_components);
269 -
      if (!componentValidation.valid) {
270 -
        db.prepare("ROLLBACK").run();
271 -
        return res.status(400).json({ error: componentValidation.error });
272 -
      }
273 -

274 -
      // Delete existing components
275 -
      db.prepare("DELETE FROM mood_components WHERE mood_id = ?").run(moodId);
276 -

277 -
      // Insert new components
278 -
      if (mood_components.length > 0) {
279 -
        const componentStmt = db.prepare(`
280 -
          INSERT INTO mood_components (mood_id, emotion, intensity)
281 -
          VALUES (?, ?, ?)
282 -
        `);
283 -

284 -
        for (const component of mood_components) {
285 -
          componentStmt.run(
286 -
            moodId,
287 -
            component.emotion.toLowerCase(),
288 -
            component.intensity
289 -
          );
290 -
        }
291 -
      }
292 -
    }
293 -

294 -
    db.prepare("COMMIT").run();
295 -
    res.json({ success: true, id: moodId });
296 -
  } catch (err) {
297 -
    db.prepare("ROLLBACK").run();
298 -
    console.error('Error updating mood:', err);
299 -
    res.status(500).json({ error: "Erro ao atualizar humor" });
300 -
  }
301 -
});
302 -

303 -
// Delete mood entry
304 -
router.delete("/:mood_id", (req, res) => {
305 -
  const moodId = req.params.mood_id;
306 -
  const db = req.db;
307 -

308 -
  const result = db.prepare("DELETE FROM mood WHERE id = ?").run(moodId);
309 -

310 -
  if (result.changes === 0) {
311 -
    return res.status(404).json({ error: "Humor não encontrado" });
312 -
  }
313 -

314 -
  res.json({ success: true, deleted: moodId });
315 -
});
316 -

317 -
// Get user's moods with optional date range
318 -
router.get("/user/:user_id", (req, res) => {
319 -
  const userId = req.params.user_id;
320 -
  const { start_date, end_date, page, limit } = req.query;
321 -
  const db = req.db;
322 -

323 -
  // Validate pagination
324 -
  const { page: validPage, limit: validLimit, offset } = validatePagination(page, limit);
325 -

326 -
  let query = `
327 -
    SELECT m.*,
328 -
           COUNT(mc.id) as component_count
329 -
    FROM mood m
330 -
    LEFT JOIN mood_components mc ON m.id = mc.mood_id
331 -
    WHERE m.user_id = ?
332 -
  `;
333 -
  const params = [userId];
334 -

335 -
  // Add date range filtering
336 -
  if (start_date && end_date) {
337 -
    if (!isValidDateRange(start_date, end_date)) {
338 -
      return res.status(400).json({ error: "Intervalo de datas inválido" });
339 -
    }
340 -
    query += " AND m.recorded_at BETWEEN ? AND ?";
341 -
    params.push(start_date, end_date);
342 -
  } else if (start_date) {
343 -
    if (!isValidDate(start_date)) {
344 -
      return res.status(400).json({ error: "Data de início inválida" });
345 -
    }
346 -
    query += " AND m.recorded_at >= ?";
347 -
    params.push(start_date);
348 -
  } else if (end_date) {
349 -
    if (!isValidDate(end_date)) {
350 -
      return res.status(400).json({ error: "Data de fim inválida" });
351 -
    }
352 -
    query += " AND m.recorded_at <= ?";
353 -
    params.push(end_date);
354 -
  }
355 -

356 -
  query += " GROUP BY m.id ORDER BY m.recorded_at DESC LIMIT ? OFFSET ?";
357 -
  params.push(validLimit, offset);
358 -

359 -
  const moods = db.prepare(query).all(...params);
360 -

361 -
  // Get components for each mood
362 -
  const moodIds = moods.map(m => m.id);
363 -
  if (moodIds.length > 0) {
364 -
    const componentsQuery = `
365 -
      SELECT mood_id, emotion, intensity
366 -
      FROM mood_components
367 -
      WHERE mood_id IN (${moodIds.map(() => '?').join(',')})
368 -
    `;
369 -

370 -
    const components = db.prepare(componentsQuery).all(...moodIds);
371 -

372 -
    // Group components by mood_id
373 -
    const componentsByMood = {};
374 -
    components.forEach(c => {
375 -
      if (!componentsByMood[c.mood_id]) {
376 -
        componentsByMood[c.mood_id] = [];
377 -
      }
378 -
      componentsByMood[c.mood_id].push({
379 -
        emotion: c.emotion,
380 -
        intensity: c.intensity
381 -
      });
382 -
    });
383 -

384 -
    // Attach components to moods
385 -
    moods.forEach(mood => {
386 -
      mood.mood_components = componentsByMood[mood.id] || [];
387 -
      if (mood.mood_components.length > 0) {
388 -
        mood.emotion_stats = calculateEmotionStats(mood.mood_components);
389 -
      }
390 -
    });
391 -
  }
392 -

393 -
  // Get total count for pagination
394 -
  let countQuery = "SELECT COUNT(*) as total FROM mood WHERE user_id = ?";
395 -
  const countParams = [userId];
396 -

397 -
  if (start_date && end_date) {
398 -
    countQuery += " AND recorded_at BETWEEN ? AND ?";
399 -
    countParams.push(start_date, end_date);
400 -
  } else if (start_date) {
401 -
    countQuery += " AND recorded_at >= ?";
402 -
    countParams.push(start_date);
403 -
  } else if (end_date) {
404 -
    countQuery += " AND recorded_at <= ?";
405 -
    countParams.push(end_date);
406 -
  }
407 -

408 -
  const { total } = db.prepare(countQuery).get(...countParams);
409 -

410 -
  res.json({
411 -
    moods,
412 -
    pagination: {
413 -
      page: validPage,
414 -
      limit: validLimit,
415 -
      total,
416 -
      total_pages: Math.ceil(total / validLimit)
417 -
    }
418 -
  });
419 -
});
420 -

421 -
// Get mood statistics for a user
422 -
router.get("/user/:user_id/stats", (req, res) => {
423 -
  const userId = req.params.user_id;
424 -
  const { start_date, end_date } = req.query;
425 -
  const db = req.db;
426 -

427 -
  let baseQuery = "FROM mood WHERE user_id = ?";
428 -
  const params = [userId];
429 -

430 -
  if (start_date && end_date) {
431 -
    if (!isValidDateRange(start_date, end_date)) {
432 -
      return res.status(400).json({ error: "Intervalo de datas inválido" });
433 -
    }
434 -
    baseQuery += " AND recorded_at BETWEEN ? AND ?";
435 -
    params.push(start_date, end_date);
436 -
  }
437 -

438 -
  // Get aggregated stats
439 -
  const statsQuery = `
440 -
    SELECT
441 -
      COUNT(*) as total_entries,
442 -
      AVG(rating) as avg_rating,
443 -
      AVG(stress_level) as avg_stress,
444 -
      AVG(anxiety_level) as avg_anxiety,
445 -
      AVG(energy_level) as avg_energy,
446 -
      MIN(stress_level) as min_stress,
447 -
      MAX(stress_level) as max_stress,
448 -
      MIN(anxiety_level) as min_anxiety,
449 -
      MAX(anxiety_level) as max_anxiety,
450 -
      MIN(energy_level) as min_energy,
451 -
      MAX(energy_level) as max_energy
452 -
    ${baseQuery}
453 -
  `;
454 -

455 -
  const stats = db.prepare(statsQuery).get(...params);
456 -

457 -
  // Get emotion frequency
458 -
  const emotionQuery = `
459 -
    SELECT
460 -
      mc.emotion,
461 -
      COUNT(*) as frequency,
462 -
      AVG(mc.intensity) as avg_intensity
463 -
    FROM mood_components mc
464 -
    JOIN mood m ON mc.mood_id = m.id
465 -
    WHERE m.user_id = ?
466 -
    ${start_date && end_date ? 'AND m.recorded_at BETWEEN ? AND ?' : ''}
467 -
    GROUP BY mc.emotion
468 -
    ORDER BY frequency DESC
469 -
  `;
470 -

471 -
  const emotionParams = [...params];
472 -
  const emotions = db.prepare(emotionQuery).all(...emotionParams);
473 -

474 -
  // Get mood trends by day of week
475 -
  const dayTrendsQuery = `
476 -
    SELECT
477 -
      CASE CAST(strftime('%w', recorded_at) AS INTEGER)
478 -
        WHEN 0 THEN 'Sunday'
479 -
        WHEN 1 THEN 'Monday'
480 -
        WHEN 2 THEN 'Tuesday'
481 -
        WHEN 3 THEN 'Wednesday'
482 -
        WHEN 4 THEN 'Thursday'
483 -
        WHEN 5 THEN 'Friday'
484 -
        WHEN 6 THEN 'Saturday'
485 -
      END as day_of_week,
486 -
      AVG(stress_level) as avg_stress,
487 -
      AVG(anxiety_level) as avg_anxiety,
488 -
      AVG(energy_level) as avg_energy,
489 -
      COUNT(*) as entries
490 -
    ${baseQuery}
491 -
    GROUP BY strftime('%w', recorded_at)
492 -
  `;
493 -

494 -
  const dayTrends = db.prepare(dayTrendsQuery).all(...params);
495 -

496 -
  res.json({
497 -
    overall: stats,
498 -
    emotions: emotions,
499 -
    trends: {
500 -
      by_day_of_week: dayTrends
501 -
    },
502 -
    date_range: {
503 -
      start: start_date || 'all time',
504 -
      end: end_date || 'all time'
505 -
    }
506 -
  });
507 -
});
508 -

509 -
export default router;
api/src/routes/users.js → /dev/null
1 -
import express from "express";
2 -
import bcrypt from "bcryptjs";
3 -

4 -
import {
5 -
  isValidEmail,
6 -
  isValidPassword,
7 -
  sanitizeString,
8 -
  validateRequiredFields
9 -
} from "../utils/validators.js";
10 -

11 -
const router = express.Router();
12 -

13 -
// Create user
14 -
router.post("/", (req, res) => {
15 -
  const { email, password, first_name, last_name, timezone } = req.body;
16 -

17 -
  // Validate required fields
18 -
  const requiredValidation = validateRequiredFields(req.body, [
19 -
    'email', 'password', 'first_name', 'last_name'
20 -
  ]);
21 -

22 -
  if (!requiredValidation.valid) {
23 -
    return res.status(400).json({
24 -
      error: "Campos faltantes",
25 -
      missing: requiredValidation.missing
26 -
    });
27 -
  }
28 -

29 -
  // Validate email format
30 -
  if (!isValidEmail(email)) {
31 -
    return res.status(400).json({ error: "Email inválido" });
32 -
  }
33 -

34 -
  // Validate password strength
35 -
  if (!isValidPassword(password)) {
36 -
    return res.status(400).json({ error: "Senha deve ter pelo menos 6 caracteres" });
37 -
  }
38 -

39 -
  const hashed = bcrypt.hashSync(password, 10);
40 -
  const db = req.db;
41 -

42 -
  try {
43 -
    const stmt = db.prepare(`
44 -
      INSERT INTO users (email, password, first_name, last_name, timezone)
45 -
      VALUES (?, ?, ?, ?, ?)
46 -
    `);
47 -

48 -
    const result = stmt.run(
49 -
      email.toLowerCase(),
50 -
      hashed,
51 -
      sanitizeString(first_name),
52 -
      sanitizeString(last_name),
53 -
      timezone || 'UTC'
54 -
    );
55 -

56 -
    res.status(201).json({ id: result.lastInsertRowid });
57 -
  } catch (err) {
58 -
    if (err.message.includes('UNIQUE')) {
59 -
      res.status(400).json({ error: "Email já cadastrado" });
60 -
    } else {
61 -
      console.error('Error creating user:', err);
62 -
      res.status(500).json({ error: "Erro ao criar usuário" });
63 -
    }
64 -
  }
65 -
});
66 -

67 -
// User login
68 -
router.post("/login", (req, res) => {
69 -
  const { email, password } = req.body;
70 -

71 -
  // Validate required fields
72 -
  if (!email || !password) {
73 -
    return res.status(400).json({ error: "Email e senha são obrigatórios" });
74 -
  }
75 -

76 -
  const db = req.db;
77 -

78 -
  const user = db.prepare(`
79 -
    SELECT id, email, password, first_name, last_name, timezone
80 -
    FROM users
81 -
    WHERE email = ?
82 -
  `).get(email.toLowerCase());
83 -

84 -
  if (!user) {
85 -
    return res.status(401).json({ error: "Credenciais inválidas" });
86 -
  }
87 -

88 -
  const passwordMatch = bcrypt.compareSync(password, user.password);
89 -

90 -
  if (!passwordMatch) {
91 -
    return res.status(401).json({ error: "Credenciais inválidas" });
92 -
  }
93 -

94 -
  // Don't send password in response
95 -
  delete user.password;
96 -

97 -
  res.json({
98 -
    success: true,
99 -
    user
100 -
  });
101 -
});
102 -

103 -
// Get user profile
104 -
router.get("/:user_id", (req, res) => {
105 -
  const userId = req.params.user_id;
106 -
  const db = req.db;
107 -

108 -
  const user = db.prepare(`
109 -
    SELECT id, email, first_name, last_name, timezone, created_at, updated_at
110 -
    FROM users
111 -
    WHERE id = ?
112 -
  `).get(userId);
113 -

114 -
  if (!user) {
115 -
    return res.status(404).json({ error: "Usuário não encontrado" });
116 -
  }
117 -

118 -
  // Get user's mood count
119 -
  const moodStats = db.prepare(`
120 -
    SELECT COUNT(*) as total_moods
121 -
    FROM mood
122 -
    WHERE user_id = ?
123 -
  `).get(userId);
124 -

125 -
  user.total_moods = moodStats.total_moods;
126 -

127 -
  res.json(user);
128 -
});
129 -

130 -
// Update user
131 -
router.put("/:user_id", (req, res) => {
132 -
  const userId = req.params.user_id;
133 -
  const { email, password, first_name, last_name, timezone } = req.body;
134 -
  const db = req.db;
135 -

136 -
  // Check if user exists
137 -
  const existingUser = db.prepare("SELECT id FROM users WHERE id = ?").get(userId);
138 -
  if (!existingUser) {
139 -
    return res.status(404).json({ error: "Usuário não encontrado" });
140 -
  }
141 -

142 -
  const updates = [];
143 -
  const values = [];
144 -

145 -
  if (email !== undefined) {
146 -
    if (!isValidEmail(email)) {
147 -
      return res.status(400).json({ error: "Email inválido" });
148 -
    }
149 -

150 -
    // Check if email is already taken by another user
151 -
    const emailCheck = db.prepare("SELECT id FROM users WHERE email = ? AND id != ?")
152 -
      .get(email.toLowerCase(), userId);
153 -

154 -
    if (emailCheck) {
155 -
      return res.status(400).json({ error: "Email já está em uso" });
156 -
    }
157 -

158 -
    updates.push("email = ?");
159 -
    values.push(email.toLowerCase());
160 -
  }
161 -

162 -
  if (password !== undefined) {
163 -
    if (!isValidPassword(password)) {
164 -
      return res.status(400).json({ error: "Senha deve ter pelo menos 6 caracteres" });
165 -
    }
166 -
    updates.push("password = ?");
167 -
    values.push(bcrypt.hashSync(password, 10));
168 -
  }
169 -

170 -
  if (first_name !== undefined) {
171 -
    updates.push("first_name = ?");
172 -
    values.push(sanitizeString(first_name));
173 -
  }
174 -

175 -
  if (last_name !== undefined) {
176 -
    updates.push("last_name = ?");
177 -
    values.push(sanitizeString(last_name));
178 -
  }
179 -

180 -
  if (timezone !== undefined) {
181 -
    updates.push("timezone = ?");
182 -
    values.push(timezone);
183 -
  }
184 -

185 -
  if (updates.length === 0) {
186 -
    return res.status(400).json({ error: "Nenhum campo para atualizar" });
187 -
  }
188 -

189 -
  updates.push("updated_at = CURRENT_TIMESTAMP");
190 -

191 -
  try {
192 -
    const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE id = ?`;
193 -
    values.push(userId);
194 -

195 -
    db.prepare(updateQuery).run(...values);
196 -

197 -
    res.json({ success: true, id: userId });
198 -
  } catch (err) {
199 -
    console.error('Error updating user:', err);
200 -
    res.status(500).json({ error: "Erro ao atualizar usuário" });
201 -
  }
202 -
});
203 -

204 -
// Delete user
205 -
router.delete("/:user_id", (req, res) => {
206 -
  const userId = req.params.user_id;
207 -
  const db = req.db;
208 -

209 -
  // This will cascade delete all moods and mood_components
210 -
  const result = db.prepare("DELETE FROM users WHERE id = ?").run(userId);
211 -

212 -
  if (result.changes === 0) {
213 -
    return res.status(404).json({ error: "Usuário não encontrado" });
214 -
  }
215 -

216 -
  res.json({ success: true, deleted: userId });
217 -
});
218 -

219 -
export default router;
/dev/null → api/src/routes/users.ts
1 +
import { Router } from "express";
2 +
import { UsersController } from '@app/controllers/users'
3 +

4 +
const router = Router();
5 +

6 +
router.post('/', UsersController.create);
7 +

8 +
export default router;