Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
111 changes: 111 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
'use strict';

import * as authService from '../services/auth.service.js';

const COOKIE_OPTIONS = {
httpOnly: true,
signed: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // cookie dura 7 dias
};

export const register = async (req, res) => {
const { name, email, password } = req.body;

if (!name || !email || !password) {
return res
.status(400)
.json({ message: 'Name, email and password are required' });
}

try {
await authService.register({ name, email, password });

return res.status(201).json({
message: 'Registration successful. Please check your email.',
});
} catch (err) {
return res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
}
};

export const activate = async (req, res) => {
const { token } = req.params;

try {
await authService.activate(token);
return res.json({
message: 'Account activated successfully. You can now log in.',
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const login = async (req, res) => {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}
try {
const user = await authService.login({ email, password });

res.cookie('userId', user.id, COOKIE_OPTIONS);
return res.json({
user: { id: user.id, name: user.name, email: user.email },
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const logout = async (req, res) => {
res.clearCookie('userId');

return res.json({ message: 'Logged out successfully' });
};

export const requestPasswordReset = async (req, res) => {
const { email } = req.body;

if (!email) {
return res.status(400).json({ message: 'Email is required' });
}

try {
await authService.requestPasswordReset(email);

return res.json({
message:
'If that email is registered, a password reset link has been sent.',
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const confirmPasswordReset = async (req, res) => {
const { token } = req.params;
const { password, confirmation } = req.body;
if (!password || !confirmation) {
return res
.status(400)
.json({ message: 'Password and confirmation are required' });
}

try {
await authService.resetPassword({ token, password, confirmation });
return res.json({
message: 'Password reset successfully.',
});
} catch (err) {
return res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
}
};
88 changes: 88 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';

import * as userService from '../services/user.service.js';

export const getProfile = async (req, res) => {
try {
const user = await userService.getProfile(req.user.id);
return res.json({ user });
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const changeName = async (req, res) => {
const { name } = req.body;

try {
const user = await userService.changeName(req.user.id, name);

return res.json({
message: 'Name updated successfully',
user: { id: user.id, name: user.name, email: user.email },
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const changePassword = async (req, res) => {
const { oldPassword, newPassword, confirmation } = req.body;

if (!oldPassword || !newPassword || !confirmation) {
return res.status(400).json({
message: 'Old password, new password and confirmation are required',
});
}

try {
await userService.changePassword(req.user.id, {
oldPassword,
newPassword,
confirmation,
});

return res.json({
message: 'Password changed successfully. Please log in again.',
});
} catch (err) {
return res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
}
};

export const changeEmail = async (req, res) => {
const { password, newEmail } = req.body;

if (!password || !newEmail) {
return res
.status(400)
.json({ message: 'Password and new email are required' });
}

try {
await userService.changeEmail(req.user.id, { password, newEmail });
return res.json({
message: 'A confirmation link has been sent to your new email address.',
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};

export const confirmEmailChange = async (req, res) => {
const { token } = req.params;

try {
const user = await userService.confirmEmailChange(token);

return res.json({
message: 'Email changed successfully.',
user: { id: user.id, name: user.name, email: user.email },
});
} catch (err) {
return res.status(err.status || 500).json({ message: err.message });
}
};
17 changes: 17 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

import { Sequelize } from 'sequelize';

const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASS,
{
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
dialect: 'postgres',
logging: false,
},
);

export default sequelize;
34 changes: 34 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
'use strict';

import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { initDb } from './models/index.js';
import authRouter from './routes/auth.route.js';
import userRouter from './routes/user.route.js';

const PORT = process.env.PORT || 3005;

const app = express();

app.use(
cors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true,
}),
);
app.use(express.json());
app.use(cookieParser(process.env.COOKIE_SECRET || 'cookie_secret_change_me'));
app.use('/auth', authRouter);
app.use('/user', userRouter);
app.use((req, res) => {
res.status(404).json({ message: 'Not found' });
});

initDb()
.then(() => {
app.listen(PORT);
})
.catch(() => {
process.exit(1);
});
12 changes: 12 additions & 0 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

export const authMiddleware = (req, res, next) => {
const userId = req.signedCookies?.userId;

if (!userId) {
return res.status(401).json({ message: 'Unauthorized' });
}

req.user = { id: userId };
next();
};
9 changes: 9 additions & 0 deletions src/middlewares/guest.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

export const guestMiddleware = (req, res, next) => {
if (req.signedCookies?.userId) {
return res.status(403).json({ message: 'You are already authenticated' });
}

next();
};
51 changes: 51 additions & 0 deletions src/models/User.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';

import { DataTypes } from 'sequelize';
import sequelize from '../db.js';

const User = sequelize.define('User', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
activationToken: {
type: DataTypes.STRING,
allowNull: true,
},
passwordResetToken: {
type: DataTypes.STRING,
allowNull: true,
},
passwordResetExpiry: {
type: DataTypes.DATE,
allowNull: true,
},
pendingEmail: {
type: DataTypes.STRING,
allowNull: true,
},
emailChangeToken: {
type: DataTypes.STRING,
allowNull: true,
},
});

export default User;
10 changes: 10 additions & 0 deletions src/models/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

import sequelize from '../db.js';
import User from './User.model.js';

export const initDb = async () => {
await sequelize.sync();
};

export { User };
17 changes: 17 additions & 0 deletions src/routes/auth.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

import express from 'express';
import * as authController from '../controllers/auth.controller.js';
import { guestMiddleware } from '../middlewares/guest.middleware.js';
import { authMiddleware } from '../middlewares/auth.middleware.js';

const authRouter = express.Router();

authRouter.post('/register', guestMiddleware, authController.register);
authRouter.get('/activate/:token', guestMiddleware, authController.activate);
authRouter.post('/login', guestMiddleware, authController.login);
authRouter.post('/logout', authMiddleware, authController.logout);
authRouter.post('/reset-password', guestMiddleware, authController.requestPasswordReset);
authRouter.post('/reset-password/:token', guestMiddleware, authController.confirmPasswordReset);

export default authRouter;
17 changes: 17 additions & 0 deletions src/routes/user.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

import express from 'express';
import * as userController from '../controllers/user.controller.js';
import { authMiddleware } from '../middlewares/auth.middleware.js';

const userRouter = express.Router();

userRouter.use(authMiddleware);

userRouter.get('/profile', userController.getProfile);
userRouter.patch('/profile/name', userController.changeName);
userRouter.patch('/profile/password', userController.changePassword);
userRouter.patch('/profile/email', userController.changeEmail);
userRouter.get('/profile/confirm-email/:token', userController.confirmEmailChange);

export default userRouter;
Loading
Loading