-
Notifications
You must be signed in to change notification settings - Fork 0
Add expense tracker app for CodeRabbit review demo #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import Database from "better-sqlite3"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const dbPath = path.join(__dirname, "..", "teamexpense.db"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const db = new Database(dbPath); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| db.pragma("journal_mode = WAL"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| db.exec(` | ||||||||||||||||||||||||||||||||||||||||||||||||
| CREATE TABLE IF NOT EXISTS users ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||||||||||||||||||||||||||||||||||||||||||||||
| email TEXT UNIQUE NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| password TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| name TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| role TEXT NOT NULL DEFAULT 'member' | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| CREATE TABLE IF NOT EXISTS expenses ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||||||||||||||||||||||||||||||||||||||||||||||
| user_id INTEGER NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| amount REAL NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| description TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| category TEXT NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||
| status TEXT NOT NULL DEFAULT 'pending', | ||||||||||||||||||||||||||||||||||||||||||||||||
| created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||||||||||||||||||||||||||||||||||||||||||||||||
| FOREIGN KEY (user_id) REFERENCES users(id) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding indexes for query performance. The 📈 Proposed schema additions for indexing FOREIGN KEY (user_id) REFERENCES users(id)
);
+
+ CREATE INDEX IF NOT EXISTS idx_expenses_user_id ON expenses(user_id);
+ CREATE INDEX IF NOT EXISTS idx_expenses_status ON expenses(status);
`);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| `); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default db; | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,30 @@ | ||||||||||||||||||
| import express from "express"; | ||||||||||||||||||
| import cors from "cors"; | ||||||||||||||||||
| import authRoutes from "./routes/auth"; | ||||||||||||||||||
| import expenseRoutes from "./routes/expenses"; | ||||||||||||||||||
| import reportRoutes from "./routes/reports"; | ||||||||||||||||||
|
|
||||||||||||||||||
| const app = express(); | ||||||||||||||||||
| const PORT = process.env.PORT || 3001; | ||||||||||||||||||
|
|
||||||||||||||||||
| // BUG: Wildcard CORS — allows any origin to make authenticated requests | ||||||||||||||||||
| app.use(cors()); | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restrict CORS to an explicit allowlist.
🤖 Prompt for AI Agents |
||||||||||||||||||
| app.use(express.json()); | ||||||||||||||||||
|
|
||||||||||||||||||
| app.use("/api/auth", authRoutes); | ||||||||||||||||||
| app.use("/api/expenses", expenseRoutes); | ||||||||||||||||||
| app.use("/api/reports", reportRoutes); | ||||||||||||||||||
|
|
||||||||||||||||||
| // BUG: No rate limiting on any endpoints — susceptible to brute-force attacks | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add request rate limiting on auth and data endpoints. No throttling leaves login and API routes open to brute-force and scraping abuse. 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // BUG: Global error handler leaks stack traces to the client in production | ||||||||||||||||||
| app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => { | ||||||||||||||||||
| console.error(err); | ||||||||||||||||||
| res.status(500).json({ error: err.message, stack: err.stack }); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+21
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not return stack traces in production responses. Returning Proposed fix app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
- res.status(500).json({ error: err.message, stack: err.stack });
+ res.status(500).json({ error: "Internal server error" });
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| app.listen(PORT, () => { | ||||||||||||||||||
| console.log(`Server running on port ${PORT}`); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| export default app; | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||
| import { Request, Response, NextFunction } from "express"; | ||||||||||||||||
| import jwt from "jsonwebtoken"; | ||||||||||||||||
|
|
||||||||||||||||
| const JWT_SECRET = "supersecretkey123"; // BUG: Hardcoded JWT secret — should use environment variable | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove hardcoded JWT secret from source code. Embedding the secret in code is a critical security exposure and makes token forgery materially easier if code is leaked. Proposed fix-const JWT_SECRET = "supersecretkey123";
+const JWT_SECRET = process.env.JWT_SECRET;
+if (!JWT_SECRET) {
+ throw new Error("JWT_SECRET is not configured");
+}🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| export interface AuthRequest extends Request { | ||||||||||||||||
| user?: { id: number; email: string; role: string }; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export function authenticate(req: AuthRequest, res: Response, next: NextFunction) { | ||||||||||||||||
| const header = req.headers.authorization; | ||||||||||||||||
| if (!header) { | ||||||||||||||||
| return res.status(401).json({ error: "No token provided" }); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // BUG: Does not validate "Bearer " prefix — any string with a valid JWT payload passes | ||||||||||||||||
| const token = header.replace("Bearer ", ""); | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+16
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Authorization header format strictly. Using Proposed fix- const token = header.replace("Bearer ", "");
+ const [scheme, token] = header.split(" ");
+ if (scheme !== "Bearer" || !token) {
+ return res.status(401).json({ error: "Invalid authorization header format" });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| try { | ||||||||||||||||
| const payload = jwt.verify(token, JWT_SECRET) as any; | ||||||||||||||||
| req.user = payload; | ||||||||||||||||
| next(); | ||||||||||||||||
| } catch { | ||||||||||||||||
| return res.status(401).json({ error: "Invalid token" }); | ||||||||||||||||
| } | ||||||||||||||||
|
Comment on lines
+10
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, let's explore the directory structure to locate auth/token issuance code
echo "=== Locating auth-related files in backend/src/routes ==="
fd auth backend/src/routes -t f 2>/dev/null || echo "No auth files found in backend/src/routes"
echo -e "\n=== Searching for jwt.sign calls across backend/src/routes ==="
rg -nP 'jwt\.sign\s*\(' backend/src/routes -t ts -t js -A5 2>/dev/null || echo "No jwt.sign found"
echo -e "\n=== Listing all files in backend/src/routes ==="
fd . backend/src/routes -t f 2>/dev/null | head -20
echo -e "\n=== Searching for jwt.sign across entire backend/src ==="
rg -nP 'jwt\.sign\s*\(' backend/src -t ts -t js -A5 2>/dev/null || echo "No jwt.sign found"
echo -e "\n=== Searching for expiresIn or exp settings ==="
rg -nP 'expiresIn|exp\s*:' backend/src -t ts -t js -B2 -A2 2>/dev/null || echo "No expiry settings found"Repository: reeder32/teamexpense Length of output: 1849 Add Tokens issued in 🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export function requireAdmin(req: AuthRequest, res: Response, next: NextFunction) { | ||||||||||||||||
| // BUG: Checks role from the JWT payload which the user controls — should verify role from DB | ||||||||||||||||
| if (req.user?.role !== "admin") { | ||||||||||||||||
| return res.status(403).json({ error: "Admin access required" }); | ||||||||||||||||
| } | ||||||||||||||||
| next(); | ||||||||||||||||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not authorize admin access from token role claim alone.
As per coding guidelines, 🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| export { JWT_SECRET }; | ||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||||||||||||||||||||||||||||||||
| import { Router, Request, Response } from "express"; | ||||||||||||||||||||||||||||||||||||
| import bcrypt from "bcryptjs"; | ||||||||||||||||||||||||||||||||||||
| import jwt from "jsonwebtoken"; | ||||||||||||||||||||||||||||||||||||
| import db from "../db"; | ||||||||||||||||||||||||||||||||||||
| import { JWT_SECRET } from "../middleware/auth"; | ||||||||||||||||||||||||||||||||||||
| import { isValidEmail } from "../utils/validate"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const router = Router(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // POST /api/auth/register | ||||||||||||||||||||||||||||||||||||
| router.post("/register", async (req: Request, res: Response) => { | ||||||||||||||||||||||||||||||||||||
| const { email, password, name } = req.body; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!email || !password || !name) { | ||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: "All fields are required" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!isValidEmail(email)) { | ||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: "Invalid email" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // BUG: No minimum password length or complexity check | ||||||||||||||||||||||||||||||||||||
| const hashed = await bcrypt.hash(password, 10); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add password strength validation. Passwords have no minimum length or complexity requirements. Weak passwords like "1" are accepted, making accounts vulnerable to brute-force attacks. 🔧 Proposed fix with password validation+ if (password.length < 8) {
+ return res.status(400).json({ error: "Password must be at least 8 characters" });
+ }
+
const hashed = await bcrypt.hash(password, 10);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||
| const stmt = db.prepare("INSERT INTO users (email, password, name) VALUES (?, ?, ?)"); | ||||||||||||||||||||||||||||||||||||
| const result = stmt.run(email, hashed, name); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const token = jwt.sign( | ||||||||||||||||||||||||||||||||||||
| { id: result.lastInsertRowid, email, role: "member" }, | ||||||||||||||||||||||||||||||||||||
| JWT_SECRET | ||||||||||||||||||||||||||||||||||||
| // BUG: No token expiration — JWTs are valid forever | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JWTs never expire, creating a security risk. Tokens without expiration remain valid forever. If a token is compromised, an attacker has permanent access. Additionally, users cannot be effectively logged out. 🔧 Proposed fix adding token expiration const token = jwt.sign(
{ id: result.lastInsertRowid, email, role: "member" },
- JWT_SECRET
- // BUG: No token expiration — JWTs are valid forever
+ JWT_SECRET,
+ { expiresIn: "24h" }
);Apply the same fix to the login route: const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
- JWT_SECRET
- // BUG: No token expiration here either
+ JWT_SECRET,
+ { expiresIn: "24h" }
);Also applies to: 65-69 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return res.status(201).json({ token, user: { id: result.lastInsertRowid, email, name, role: "member" } }); | ||||||||||||||||||||||||||||||||||||
| } catch (err: any) { | ||||||||||||||||||||||||||||||||||||
| if (err.code === "SQLITE_CONSTRAINT_UNIQUE") { | ||||||||||||||||||||||||||||||||||||
| return res.status(409).json({ error: "Email already registered" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| return res.status(500).json({ error: "Registration failed" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // POST /api/auth/login | ||||||||||||||||||||||||||||||||||||
| router.post("/login", async (req: Request, res: Response) => { | ||||||||||||||||||||||||||||||||||||
| const { email, password } = req.body; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!email || !password) { | ||||||||||||||||||||||||||||||||||||
| return res.status(400).json({ error: "Email and password required" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // BUG: SQL injection — user input interpolated directly into query | ||||||||||||||||||||||||||||||||||||
| const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: SQL injection vulnerability. User input is directly interpolated into the SQL query. An attacker can bypass authentication or extract data by submitting a malicious email like: This allows complete database compromise. 🚨 Required fix using parameterized query- // BUG: SQL injection — user input interpolated directly into query
- const user = db.prepare(`SELECT * FROM users WHERE email = '${email}'`).get() as any;
+ const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email) as any;As per coding guidelines: "API routes: Flag any raw SQL queries that don't use parameterized statements." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||||||||||||
| // BUG: Leaks whether an email exists — allows user enumeration | ||||||||||||||||||||||||||||||||||||
| return res.status(401).json({ error: "No account found with that email" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const valid = await bcrypt.compare(password, user.password); | ||||||||||||||||||||||||||||||||||||
| if (!valid) { | ||||||||||||||||||||||||||||||||||||
| return res.status(401).json({ error: "Incorrect password" }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. User enumeration vulnerability via distinct error messages. Different error messages for "email not found" vs "incorrect password" allow attackers to enumerate valid email addresses. This information aids targeted attacks. 🔧 Proposed fix with generic error message if (!user) {
- // BUG: Leaks whether an email exists — allows user enumeration
- return res.status(401).json({ error: "No account found with that email" });
+ return res.status(401).json({ error: "Invalid email or password" });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
- return res.status(401).json({ error: "Incorrect password" });
+ return res.status(401).json({ error: "Invalid email or password" });
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const token = jwt.sign( | ||||||||||||||||||||||||||||||||||||
| { id: user.id, email: user.email, role: user.role }, | ||||||||||||||||||||||||||||||||||||
| JWT_SECRET | ||||||||||||||||||||||||||||||||||||
| // BUG: No token expiration here either | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role } }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export default router; | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enable foreign key enforcement in SQLite.
SQLite does not enforce foreign key constraints by default. The
FOREIGN KEYclause on line 26 will be silently ignored without enabling enforcement, allowing orphaned expense records if users are deleted.🔧 Proposed fix to enable foreign key enforcement
db.pragma("journal_mode = WAL"); +db.pragma("foreign_keys = ON");📝 Committable suggestion
🤖 Prompt for AI Agents