From e55eb89f837d80ba4ae61ff9c1fe5bac642d7189 Mon Sep 17 00:00:00 2001 From: chlodav <153681219+chlodav@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:25:39 +0300 Subject: [PATCH 1/4] add task solution --- src/index.js | 547 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.test.js | 193 ++++++++++++++++ 2 files changed, 740 insertions(+) create mode 100644 src/index.test.js diff --git a/src/index.js b/src/index.js index ad9a93a7..cd7190ad 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,548 @@ 'use strict'; + +const crypto = require('node:crypto'); +const http = require('node:http'); +const { URL } = require('node:url'); + +function createServer() { + const users = []; + const sessions = new Map(); + let nextId = 1; + + function escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function parseBody(req) { + return new Promise((resolve, reject) => { + let body = ''; + + req.on('data', (chunk) => { + body += chunk; + }); + + req.on('end', () => { + const params = new URLSearchParams(body); + + const data = {}; + + for (const [key, value] of params.entries()) { + data[key] = value; + } + + resolve(data); + }); + + req.on('error', reject); + }); + } + + function renderPage(title, content, user) { + const welcome = user ? `

Hello, ${escapeHtml(user.name)}.

` : ''; + + return ` + + + + ${escapeHtml(title)} + + + + + ${welcome} +

${escapeHtml(title)}

+ ${content} + +`; + } + + function sendHtml(res, statusCode, body, headers = {}) { + res.writeHead(statusCode, { + 'content-type': 'text/html; charset=utf-8', + ...headers, + }); + res.end(body); + } + + function createSession(user) { + const sessionId = crypto.randomBytes(16).toString('hex'); + + sessions.set(sessionId, user.email); + + return sessionId; + } + + function getAuthenticatedUser(req) { + const cookies = req.headers.cookie || ''; + const sessionCookie = cookies + .split(';') + .map((entry) => entry.trim()) + .find((entry) => entry.startsWith('sessionId=')); + + if (!sessionCookie) { + return null; + } + + const sessionId = sessionCookie.split('=')[1]; + + const email = sessions.get(sessionId); + + if (!email) { + return null; + } + + return users.find((user) => user.email === email) || null; + } + + function validatePassword(password) { + return ( + password.length >= 8 && + /[A-Z]/.test(password) && + /[a-z]/.test(password) && + /[0-9]/.test(password) && + /[^A-Za-z0-9]/.test(password) + ); + } + + function getUserByEmail(email) { + return users.find((user) => user.email === email); + } + + function getUserByActivationToken(token) { + return users.find((user) => user.activationToken === token); + } + + function getUserByResetToken(token) { + return users.find((user) => user.resetToken === token); + } + + return http.createServer(async (req, res) => { + const url = new URL(req.url, 'http://127.0.0.1'); + const { pathname } = url; + + if (req.method === 'GET' && pathname === '/') { + res.writeHead(302, { location: '/login' }); + res.end(); + + return; + } + + if (req.method === 'GET' && pathname === '/login') { + const content = ` +
+ + + + + +
+ `; + + sendHtml(res, 200, renderPage('Login', content)); + + return; + } + + if (req.method === 'POST' && pathname === '/login') { + const body = await parseBody(req); + const user = getUserByEmail(body.email); + + if (!user || user.password !== body.password) { + sendHtml( + res, + 200, + renderPage( + 'Login', + '

Invalid email or password.

Try again', + ), + ); + + return; + } + + if (!user.active) { + sendHtml( + res, + 200, + renderPage( + 'Login', + `

Please activate your email before signing in. Resend activation

`, + ), + ); + + return; + } + + const sessionId = createSession(user); + + res.writeHead(302, { + location: '/profile', + 'set-cookie': [`sessionId=${sessionId}; HttpOnly; Path=/`], + }); + res.end(); + + return; + } + + if (req.method === 'GET' && pathname === '/register') { + const content = ` +

Password rules: at least 8 characters, one uppercase, one lowercase, one number, one special character.

+
+ + + + + + + +
+ `; + + sendHtml(res, 200, renderPage('Register', content)); + + return; + } + + if (req.method === 'POST' && pathname === '/register') { + const body = await parseBody(req); + + if (!body.name || !body.email || !body.password) { + sendHtml( + res, + 200, + renderPage( + 'Register', + '

All fields are required.

', + ), + ); + + return; + } + + if (!validatePassword(body.password)) { + sendHtml( + res, + 200, + renderPage( + 'Register', + '

Password must follow the required rules.

', + ), + ); + + return; + } + + if (getUserByEmail(body.email)) { + sendHtml( + res, + 200, + renderPage( + 'Register', + '

An account with that email already exists.

', + ), + ); + + return; + } + + const activationToken = crypto.randomBytes(16).toString('hex'); + const user = { + id: nextId++, + name: body.name, + email: body.email, + password: body.password, + active: false, + activationToken, + resetToken: null, + }; + + users.push(user); + + const content = ` +

Account created. Please confirm your email.

+

Activate account

+ `; + + sendHtml(res, 200, renderPage('Register', content)); + + return; + } + + if (req.method === 'GET' && pathname === '/activate') { + const token = url.searchParams.get('token'); + const user = getUserByActivationToken(token); + + if (!user) { + sendHtml( + res, + 200, + renderPage( + 'Activation', + '

Invalid activation token.

', + ), + ); + + return; + } + + user.active = true; + user.activationToken = null; + const sessionId = createSession(user); + + res.writeHead(302, { + location: '/profile', + 'set-cookie': [`sessionId=${sessionId}; HttpOnly; Path=/`], + }); + res.end(); + + return; + } + + if (req.method === 'GET' && pathname === '/forgot-password') { + const content = ` +
+ + + +
+ `; + + sendHtml(res, 200, renderPage('Forgot password', content)); + + return; + } + + if (req.method === 'POST' && pathname === '/forgot-password') { + const body = await parseBody(req); + const user = getUserByEmail(body.email); + + if (user) { + user.resetToken = crypto.randomBytes(16).toString('hex'); + } + + const resetLink = user + ? `

Reset password

` + : ''; + + const content = ` +

If the email exists, a reset link has been sent.

+ ${resetLink} + `; + + sendHtml(res, 200, renderPage('Forgot password', content)); + + return; + } + + if (req.method === 'GET' && pathname === '/reset-password') { + const token = url.searchParams.get('token'); + const user = getUserByResetToken(token); + + if (!user) { + sendHtml( + res, + 200, + renderPage( + 'Reset password', + '

Invalid reset token.

', + ), + ); + + return; + } + + const content = ` +
+ + + + + +
+ `; + + sendHtml(res, 200, renderPage('Reset password', content)); + + return; + } + + if (req.method === 'POST' && pathname === '/reset-password') { + const token = url.searchParams.get('token'); + const body = await parseBody(req); + const user = getUserByResetToken(token); + + if (!user) { + sendHtml( + res, + 200, + renderPage( + 'Reset password', + '

Invalid reset token.

', + ), + ); + + return; + } + + if (body.password !== body.confirmation) { + sendHtml( + res, + 200, + renderPage( + 'Reset password', + '

Passwords must match.

', + ), + ); + + return; + } + + if (!validatePassword(body.password)) { + sendHtml( + res, + 200, + renderPage( + 'Reset password', + '

Password must follow the required rules.

', + ), + ); + + return; + } + + user.password = body.password; + user.resetToken = null; + + const content = ` +

Password updated. Sign in

+ `; + + sendHtml(res, 200, renderPage('Reset password', content)); + + return; + } + + if (req.method === 'GET' && pathname === '/profile') { + const user = getAuthenticatedUser(req); + + if (!user) { + res.writeHead(302, { location: '/login' }); + res.end(); + + return; + } + + const content = ` +

Welcome, ${escapeHtml(user.name)}!

+
+ + + +
+

Logout

+ `; + + sendHtml(res, 200, renderPage('Profile', content, user)); + + return; + } + + if (req.method === 'POST' && pathname === '/profile') { + const user = getAuthenticatedUser(req); + + if (!user) { + res.writeHead(302, { location: '/login' }); + res.end(); + + return; + } + + const body = await parseBody(req); + let message = 'Profile updated.'; + + if (body.name) { + user.name = body.name; + } + + if ( + body.oldPassword !== undefined || + body.newPassword !== undefined || + body.confirmation !== undefined + ) { + if ( + !body.oldPassword || + !body.newPassword || + !body.confirmation || + user.password !== body.oldPassword + ) { + message = 'Current password is incorrect.'; + } else if (body.newPassword !== body.confirmation) { + message = 'New passwords must match.'; + } else if (!validatePassword(body.newPassword)) { + message = 'Password must follow the required rules.'; + } else { + user.password = body.newPassword; + message = 'Password updated.'; + } + } + + const content = ` +

${escapeHtml(message)}

+

Welcome, ${escapeHtml(user.name)}!

+ `; + + sendHtml(res, 200, renderPage('Profile', content, user)); + + return; + } + + if (req.method === 'GET' && pathname === '/logout') { + const cookies = req.headers.cookie || ''; + const sessionCookie = cookies + .split(';') + .map((entry) => entry.trim()) + .find((entry) => entry.startsWith('sessionId=')); + + if (sessionCookie) { + const sessionId = sessionCookie.split('=')[1]; + + sessions.delete(sessionId); + } + + res.writeHead(302, { + location: '/login', + 'set-cookie': ['sessionId=; Max-Age=0; Path=/'], + }); + res.end(); + + return; + } + + sendHtml( + res, + 404, + renderPage('Not found', '

Page not found.

'), + ); + }); +} + +module.exports = { + createServer, +}; diff --git a/src/index.test.js b/src/index.test.js new file mode 100644 index 00000000..b09bc85a --- /dev/null +++ b/src/index.test.js @@ -0,0 +1,193 @@ +'use strict'; + +const assert = require('node:assert'); +const http = require('node:http'); +const { createServer } = require('./index'); + +function startServer() { + const server = createServer(); + + return new Promise((resolve) => { + server.listen(0, () => { + const address = server.address(); + + resolve({ + server, + port: address.port, + }); + }); + }); +} + +function makeRequest(server, method, path, options = {}) { + const { body = '', headers = {} } = options; + + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: server.address().port, + path, + method, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + ...headers, + }, + }, + (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: data, + }); + }); + }, + ); + + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function getCookie(headers) { + const setCookie = headers['set-cookie']; + + return Array.isArray(setCookie) ? setCookie[0] : setCookie; +} + +describe('auth application', () => { + let server; + let port; + + beforeEach(async () => { + ({ server, port } = await startServer()); + }); + + afterEach((done) => { + server.close(done); + }); + + it('registers a user and activates the account via email token', async () => { + const registerResponse = await makeRequest(server, 'POST', '/register', { + body: 'name=Test+User&email=test@example.com&password=Abc123!@', + }); + + assert.strictEqual(registerResponse.statusCode, 200); + const tokenMatch = registerResponse.body.match(/activate\?token=([^"']+)/); + + assert.ok(tokenMatch, 'activation link should be present'); + + const activationResponse = await makeRequest( + server, + 'GET', + `/activate?token=${tokenMatch[1]}`, + ); + + assert.strictEqual(activationResponse.statusCode, 302); + assert.strictEqual(activationResponse.headers.location, '/profile'); + + const profileResponse = await makeRequest(server, 'GET', '/profile', { + headers: { + cookie: getCookie(activationResponse.headers), + }, + }); + + assert.strictEqual(profileResponse.statusCode, 200); + assert.match(profileResponse.body, /Welcome, Test User/i); + }); + + it('allows a user to reset the password and sign in with the new password', async () => { + const registerResponse = await makeRequest(server, 'POST', '/register', { + body: 'name=Reset+User&email=reset@example.com&password=Abc123!@', + }); + + const activationTokenMatch = registerResponse.body.match( + /activate\?token=([^"']+)/, + ); + + await makeRequest( + server, + 'GET', + `/activate?token=${activationTokenMatch[1]}`, + ); + + const forgotResponse = await makeRequest( + server, + 'POST', + '/forgot-password', + { + body: 'email=reset@example.com', + }, + ); + + const resetTokenMatch = forgotResponse.body.match( + /reset-password\?token=([^"']+)/, + ); + + assert.ok(resetTokenMatch, 'reset link should be present'); + + const resetResponse = await makeRequest( + server, + 'POST', + `/reset-password?token=${resetTokenMatch[1]}`, + { + body: 'password=NewPass123!&confirmation=NewPass123!', + }, + ); + + assert.strictEqual(resetResponse.statusCode, 200); + assert.match(resetResponse.body, /Password updated/i); + + const loginResponse = await makeRequest(server, 'POST', '/login', { + body: 'email=reset@example.com&password=NewPass123!', + }); + + assert.strictEqual(loginResponse.statusCode, 302); + assert.strictEqual(loginResponse.headers.location, '/profile'); + }); + + it('allows a signed in user to change their password from the profile page', async () => { + const registerResponse = await makeRequest(server, 'POST', '/register', { + body: 'name=Profile+User&email=profile@example.com&password=Abc123!@', + }); + + const activationTokenMatch = registerResponse.body.match( + /activate\?token=([^"']+)/, + ); + + await makeRequest( + server, + 'GET', + `/activate?token=${activationTokenMatch[1]}`, + ); + + const loginResponse = await makeRequest(server, 'POST', '/login', { + body: 'email=profile@example.com&password=Abc123!@', + }); + + const profileResponse = await makeRequest(server, 'POST', '/profile', { + body: 'oldPassword=Abc123!@&newPassword=NewPass456!&confirmation=NewPass456!', + headers: { + cookie: getCookie(loginResponse.headers), + }, + }); + + assert.strictEqual(profileResponse.statusCode, 200); + assert.match(profileResponse.body, /Password updated/i); + + const loginWithNewPasswordResponse = await makeRequest(server, 'POST', '/login', { + body: 'email=profile@example.com&password=NewPass456!', + }); + + assert.strictEqual(loginWithNewPasswordResponse.statusCode, 302); + assert.strictEqual(loginWithNewPasswordResponse.headers.location, '/profile'); + }); +}); From 4356359a65aac708a156dc51502372f497bc9019 Mon Sep 17 00:00:00 2001 From: chlodav <153681219+chlodav@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:37:07 +0300 Subject: [PATCH 2/4] Add auth-only guards and email update flow --- src/index.js | 79 +++++++++++++++++++++++++++++++++++++++++++++-- src/index.test.js | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index cd7190ad..01bd729f 100644 --- a/src/index.js +++ b/src/index.js @@ -121,6 +121,10 @@ function createServer() { ); } + function isAuthenticated(req) { + return Boolean(getAuthenticatedUser(req)); + } + function getUserByEmail(email) { return users.find((user) => user.email === email); } @@ -145,6 +149,13 @@ function createServer() { } if (req.method === 'GET' && pathname === '/login') { + if (isAuthenticated(req)) { + res.writeHead(302, { location: '/profile' }); + res.end(); + + return; + } + const content = `
@@ -202,6 +213,13 @@ function createServer() { } if (req.method === 'GET' && pathname === '/register') { + if (isAuthenticated(req)) { + res.writeHead(302, { location: '/profile' }); + res.end(); + + return; + } + const content = `

Password rules: at least 8 characters, one uppercase, one lowercase, one number, one special character.

@@ -276,7 +294,7 @@ function createServer() { users.push(user); const content = ` -

Account created. Please confirm your email.

+

Account created. An activation email was sent to ${escapeHtml(user.email)}.

Activate account

`; @@ -286,6 +304,13 @@ function createServer() { } if (req.method === 'GET' && pathname === '/activate') { + if (isAuthenticated(req)) { + res.writeHead(302, { location: '/profile' }); + res.end(); + + return; + } + const token = url.searchParams.get('token'); const user = getUserByActivationToken(token); @@ -316,6 +341,13 @@ function createServer() { } if (req.method === 'GET' && pathname === '/forgot-password') { + if (isAuthenticated(req)) { + res.writeHead(302, { location: '/profile' }); + res.end(); + + return; + } + const content = ` @@ -342,7 +374,7 @@ function createServer() { : ''; const content = ` -

If the email exists, a reset link has been sent.

+

If the email exists, a reset email was sent to ${escapeHtml(body.email || '')}.

${resetLink} `; @@ -352,6 +384,13 @@ function createServer() { } if (req.method === 'GET' && pathname === '/reset-password') { + if (isAuthenticated(req)) { + res.writeHead(302, { location: '/profile' }); + res.end(); + + return; + } + const token = url.searchParams.get('token'); const user = getUserByResetToken(token); @@ -456,6 +495,24 @@ function createServer() {
+
+ + + + + + + +
+
+ + + + + + + +

Logout

`; @@ -481,6 +538,24 @@ function createServer() { user.name = body.name; } + if ( + body.newEmail !== undefined || + body.confirmationEmail !== undefined || + body.password !== undefined + ) { + if (!body.password || !body.newEmail || !body.confirmationEmail) { + message = 'Please provide your current password and both email fields.'; + } else if (user.password !== body.password) { + message = 'Current password is incorrect.'; + } else if (body.newEmail !== body.confirmationEmail) { + message = 'New emails must match.'; + } else { + const previousEmail = user.email; + user.email = body.newEmail; + message = `Email updated. A notification was sent to ${escapeHtml(previousEmail)}.`; + } + } + if ( body.oldPassword !== undefined || body.newPassword !== undefined || diff --git a/src/index.test.js b/src/index.test.js index b09bc85a..2c934415 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -190,4 +190,67 @@ describe('auth application', () => { assert.strictEqual(loginWithNewPasswordResponse.statusCode, 302); assert.strictEqual(loginWithNewPasswordResponse.headers.location, '/profile'); }); + + it('redirects authenticated users away from the login page', async () => { + const registerResponse = await makeRequest(server, 'POST', '/register', { + body: 'name=Guard+User&email=guard@example.com&password=Abc123!@', + }); + + const activationTokenMatch = registerResponse.body.match( + /activate\?token=([^"']+)/, + ); + + const activationResponse = await makeRequest( + server, + 'GET', + `/activate?token=${activationTokenMatch[1]}`, + ); + + const loginResponse = await makeRequest(server, 'GET', '/login', { + headers: { + cookie: getCookie(activationResponse.headers), + }, + }); + + assert.strictEqual(loginResponse.statusCode, 302); + assert.strictEqual(loginResponse.headers.location, '/profile'); + }); + + it('allows a signed in user to change their email from the profile page', async () => { + const registerResponse = await makeRequest(server, 'POST', '/register', { + body: 'name=Email+User&email=old@example.com&password=Abc123!@', + }); + + const activationTokenMatch = registerResponse.body.match( + /activate\?token=([^"']+)/, + ); + + const activationResponse = await makeRequest( + server, + 'GET', + `/activate?token=${activationTokenMatch[1]}`, + ); + + const loginResponse = await makeRequest(server, 'POST', '/login', { + body: 'email=old@example.com&password=Abc123!@', + }); + + const profileResponse = await makeRequest(server, 'POST', '/profile', { + body: 'password=Abc123!@&newEmail=new@example.com&confirmationEmail=new@example.com', + headers: { + cookie: getCookie(loginResponse.headers), + }, + }); + + assert.strictEqual(profileResponse.statusCode, 200); + assert.match(profileResponse.body, /Email updated/i); + assert.match(profileResponse.body, /old@example.com/i); + + const loginWithNewEmailResponse = await makeRequest(server, 'POST', '/login', { + body: 'email=new@example.com&password=Abc123!@', + }); + + assert.strictEqual(loginWithNewEmailResponse.statusCode, 302); + assert.strictEqual(loginWithNewEmailResponse.headers.location, '/profile'); + }); }); From 9f38fd8c5ce54cc9589a1dfa098a362f7a6ff647 Mon Sep 17 00:00:00 2001 From: chlodav <153681219+chlodav@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:39:07 +0300 Subject: [PATCH 3/4] add task solution --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 288d83dd..f67c20c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -1467,10 +1467,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index 5e195a15..9d160819 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", From 596fd765239cdb2263294d5b4d18eeb99bd22b48 Mon Sep 17 00:00:00 2001 From: chlodav <153681219+chlodav@users.noreply.github.com> Date: Sun, 28 Jun 2026 01:30:03 +0300 Subject: [PATCH 4/4] add task solution --- src/index.js | 38 ++++++++++++++++++++++++-------------- src/index.test.js | 45 +++++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/index.js b/src/index.js index 01bd729f..1b63d204 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,10 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable indent */ 'use strict'; -const crypto = require('node:crypto'); +const nodeCrypto = require('node:crypto'); const http = require('node:http'); -const { URL } = require('node:url'); +const { URL: NodeURL } = require('node:url'); function createServer() { const users = []; @@ -28,7 +30,6 @@ function createServer() { req.on('end', () => { const params = new URLSearchParams(body); - const data = {}; for (const [key, value] of params.entries()) { @@ -82,7 +83,7 @@ function createServer() { } function createSession(user) { - const sessionId = crypto.randomBytes(16).toString('hex'); + const sessionId = nodeCrypto.randomBytes(16).toString('hex'); sessions.set(sessionId, user.email); @@ -101,7 +102,6 @@ function createServer() { } const sessionId = sessionCookie.split('=')[1]; - const email = sessions.get(sessionId); if (!email) { @@ -138,7 +138,7 @@ function createServer() { } return http.createServer(async (req, res) => { - const url = new URL(req.url, 'http://127.0.0.1'); + const url = new NodeURL(req.url, 'http://127.0.0.1'); const { pathname } = url; if (req.method === 'GET' && pathname === '/') { @@ -181,7 +181,8 @@ function createServer() { 200, renderPage( 'Login', - '

Invalid email or password.

Try again', + '

Invalid email or password.

' + + 'Try again', ), ); @@ -194,7 +195,9 @@ function createServer() { 200, renderPage( 'Login', - `

Please activate your email before signing in. Resend activation

`, + // eslint-disable-next-line max-len + '

Please activate your email before signing in.

' + + '

Resend activation

', ), ); @@ -221,7 +224,8 @@ function createServer() { } const content = ` -

Password rules: at least 8 characters, one uppercase, one lowercase, one number, one special character.

+

Password rules: at least 8 characters, one uppercase, one lowercase, + one number, one special character.

@@ -280,7 +284,7 @@ function createServer() { return; } - const activationToken = crypto.randomBytes(16).toString('hex'); + const activationToken = nodeCrypto.randomBytes(16).toString('hex'); const user = { id: nextId++, name: body.name, @@ -294,7 +298,9 @@ function createServer() { users.push(user); const content = ` -

Account created. An activation email was sent to ${escapeHtml(user.email)}.

+

Account created. An activation email was sent to ${escapeHtml( + user.email, + )}.

Activate account

`; @@ -329,6 +335,7 @@ function createServer() { user.active = true; user.activationToken = null; + const sessionId = createSession(user); res.writeHead(302, { @@ -366,7 +373,7 @@ function createServer() { const user = getUserByEmail(body.email); if (user) { - user.resetToken = crypto.randomBytes(16).toString('hex'); + user.resetToken = nodeCrypto.randomBytes(16).toString('hex'); } const resetLink = user @@ -374,7 +381,8 @@ function createServer() { : ''; const content = ` -

If the email exists, a reset email was sent to ${escapeHtml(body.email || '')}.

+

If the email exists, a reset email was sent to + ${escapeHtml(body.email || '')}.

${resetLink} `; @@ -544,13 +552,15 @@ function createServer() { body.password !== undefined ) { if (!body.password || !body.newEmail || !body.confirmationEmail) { - message = 'Please provide your current password and both email fields.'; + message = + 'Please provide your current password and both email fields.'; } else if (user.password !== body.password) { message = 'Current password is incorrect.'; } else if (body.newEmail !== body.confirmationEmail) { message = 'New emails must match.'; } else { const previousEmail = user.email; + user.email = body.newEmail; message = `Email updated. A notification was sent to ${escapeHtml(previousEmail)}.`; } diff --git a/src/index.test.js b/src/index.test.js index 2c934415..f26744b6 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -65,10 +65,9 @@ function getCookie(headers) { describe('auth application', () => { let server; - let port; beforeEach(async () => { - ({ server, port } = await startServer()); + ({ server } = await startServer()); }); afterEach((done) => { @@ -81,6 +80,7 @@ describe('auth application', () => { }); assert.strictEqual(registerResponse.statusCode, 200); + const tokenMatch = registerResponse.body.match(/activate\?token=([^"']+)/); assert.ok(tokenMatch, 'activation link should be present'); @@ -104,6 +104,7 @@ describe('auth application', () => { assert.match(profileResponse.body, /Welcome, Test User/i); }); + // eslint-disable-next-line max-len it('allows a user to reset the password and sign in with the new password', async () => { const registerResponse = await makeRequest(server, 'POST', '/register', { body: 'name=Reset+User&email=reset@example.com&password=Abc123!@', @@ -154,6 +155,7 @@ describe('auth application', () => { assert.strictEqual(loginResponse.headers.location, '/profile'); }); + // eslint-disable-next-line max-len it('allows a signed in user to change their password from the profile page', async () => { const registerResponse = await makeRequest(server, 'POST', '/register', { body: 'name=Profile+User&email=profile@example.com&password=Abc123!@', @@ -174,7 +176,9 @@ describe('auth application', () => { }); const profileResponse = await makeRequest(server, 'POST', '/profile', { - body: 'oldPassword=Abc123!@&newPassword=NewPass456!&confirmation=NewPass456!', + body: + 'oldPassword=Abc123!@&newPassword=NewPass456!&confirmation=' + + 'NewPass456!', headers: { cookie: getCookie(loginResponse.headers), }, @@ -183,12 +187,21 @@ describe('auth application', () => { assert.strictEqual(profileResponse.statusCode, 200); assert.match(profileResponse.body, /Password updated/i); - const loginWithNewPasswordResponse = await makeRequest(server, 'POST', '/login', { - body: 'email=profile@example.com&password=NewPass456!', - }); + const loginWithNewPasswordResponse = await makeRequest( + server, + 'POST', + '/login', + { + body: 'email=profile@example.com&password=NewPass456!', + }, + ); assert.strictEqual(loginWithNewPasswordResponse.statusCode, 302); - assert.strictEqual(loginWithNewPasswordResponse.headers.location, '/profile'); + + assert.strictEqual( + loginWithNewPasswordResponse.headers.location, + '/profile', + ); }); it('redirects authenticated users away from the login page', async () => { @@ -216,6 +229,7 @@ describe('auth application', () => { assert.strictEqual(loginResponse.headers.location, '/profile'); }); + // eslint-disable-next-line max-len it('allows a signed in user to change their email from the profile page', async () => { const registerResponse = await makeRequest(server, 'POST', '/register', { body: 'name=Email+User&email=old@example.com&password=Abc123!@', @@ -225,7 +239,7 @@ describe('auth application', () => { /activate\?token=([^"']+)/, ); - const activationResponse = await makeRequest( + await makeRequest( server, 'GET', `/activate?token=${activationTokenMatch[1]}`, @@ -236,7 +250,9 @@ describe('auth application', () => { }); const profileResponse = await makeRequest(server, 'POST', '/profile', { - body: 'password=Abc123!@&newEmail=new@example.com&confirmationEmail=new@example.com', + body: + 'password=Abc123!@&newEmail=new@example.com&confirmationEmail=' + + 'new@example.com', headers: { cookie: getCookie(loginResponse.headers), }, @@ -246,9 +262,14 @@ describe('auth application', () => { assert.match(profileResponse.body, /Email updated/i); assert.match(profileResponse.body, /old@example.com/i); - const loginWithNewEmailResponse = await makeRequest(server, 'POST', '/login', { - body: 'email=new@example.com&password=Abc123!@', - }); + const loginWithNewEmailResponse = await makeRequest( + server, + 'POST', + '/login', + { + body: 'email=new@example.com&password=Abc123!@', + }, + ); assert.strictEqual(loginWithNewEmailResponse.statusCode, 302); assert.strictEqual(loginWithNewEmailResponse.headers.location, '/profile');