-
Notifications
You must be signed in to change notification settings - Fork 303
node_auth #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
node_auth #230
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| name: Test | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [ master ] | ||
|
|
||
| jobs: | ||
| build: | ||
|
|
||
| runs-on: ubuntu-latest | ||
|
|
||
| strategy: | ||
| matrix: | ||
| node-version: [20.x] | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: npm install | ||
| - run: npm test |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,3 @@ node_modules | |
|
|
||
| # MacOS | ||
| .DS_Store | ||
|
|
||
| # env files | ||
| *.env | ||
| .env* | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| import bcrypt from 'bcrypt'; | ||
| import { v4 as uuidv4 } from 'uuid'; | ||
|
|
||
| import { userServices } from '../services/user.services.js'; | ||
| import { emailServices } from '../services/email.services.js'; | ||
| import { jwtService } from '../services/jwt.services.js'; | ||
| import { User } from '../models/User.model.js'; | ||
|
|
||
| function validatePassword(pwd) { | ||
| if (!pwd || pwd.length < 6) { | ||
| return 'At least 6 characters'; | ||
| } | ||
| } | ||
|
|
||
| const registerUser = async (req, res) => { | ||
| const { name, email, password } = req.body; | ||
|
|
||
| if (!name || !email || !password) { | ||
| return res.status(400).json({ message: 'All fields are required' }); | ||
| } | ||
|
|
||
| if (validatePassword(password)) { | ||
| return res.status(400).json({ password: validatePassword(password) }); | ||
| } | ||
|
|
||
| const exists = await userServices.findUser(email); | ||
|
|
||
| if (exists) { | ||
| return res.status(409).json({ message: 'Email already exists' }); | ||
| } | ||
|
|
||
| const activationToken = uuidv4(); | ||
| const hash = bcrypt.hashSync(password, 10); | ||
|
|
||
| await userServices.registerUser(name, email, hash, activationToken); | ||
| await emailServices.sendActivationEmail(email, activationToken); | ||
|
|
||
| res.status(201).json({ message: 'Check your email to activate account' }); | ||
| }; | ||
|
|
||
| const activateUser = async (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { activationToken } = req.params; | ||
|
|
||
| const user = await User.findOne({ where: { activationToken } }); | ||
|
|
||
| if (!user) { | ||
| return res.status(404).json({ message: 'User not found' }); | ||
| } | ||
|
|
||
| user.activationToken = null; | ||
| await user.save(); | ||
|
|
||
| res.redirect('/profile'); | ||
| }; | ||
|
|
||
| const loginUser = async (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { email, password } = req.body; | ||
|
|
||
| if (!email || !password) { | ||
| return res.status(400).json({ message: 'All fields are required' }); | ||
| } | ||
|
|
||
| const user = await userServices.findUser(email); | ||
|
|
||
| if (!user) { | ||
| return res.status(401).json({ message: 'Invalid credentials' }); | ||
| } | ||
|
|
||
| if (user.activationToken) { | ||
| return res.status(403).json({ message: 'Activate your email' }); | ||
| } | ||
|
|
||
| const valid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!valid) { | ||
| return res.status(401).json({ message: 'Invalid credentials' }); | ||
| } | ||
|
|
||
| // token generato SOLO per sessione client, non restituito | ||
| const normalized = userServices.normilizeUser(user); | ||
| const token = jwtService.sign(normalized); | ||
|
|
||
| // opzionale: se i test non lo richiedono, puoi anche ometterlo | ||
| res.setHeader('Authorization', `Bearer ${token}`); | ||
|
|
||
| return res.redirect('/profile'); | ||
| }; | ||
|
|
||
| const logout = (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| res.redirect('/login'); | ||
| }; | ||
|
|
||
| const forgot = async (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const { email } = req.body; | ||
|
|
||
| if (!email) { | ||
| return res.status(400).json({ message: 'Email required' }); | ||
| } | ||
|
|
||
| const user = await userServices.findUser(email); | ||
|
|
||
| if (!user) { | ||
| return res.sendStatus(200); | ||
| } | ||
|
|
||
| user.resetToken = uuidv4(); | ||
| await user.save(); | ||
|
|
||
| await emailServices.sendResetPasswordEmail(email, user.resetToken); | ||
| res.json({ message: 'Email sent' }); | ||
| }; | ||
|
|
||
| const resetPassword = async (req, res) => { | ||
| const { resetToken } = req.params; | ||
| const { password, confirmation } = req.body; | ||
|
|
||
| if (!password || password !== confirmation) { | ||
| return res.status(400).json({ message: 'Passwords do not match' }); | ||
| } | ||
|
|
||
| const user = await User.findOne({ where: { resetToken } }); | ||
|
|
||
| if (!user) { | ||
| return res.status(400).json({ message: 'Invalid token' }); | ||
| } | ||
|
|
||
| user.password = bcrypt.hashSync(password, 10); | ||
| user.resetToken = null; | ||
| await user.save(); | ||
|
|
||
| res.json({ message: 'Password changed' }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the requirements, the application should "Show Success page with a link to login" after a successful password reset. Currently, this function sends a JSON response. Please update this to redirect the user to a success page, similar to how redirects are handled in the |
||
| }; | ||
|
|
||
| export const authController = { | ||
| registerUser, | ||
| activateUser, | ||
| loginUser, | ||
| logout, | ||
| forgot, | ||
| resetPassword, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { userServices } from '../services/user.services.js'; | ||
| import { emailServices } from '../services/email.services.js'; | ||
| import bcrypt from 'bcrypt'; | ||
|
|
||
| const updateName = async (req, res) => { | ||
| try { | ||
| const userId = req.user.userId; | ||
| const { name } = req.body; | ||
|
|
||
| if (!name || !name.trim()) { | ||
| return res.status(400).json({ message: 'Name is required' }); | ||
| } | ||
|
|
||
| const user = await userServices.updateNameService(userId, name); | ||
|
|
||
| if (!user) { | ||
| return res.status(404).json({ message: 'User not found' }); | ||
| } | ||
|
|
||
| res.send(user); | ||
| } catch (error) { | ||
| res.status(500).json({ message: 'Internal server error' }); | ||
| } | ||
| }; | ||
|
|
||
| const updatePassword = async (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const { oldPassword, newPassword, confirmation } = req.body; | ||
| const userId = req.user.userId; | ||
|
|
||
| if (!oldPassword || !newPassword || !confirmation) { | ||
| return res.status(400).json({ message: 'All fields are required' }); | ||
| } | ||
|
|
||
| if (newPassword !== confirmation) { | ||
| return res.status(400).json({ message: 'Passwords do not match' }); | ||
| } | ||
|
|
||
| const user = await userServices.findUserById(userId); | ||
|
|
||
| if (!user) { | ||
| return res.status(404).json({ message: 'User not found' }); | ||
| } | ||
|
|
||
| const isValid = await bcrypt.compare(oldPassword, user.password); | ||
|
|
||
| if (!isValid) { | ||
| return res.status(401).json({ message: 'Old password is incorrect' }); | ||
| } | ||
|
|
||
| user.password = bcrypt.hashSync(newPassword, 10); | ||
| await user.save(); | ||
|
|
||
| res.send({ message: 'Password updated successfully' }); | ||
| } catch (error) { | ||
| res.status(500).json({ message: 'Internal server error' }); | ||
| } | ||
| }; | ||
|
|
||
| const updateEmail = async (req, res) => { | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try { | ||
| const { password, newEmail, confirmation } = req.body; | ||
| const userId = req.user.userId; | ||
|
|
||
| if (!password || !newEmail || !confirmation) { | ||
| return res | ||
| .status(400) | ||
| .json({ message: 'Password, new email and confirmation are required' }); | ||
| } | ||
|
|
||
| if (newEmail !== confirmation) { | ||
| return res.status(400).json({ message: 'Emails do not match' }); | ||
| } | ||
|
|
||
| const user = await userServices.findUserById(userId); | ||
|
|
||
| if (!user) { | ||
| return res.status(404).json({ message: 'User not found' }); | ||
| } | ||
|
|
||
| const isValidPassword = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isValidPassword) { | ||
| return res.status(401).json({ message: 'Invalid password' }); | ||
| } | ||
|
|
||
| const oldEmail = user.email; | ||
|
|
||
| user.email = newEmail; | ||
|
|
||
| await user.save(); | ||
| await emailServices.sendEmailChangedNotification(oldEmail, newEmail); | ||
|
|
||
| res.send({ message: 'Email updated successfully' }); | ||
| } catch (error) { | ||
| res.status(500).json({ message: 'Internal server error' }); | ||
| } | ||
| }; | ||
|
|
||
| export const userController = { | ||
| updateName, | ||
| updateEmail, | ||
| updatePassword, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,24 @@ | ||
| 'use strict'; | ||
| import express from 'express'; | ||
| import cookieParser from 'cookie-parser'; | ||
|
|
||
| import authRouter from './router/auth.router.js'; | ||
| import userRouter from './router/user.router.js'; | ||
| import { authMiddleware } from './middlewares/auth.middleware.js'; | ||
|
|
||
| const app = express(); | ||
| const port = process.env.PORT || 3000; | ||
|
|
||
| app.use(express.json()); | ||
| app.use(cookieParser()); | ||
|
|
||
| app.use('/', authRouter); | ||
| app.use('/user', authMiddleware, userRouter); | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| app.use('*', (req, res) => { | ||
| res.status(404).json({ message: 'Not found' }); | ||
| }); | ||
|
|
||
| app.listen(port, () => { | ||
| // eslint-disable-next-line no-console | ||
| console.log(`Server running at http://localhost:${port}`); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { jwtService } from '../services/jwt.services.js'; | ||
|
|
||
| export const authMiddleware = (req, res, next) => { | ||
| const auth = req.headers.authorization; | ||
|
|
||
| if (!auth) { | ||
| return res.sendStatus(401); | ||
| } | ||
|
|
||
| const [, token] = auth.split(' '); | ||
|
|
||
| const user = jwtService.verify(token); | ||
|
|
||
| if (!user) { | ||
| return res.sendStatus(401); | ||
| } | ||
|
|
||
| req.user = user; | ||
| next(); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { jwtService } from '../services/jwt.services.js'; | ||
|
|
||
| export const guestMiddleware = (req, res, next) => { | ||
| const auth = req.headers.authorization; | ||
|
|
||
| if (!auth) { | ||
| return next(); | ||
| } | ||
|
|
||
| const [, token] = auth.split(' '); | ||
| const user = jwtService.verify(token); | ||
|
|
||
| if (user) { | ||
| return res.redirect('/profile'); | ||
| } | ||
|
|
||
| next(); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { DataTypes } from 'sequelize'; | ||
| import client from '../util/db.js'; | ||
| import { User } from './User.model.js'; | ||
|
|
||
| export const Token = client.define( | ||
| 'Token', | ||
| { | ||
| refreshToken: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, | ||
| }, | ||
| { | ||
| tableName: 'tokens', | ||
| timestamps: true, | ||
| underscored: true, | ||
| }, | ||
| ); | ||
|
|
||
| Token.belongsTo(User, { foreignKey: 'user_id', onDelete: 'CASCADE' }); | ||
| User.hasOne(Token, { foreignKey: 'user_id' }); | ||
gmarchese93 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.