feat(week8): JWT auth, SSM Parameter Store, and UI overhaul

This commit is contained in:
ikechukwu Elizabeth
2026-05-30 07:06:21 +01:00
parent 0de2d1dd42
commit dcaef8ff13
49 changed files with 8868 additions and 723 deletions
+112
View File
@@ -0,0 +1,112 @@
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { pool } = require("../db");
const { requireAuth, JWT_SECRET } = require("../middleware/auth");
const router = express.Router();
// Helper - sign a JWT for a user row
function signToken(user) {
return jwt.sign(
{ id: user.id, email: user.email, name: user.name },
JWT_SECRET,
{ expiresIn: "7d" }
);
}
// POST /api/auth/register
router.post("/register", async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ error: "Name, email, and password are required" });
}
if (password.length < 8) {
return res.status(400).json({ error: "Password must be at least 8 characters" });
}
try {
// Check if email already exists
const existing = await pool.query(
"SELECT id FROM users WHERE email = $1",
[email.toLowerCase().trim()]
);
if (existing.rowCount > 0) {
return res.status(409).json({ error: "An account with that email already exists" });
}
// Hash password
const hashed = await bcrypt.hash(password, 12);
// Insert user
const result = await pool.query(
"INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING id, name, email, created_at",
[name.trim(), email.toLowerCase().trim(), hashed]
);
const user = result.rows[0];
const token = signToken(user);
res.status(201).json({ token, user: { id: user.id, name: user.name, email: user.email } });
} catch (err) {
console.error("POST /api/auth/register error:", err);
res.status(500).json({ error: "Registration failed" });
}
});
// POST /api/auth/login
router.post("/login", async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: "Email and password are required" });
}
try {
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email.toLowerCase().trim()]
);
if (result.rowCount === 0) {
return res.status(401).json({ error: "Invalid email or password" });
}
const user = result.rows[0];
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({ error: "Invalid email or password" });
}
const token = signToken(user);
res.json({ token, user: { id: user.id, name: user.name, email: user.email } });
} catch (err) {
console.error("POST /api/auth/login error:", err);
res.status(500).json({ error: "Login failed" });
}
});
// GET /api/auth/me - returns current user from token
router.get("/me", requireAuth, async (req, res) => {
try {
const result = await pool.query(
"SELECT id, name, email, created_at FROM users WHERE id = $1",
[req.user.id]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: "User not found" });
}
res.json(result.rows[0]);
} catch (err) {
console.error("GET /api/auth/me error:", err);
res.status(500).json({ error: "Failed to fetch user" });
}
});
module.exports = router;
+60
View File
@@ -0,0 +1,60 @@
const express = require('express')
const { pool } = require('../db')
const { requireAuth } = require('../middleware/auth')
const router = express.Router()
// All project routes require a valid JWT
router.use(requireAuth)
// GET /api/projects - return all projects for the logged-in user
router.get('/', async (req, res) => {
try {
const result = await pool.query(
'SELECT * FROM projects WHERE user_id = $1 ORDER BY created_at DESC',
[req.user.id]
)
res.json(result.rows)
} catch (err) {
console.error('GET /api/projects error:', err)
res.status(500).json({ error: 'Failed to fetch projects' })
}
})
// POST /api/projects - create a new project
router.post('/', async (req, res) => {
const { name, description } = req.body
if (!name) {
return res.status(400).json({ error: 'Project name is required' })
}
try {
const result = await pool.query(
'INSERT INTO projects (name, description, user_id) VALUES ($1, $2, $3) RETURNING *',
[name.trim(), description || '', req.user.id]
)
res.status(201).json(result.rows[0])
} catch (err) {
console.error('POST /api/projects error:', err)
res.status(500).json({ error: 'Failed to create project' })
}
})
// DELETE /api/projects/:id - delete a project (owner only)
router.delete('/:id', async (req, res) => {
const { id } = req.params
try {
const result = await pool.query(
'DELETE FROM projects WHERE id = $1 AND user_id = $2',
[id, req.user.id]
)
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Project not found' })
}
res.status(204).send()
} catch (err) {
console.error('DELETE /api/projects/:id error:', err)
res.status(500).json({ error: 'Failed to delete project' })
}
})
module.exports = router
+81
View File
@@ -0,0 +1,81 @@
const express = require("express");
const { pool } = require("../db");
const { requireAuth } = require("../middleware/auth");
const router = express.Router();
// All task routes require a valid JWT
router.use(requireAuth);
// GET /api/tasks - return tasks for the logged-in user, newest first
router.get("/", async (req, res) => {
try {
const result = await pool.query(
"SELECT * FROM tasks WHERE user_id = $1 ORDER BY created_at DESC",
[req.user.id]
);
res.json(result.rows);
} catch (err) {
console.error("GET /api/tasks error:", err);
res.status(500).json({ error: "Failed to fetch tasks" });
}
});
// POST /api/tasks - create a task for the logged-in user
router.post("/", async (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: "Title is required" });
}
try {
const result = await pool.query(
"INSERT INTO tasks (title, user_id) VALUES ($1, $2) RETURNING *",
[title, req.user.id]
);
res.status(201).json(result.rows[0]);
} catch (err) {
console.error("POST /api/tasks error:", err);
res.status(500).json({ error: "Failed to create task" });
}
});
// PATCH /api/tasks/:id - toggle completed (only owner can update)
router.patch("/:id", async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
`UPDATE tasks
SET completed = NOT completed
WHERE id = $1 AND user_id = $2
RETURNING *`,
[id, req.user.id]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: "Task not found" });
}
res.json(result.rows[0]);
} catch (err) {
console.error("PATCH /api/tasks/:id error:", err);
res.status(500).json({ error: "Failed to update task" });
}
});
// DELETE /api/tasks/:id - delete (only owner can delete)
router.delete("/:id", async (req, res) => {
const { id } = req.params;
try {
const result = await pool.query(
"DELETE FROM tasks WHERE id = $1 AND user_id = $2",
[id, req.user.id]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: "Task not found" });
}
res.status(204).send();
} catch (err) {
console.error("DELETE /api/tasks/:id error:", err);
res.status(500).json({ error: "Failed to delete task" });
}
});
module.exports = router;
+78
View File
@@ -0,0 +1,78 @@
const express = require('express')
const { pool } = require('../db')
const { requireAuth } = require('../middleware/auth')
const router = express.Router()
// All team routes require a valid JWT
router.use(requireAuth)
// GET /api/team - return all team members for the logged-in user's workspace
router.get('/', async (req, res) => {
try {
const result = await pool.query(
`SELECT tm.id, tm.email, tm.role, tm.created_at,
u.name
FROM team_members tm
LEFT JOIN users u ON u.email = tm.email
WHERE tm.owner_id = $1
ORDER BY tm.created_at ASC`,
[req.user.id]
)
res.json(result.rows)
} catch (err) {
console.error('GET /api/team error:', err)
res.status(500).json({ error: 'Failed to fetch team members' })
}
})
// POST /api/team/invite - invite a team member by email
router.post('/invite', async (req, res) => {
const { email, role } = req.body
if (!email) {
return res.status(400).json({ error: 'Email is required' })
}
const validRoles = ['admin', 'member', 'viewer']
const memberRole = validRoles.includes(role) ? role : 'member'
try {
// Check if already invited
const existing = await pool.query(
'SELECT id FROM team_members WHERE owner_id = $1 AND email = $2',
[req.user.id, email.toLowerCase().trim()]
)
if (existing.rowCount > 0) {
return res.status(409).json({ error: 'This person has already been invited' })
}
const result = await pool.query(
'INSERT INTO team_members (owner_id, email, role) VALUES ($1, $2, $3) RETURNING *',
[req.user.id, email.toLowerCase().trim(), memberRole]
)
res.status(201).json(result.rows[0])
} catch (err) {
console.error('POST /api/team/invite error:', err)
res.status(500).json({ error: 'Failed to send invitation' })
}
})
// DELETE /api/team/:id - remove a team member
router.delete('/:id', async (req, res) => {
const { id } = req.params
try {
const result = await pool.query(
'DELETE FROM team_members WHERE id = $1 AND owner_id = $2',
[id, req.user.id]
)
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Team member not found' })
}
res.status(204).send()
} catch (err) {
console.error('DELETE /api/team/:id error:', err)
res.status(500).json({ error: 'Failed to remove team member' })
}
})
module.exports = router