Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,378 changes: 1,176 additions & 202 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,22 @@
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"prettier": "^3.3.2"
"prettier": "^3.3.2",
"supertest": "^7.2.2"
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"bcryptjs": "^3.0.3",
"body-parser": "^2.2.2",
"cookie-parser": "^1.4.7",
"dotenv": "^17.4.1",
"ejs": "^5.0.2",
"express": "^5.2.1",
"express-validator": "^7.3.2",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^8.0.5",
"uuid": "^13.0.0"
}
}
33 changes: 33 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const { router: authRouter } = require('./routes/auth');
const { router: passwordRouter } = require('./routes/passwordReset');
const { router: profileRouter } = require('./routes/profile');

const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use('/auth', authRouter);
app.use('/password', passwordRouter);
app.use('/profile', profileRouter);

app.get('/', (req, res) => {
res.redirect('/auth/login');
});

app.use((req, res) => {
res.status(404).render('errors/404', { title: 'Not Found' });
});

module.exports = app;
18 changes: 18 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

require('dotenv').config();

module.exports = {
port: process.env.PORT || 3000,
jwtSecret: process.env.JWT_SECRET || 'dev_secret',
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h',
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
nodeEnv: process.env.NODE_ENV || 'development',
smtp: {
host: process.env.SMTP_HOST || 'smtp.ethereal.email',
port: parseInt(process.env.SMTP_PORT, 10) || 587,
secure: process.env.SMTP_SECURE === 'true',
user: process.env.SMTP_USER || '',
pass: process.env.SMTP_PASS || '',
},
};
13 changes: 13 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
'use strict';

require('dotenv').config();

const config = require('./config');
const app = require('./app');
const { User } = require('./models/User');

app.listen(config.port, () => {
// eslint-disable-next-line no-console
console.log(`Server running on port ${config.port}`);
});

module.exports = { app, User };
24 changes: 24 additions & 0 deletions src/middleware/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const { verifyToken } = require('../services/token');

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

if (!token) {
return res.redirect('/auth/login');
}

const decoded = verifyToken(token);

if (!decoded) {
res.clearCookie('token');

return res.redirect('/auth/login');
}

req.userId = decoded.userId;
next();
}

module.exports = { authenticate };
22 changes: 22 additions & 0 deletions src/middleware/guest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const { verifyToken } = require('../services/token');

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

if (!token) {
return next();
}

const decoded = verifyToken(token);

if (decoded) {
return res.redirect('/profile');
}

res.clearCookie('token');
next();
}

module.exports = { guest };
172 changes: 172 additions & 0 deletions src/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use strict';

const { randomUUID } = require('crypto');
const bcrypt = require('bcryptjs');

const users = new Map();
const resetTokens = new Map();
const activationTokens = new Map();

function hashPassword(password) {
return bcrypt.hashSync(password, 10);
}

function comparePassword(password, hash) {
return bcrypt.compareSync(password, hash);
}

class User {
constructor({ id, name, email, password, isActive }) {
this.id = id;
this.name = name;
this.email = email;
this.password = password;
this.isActive = isActive;
}

toJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
isActive: this.isActive,
};
}

static create({ name, email, password }) {
const id = randomUUID();
const hashedPassword = hashPassword(password);
const user = new User({
id,
name,
email: email.toLowerCase(),
password: hashedPassword,
isActive: false,
});

users.set(id, user);

const activationToken = randomUUID();

activationTokens.set(activationToken, id);

return { user, activationToken };
}

static findById(id) {
return users.get(id) || null;
}

static findByEmail(email) {
for (const user of users.values()) {
if (user.email === email.toLowerCase()) {
return user;
}
}

return null;
}

static verifyPassword(user, password) {
return comparePassword(password, user.password);
}

static activate(activationToken) {
const userId = activationTokens.get(activationToken);

if (!userId) {
return null;
}

const user = users.get(userId);

if (!user) {
return null;
}

user.isActive = true;
activationTokens.delete(activationToken);

return user;
}

static updateName(id, name) {
const user = users.get(id);

if (!user) {
return null;
}

user.name = name;

return user;
}

static updatePassword(id, oldPassword, newPassword) {
const user = users.get(id);

if (!user || !comparePassword(oldPassword, user.password)) {
return null;
}

user.password = hashPassword(newPassword);

return user;
}

static updateEmail(id, password, newEmail) {
const user = users.get(id);

if (!user || !comparePassword(password, user.password)) {
return null;
}

const oldEmail = user.email;

user.email = newEmail.toLowerCase();

return { user, oldEmail };
}

static createResetToken(email) {
const user = User.findByEmail(email);

if (!user) {
return null;
}

const token = randomUUID();

resetTokens.set(token, { userId: user.id, createdAt: Date.now() });

return token;
}

static resetPassword(token, newPassword) {
const data = resetTokens.get(token);

if (!data) {
return null;
}

const user = users.get(data.userId);

if (!user) {
return null;
}

user.password = hashPassword(newPassword);
user.isActive = true;
resetTokens.delete(token);

return user;
}

static reset() {
users.clear();
resetTokens.clear();
activationTokens.clear();
}
}

module.exports = { User };
Loading
Loading