Skip to content

🤓 Refactoring

Melvin Idema edited this page Mar 10, 2022 · 2 revisions

De refactoring opdracht komt eigenlijk heel gelegen. Mijn applicatie is zodanig gegroeid dat m'n serve.js bestand 135 lijnen betreft. En dat vond ik onoverzichtelijk worden. Ik had al geprobeerd wat logica te abstraheren. Zoals de database, authenticatie middleware en de authenticatie helper. Maar het werd erg onoverzichtelijk.

Authenticatie

Na onderzoek gedaan te hebben kwam ik uit op JSON Web Tokens als een authenticatie methode in tegen stelling tot de, gesuggereerde, 'ouderwetse' methode van session cookies. Waarbij ik deze bronnen gebruikt heb:

https://hackernoon.com/using-session-cookies-vs-jwt-for-authentication-sd2v3vci https://www.digitalocean.com/community/tutorials/nodejs-jwt-expressjs https://stackoverflow.com/questions/27978868/destroy-cookie-nodejs https://codeforgeek.com/refresh-token-jwt-nodejs-authentication/ https://stackoverflow.com/questions/59511628/is-it-secure-to-store-a-refresh-token-in-the-database-to-issue-new-access-toke

Na het aan de praat gekregen te hebben van JWT en een succesvol registratie & login feature te hebben gebouwd liep ik al gauw tegen de lamp. Want mijn implementatie voelde niet helemaal zoals het zou moeten. Dus heb ik een vraag gesteld op Stackoverflow: https://stackoverflow.com/questions/71382334/expressjs-authorization-with-jwt-is-this-correct

Hier kwam al gauw naar voren dat mijn implementatie inderdaad niet helemaal klopte en eigenlijk gewoon een session cookie methode is met een JWT erin. Dus heb ik het herschreven naar Session Cookies met de express-session package.

Dit heeft m'n code trouwens ook enorm versimpeld. Van deze belachelijk grote middleware:

import Log from '../services/Log.js';
import jwt from 'jsonwebtoken';
import Auth from '../services/Auth.js';

function authenticateToken(req, res, next) {
  const token = req.cookies.token;
  const refreshToken = req.cookies.refreshToken;

  if (token === null) return res.redirect('/login');

  try {
    const verified = jwt.verify(token, process.env.TOKEN_SECRET);
    if (verified) {
      req.token = verified;
      return next();
    }
  } catch (err) {
    try {
      const refreshInDb = Auth.refreshTokens.find(
        (token) => token === refreshToken
      );
      const refreshVerified =
        refreshInDb &&
        jwt.verify(refreshToken, process.env.REFRESHTOKEN_SECRET);
      const newToken = jwt.sign(
        { email: refreshVerified.email },
        process.env.TOKEN_SECRET,
        { expiresIn: 20 }
      );
      res.cookie('token', newToken, { maxAge: 900000, httpOnly: true });
      return next();
    } catch (err) {
      Log(err);
      return res.redirect('/user/login');
    }
  }
}

export default authenticateToken;

Naar dit kleine functietje:

import Log from '../services/Log.js';

function authenticated(req, res, next) {
  try {
    if (!req.session.email) return res.redirect('/user/login');
    next();
  } catch (err) {
    Log(err);
  }
}

export default authenticated;

143 -> 35

Door gebruik te maken van de Model View Controller heb ik de lijntjes in serve.js drastisch kunnen verlagen. En alles netjes in mappen kunnen verdelen. Het is een beetje veel om de before & after hier te laten zien. Dus ik verwijs graag naar release week-3 en week-4 om het verschil te kunnen zien.

CommonJS -> ESM

Verder vind ik de CommonJS notatie niet expressief genoeg. Het leest niet lekker weg en maakt de code lelijker. Daarom ben ik overgestapt op ESModules

-const path = require('path');
-require('dotenv').config({ path: path.join(__dirname, '/.env') });
+import 'dotenv/config';

Path is niet meer nodig door de ESModules en dotenv is makkelijker te importeren. 🗡️

Ik heb in het User model een assign functie gemaakt die een default User object creëert. Zodat het user object altijd hetzelfde eruit ziet.

const assign = user => Object.assign({
  _id: '',
  created_at: Date.now(),
  email: '',
  name: 'Unknown',
  password: ''
}, user);

En vervolgens heb ik in m'n register functie het volgende aangepast:

-  const user = {
+  const user = User.assign({
    created_at: Date.now(),
    email: req.body.email,
    name: req.body.name,
-  );
+  });
async function login(req, res) {
  if (req.method === 'GET') return res.render('login');

  const user = { email: req.body.email, password: req.body.password };
  const dbUser = await User.getByEmail(user.email);

-  if (!dbUser || !bcrypt.compareSync(user.password, dbUser.password)) {
+  if (dbUser && bcrypt.compareSync(user.password, dbUser.password)) {
-    return res.render('login', {
-      email: user.email,
-      alert: {
-        title: 'Whoops!',
-        body: 'There seems to be something wrong with your account details.',
-      },
-    });
+    const [token, refreshToken] = Auth.generateToken(user.email);
+    res.cookie('token', token, { maxAge: 900000, httpOnly: true });
+    res.cookie('refreshToken', refreshToken, {
+      maxAge: 900000,
+      httpOnly: true,
+    });
+    return res.redirect('/');
  }

-  const [token, refreshToken] = Auth.generateToken(user.email);
-  res.cookie('token', token, { maxAge: 900000, httpOnly: true });
-  res.cookie('refreshToken', refreshToken, {
-    maxAge: 900000,
-    httpOnly: true,
-  });
-  return res.redirect('/');

+  return res.render('login', {
+    email: user.email,
+    alert: {
+      title: 'Whoops!',
+      body: 'There seems to be something wrong with your account details.',
+    },
+  });
}

Eerst logde ik al m'n errors gewoon naar de console:

} catch (err) {
    console.error(err);
    return err;
}

Om dit te refactoren heb ik een Log service gemaakt in app/services/Log.js:

export default function(error) {
  console.log(error);
}

En dit aangepast overal waar ik console.error gebruikte:

import Log from '../services/Log.js';
} catch (err) {
   Log(err);
}

Vervolgens ben ik gaan zoeken naar een cloud service en kwam ik terecht bij Sentry. Ze hebben een erg makkelijke opzet voor NodeJS: https://docs.sentry.io/platforms/node/usage/ en dus heb ik m'n Log functie aangepast:

import * as Sentry from "@sentry/node";
import "@sentry/tracing";

Sentry.init({
  dsn: "<redacted>",
  // We recommend adjusting this value in production, or using tracesSampler
  // for finer control
  tracesSampleRate: 1.0,
});

export default function(error) {
  Sentry.captureException(error);
}

En nu krijg ik m'n errors in een mooi overzicht te zien: Schermafbeelding 2022-03-09 om 18 53 27