feat(week8): JWT auth, SSM Parameter Store, and UI overhaul
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user