Anime Collection + Login
It's the same MVC app from Week 11 โ but now it knows who you are. Browsing is open to everyone; adding and deleting require a login. Sessions + bcrypt, added in a handful of small files. Play it, then build the auth. ๐
Play the finished app
Guided tour: โ click a title to view it (public) ยท โก click + Add anime โ requireLogin redirects you to /login ยท โข log in with sensei / anime123 ยท โฃ now Add & Delete work, and the header shows ๐ค sensei ยท Log out ยท โค log out and watch the tools lock again. Watch the status line at the bottom.
What we added to the Week 11 app
Four new files and two small edits turn the open app into an authenticated one. New files are marked NEW:
The one new idea: a request to a protected route now passes through requireLogin first. Logged in? It calls next() and the controller runs. Not logged in? It res.redirect("/login") and the controller never runs.
Build the login, step by step
Start from your finished Week 11 app (or download the files above), then add these pieces.
Install the two new packages
npm install express-session bcryptjs
express-session stores the logged-in user on the server; bcryptjs hashes passwords so we never store them in plain text.
Turn on sessions โ edit app.js
Add the session middleware, then a tiny middleware that copies the logged-in user onto res.locals so every view can see it as user.
const session = require("express-session");
app.use(session({
secret: "change-me-in-production", // signs the session-id cookie
resave: false,
saveUninitialized: false // don't make a session until login
}));
// make the logged-in user available to every view as `user`
app.use((req, res, next) => {
res.locals.user = req.session.user || null;
next();
});
// mount the auth routes alongside the anime routes
app.use("/", require("./routes/auth"));
app.use("/", require("./routes/anime"));
The auth controller โ controllers/authController.js NEW
This is where hashing lives. We store a demo user whose password is hashed once at startup, then check logins with bcrypt.compareSync.
const bcrypt = require("bcryptjs");
// In a real app users live in a database (Week 12). We never store the
// raw password โ we hash it. demo โ sensei / anime123
const users = [
{ username: "sensei", passwordHash: bcrypt.hashSync("anime123", 10) }
];
// GET /login โ show the form
exports.showLogin = (req, res) => {
res.render("login", { title: "Log in", error: null });
};
// POST /login โ check credentials, start a session
exports.login = (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (user && bcrypt.compareSync(password, user.passwordHash)) {
req.session.user = { username: user.username }; // โ logged in!
return res.redirect("/");
}
res.status(401).render("login", { title: "Log in", error: "Wrong username or password." });
};
// POST /logout โ destroy the session
exports.logout = (req, res) => {
req.session.destroy(() => res.redirect("/"));
};
Why hashing? If your data ever leaks, the attacker sees a bcrypt hash โ not anime123. You never decrypt a hash; you compare the typed password against it.
The guard โ middleware/requireLogin.js NEW
// Only let logged-in users through. Otherwise send them to /login.
module.exports = function requireLogin(req, res, next) {
if (req.session.user) return next();
res.redirect("/login");
};
The routes โ routes/auth.js NEW & guard the anime routes
const express = require("express");
const router = express.Router();
const auth = require("../controllers/authController");
router.get("/login", auth.showLogin);
router.post("/login", auth.login);
router.post("/logout", auth.logout);
module.exports = router;const requireLogin = require("../middleware/requireLogin");
// public
router.get("/", c.index);
router.get("/anime/:id", c.details);
// protected โ requireLogin runs first
router.get("/add", requireLogin, c.addForm);
router.post("/add", requireLogin, c.create);
router.post("/anime/:id/delete", requireLogin, c.remove);Middleware you list before the controller runs first. router.post("/add", requireLogin, c.create) means: run requireLogin, and only if it calls next() does c.create run.
The login view โ views/login.pug NEW
extends layout
block content
h1 Log in
if error
p.error #{error}
form.form(action="/login" method="POST")
label(for="username") Username
input(type="text" name="username" required)
label(for="password") Password
input(type="password" name="password" required)
button.btn(type="submit") Log in
p.hint Demo โ username: sensei ยท password: anime123
Make the nav session-aware โ views/layout.pug
Show Log in to guests; show the username + a Log out button to logged-in users. The user variable comes from the res.locals.user line you added in app.js.
header.site-header
a.brand(href="/") ๐ฌ Anime Collection
.nav-actions
a.btn(href="/add") + Add anime
if user
span.user-badge ๐ค #{user.username}
form.logout-form(action="/logout" method="POST")
button.linkbtn(type="submit") Log out
else
a.loginlink(href="/login") Log in
And in views/details.pug, only show the Delete button to logged-in users:
if user
form.inline(action=`/anime/${anime.id}/delete` method="POST")
button.btn.danger(type="submit") Delete
else
p.hint ๐ Log in to delete anime.
Run it on your own machine
# 1. grab the files from the tree above (every filename is a download link)
# 2. install everything (express, pug, express-session, bcryptjs)
npm install
# 3. start the server
node app.js
# 4. open http://localhost:3000 โ log in with sensei / anime123 ๐
Grab the files: every filename in the tree above is a download link. Recreate the structure, run the commands, and you've got the exact app you just played with โ login and all.
Make it yours (challenges)
- Register โ add a /register route + form that bcrypt.hashSynces a new user into the list.
- Real users โ move the users array into MongoDB (Week 12) so accounts persist.
- Return-to โ after login, redirect back to the page the user originally wanted.
- Ownership โ store addedBy on each anime and only let its owner delete it.
- Tokens โ swap sessions for a JWT (see the Week 13 island) for an API version.