GUIDED PROJECT · MVC + AUTH

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. ๐Ÿ”‘

Adds express-session bcryptjs requireLogin guard login ยท logout

Play the finished app

Try it as a guest first โ€” click + Add anime and you'll be bounced to the login. Then log in and everything unlocks.
๐Ÿ”’ localhost:3000/

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

The list/add/delete logic is unchanged. We bolted on a session, a login/logout controller, and one guard.

Four new files and two small edits turn the open app into an authenticated one. New files are marked NEW:

anime-collection-auth/ โ”œโ”€โ”€ app.js # + session & res.locals.user (edited) โ”œโ”€โ”€ package.json # + express-session, bcryptjs โ”‚ โ”œโ”€โ”€ data/ โ”‚ โ””โ”€โ”€ anime.json โ† MODEL โ”‚ โ”œโ”€โ”€ middleware/ โ† NEW โ”‚ โ””โ”€โ”€ requireLogin.js โ† the route guard โ”‚ โ”œโ”€โ”€ controllers/ โ”‚ โ”œโ”€โ”€ animeController.js โ† CONTROLLER (same) โ”‚ โ””โ”€โ”€ authController.js โ† NEW ยท login / logout โ”‚ โ”œโ”€โ”€ routes/ โ”‚ โ”œโ”€โ”€ anime.js # + requireLogin on add/delete โ”‚ โ””โ”€โ”€ auth.js โ† NEW โ”‚ โ”œโ”€โ”€ views/ โ† VIEW (Pug) โ”‚ โ”œโ”€โ”€ layout.pug # session-aware nav (edited) โ”‚ โ”œโ”€โ”€ index.pug โ”‚ โ”œโ”€โ”€ add.pug โ”‚ โ”œโ”€โ”€ details.pug # delete only if logged in โ”‚ โ””โ”€โ”€ login.pug โ† NEW โ”‚ โ””โ”€โ”€ public/ โ””โ”€โ”€ css/ โ””โ”€โ”€ style.css

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

terminal
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.

app.js (additions)
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.

controllers/authController.js
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

middleware/requireLogin.js
// 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

routes/auth.js
auth.js
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;
routes/anime.js (guarded)
anime.js
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

views/login.pug
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.

views/layout.pug (header)
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:

views/details.pug (Delete)
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

terminal
# 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.