Registration and activation

This commit is contained in:
Torsten Schulz
2024-07-20 20:43:18 +02:00
parent 3880a265eb
commit bbf4a2deb3
51 changed files with 3016 additions and 69 deletions

View File

@@ -2,15 +2,19 @@ import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import chatRouter from './routers/chatRouter.js';
import bodyParser from 'body-parser';
import authRouter from './routers/authRouter.js';
import cors from 'cors';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(bodyParser.json());
app.use(cors());
app.use(express.json()); // To handle JSON request bodies
app.use('/api/chat', chatRouter);
app.use('/api/auth', authRouter);
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
app.get('*', (req, res) => {

View File

@@ -0,0 +1,23 @@
{
"development": {
"username": "yourpart",
"password": "hitomisan",
"database": "yp3",
"host": "127.0.0.1",
"dialect": "postgres"
},
"test": {
"username": "root",
"password": null,
"database": "database_test",
"host": "127.0.0.1",
"dialect": "mysql"
},
"production": {
"username": "root",
"password": null,
"database": "database_production",
"host": "127.0.0.1",
"dialect": "mysql"
}
}

View File

@@ -0,0 +1,97 @@
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import User from '../models/community/user.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
import { sendAccountActivationEmail, sendPasswordResetEmail } from '../services/emailService.js';
import i18n from '../utils/i18n.js';
const saltRounds = 10;
export const register = async (req, res) => {
const { email, username, password, language } = req.body;
try {
const hashedPassword = await bcrypt.hash(password, saltRounds);
const resetToken = uuidv4();
const user = await User.create({
email,
username,
password: hashedPassword,
resetToken: resetToken,
active: false,
registration_date: new Date()
});
const languageType = await UserParamType.findOne({ where: { description: 'language' } });
if (!languageType) {
return res.status(500).json({ error: 'Language type not found' });
}
console.log(user.id, languageType.id);
await UserParam.create({
userId: user.id,
paramTypeId: languageType.id,
value: language
});
const activationLink = `${process.env.FRONTEND_URL}/activate?token=${resetToken}`;
await sendAccountActivationEmail(email, activationLink, username, resetToken, language);
res.status(201).json({ id: user.hashedId, username: user.username, active: user.active });
} catch (error) {
console.log(error);
res.status(500).json({ error: error.message });
}
};
export const login = async (req, res) => {
const { email, password } = req.body;
try {
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ error: 'Invalid email or password' });
}
if (!user.active) {
return res.status(403).json({ error: 'Account not activated' });
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({ error: 'Invalid email or password' });
}
res.status(200).json({ id: user.hashed_id, username: user.username });
} catch (error) {
res.status(500).json({ error: 'Error logging in' });
}
};
export const forgotPassword = async (req, res) => {
const { email } = req.body;
try {
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(404).json({ error: 'Email not found' });
}
const resetToken = uuidv4();
const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${resetToken}`;
await user.update({ reset_token: resetToken });
const languageParam = await UserParam.findOne({ where: { user_id: user.id, param_type_id: languageType.id } });
const userLanguage = languageParam ? languageParam.value : 'en';
await sendPasswordResetEmail(email, resetLink, userLanguage);
res.status(200).json({ message: 'Password reset email sent' });
} catch (error) {
res.status(500).json({ error: 'Error processing forgot password' });
}
};
export const activateAccount = async (req, res) => {
const { token } = req.body;
try {
const user = await User.findOne({ where: { reset_token: token } });
if (!user) {
return res.status(404).json({ error: 'Invalid token' });
}
await user.update({ active: true, reset_token: null });
res.status(200).json({ message: 'Account activated' });
} catch (error) {
res.status(500).json({ error: 'Error activating account' });
}
};

6
backend/locales/de.json Normal file
View File

@@ -0,0 +1,6 @@
{
"account_activation_subject": "Aktivierung Deines yourPart-Zugangs",
"account_activation_html": "<p>Hallo {{username}},</p><p>Herzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:</p><p><a href='{{activationLink}}'>{{activationLink}}</a></p><p>Alternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:</p><p>{{resetToken}}</p><p>Dein yourPart-Team</p>",
"account_activation_text": "Hallo {{username}},\n\nHerzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:\n\n{{activationLink}}\n\nAlternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:\n\n{{resetToken}}\n\nDein yourPart-Team",
"welcome": "welcome"
}

6
backend/locales/en.json Normal file
View File

@@ -0,0 +1,6 @@
{
"account_activation_subject": "account_activation_subject",
"account_activation_text": "account_activation_text",
"welcome": "welcome",
"account_activation_html": "account_activation_html"
}

View File

@@ -0,0 +1,30 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
CREATE OR REPLACE FUNCTION community.update_hashed_id() RETURNS TRIGGER AS $$
BEGIN
NEW.hashed_id = encode(digest(NEW.id::text, 'sha256'), 'hex');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`);
await queryInterface.sequelize.query(`
CREATE TRIGGER update_hashed_id_trigger
BEFORE INSERT OR UPDATE ON community.user
FOR EACH ROW EXECUTE FUNCTION community.update_hashed_id();
`);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query(`
DROP TRIGGER IF EXISTS update_hashed_id_trigger ON community.user;
`);
await queryInterface.sequelize.query(`
DROP FUNCTION IF EXISTS community.update_hashed_id();
`);
}
};

View File

@@ -0,0 +1,49 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import bcrypt from 'bcrypt';
const User = sequelize.define('user', {
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
set(value) {
this.setDataValue('email', bcrypt.hashSync(value, 10));
}
},
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
password: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
this.setDataValue('password', bcrypt.hashSync(value, 10));
}
},
registrationDate: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
active: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
resetToken: {
type: DataTypes.UUID,
allowNull: true
},
hashedId: {
type: DataTypes.STRING,
allowNull: true
}
}, {
tableName: 'user',
schema: 'community',
underscored: true,
});
export default User;

View File

@@ -0,0 +1,37 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import User from './user.js';
import UserParamType from '../type/user_param.js';
const UserParam = sequelize.define('user_param', {
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id'
}
},
paramTypeId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: UserParamType,
key: 'id'
},
},
value: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'user_param',
schema: 'community',
underscored: true
});
UserParam.belongsTo(User, { foreignKey: 'userId' });
UserParam.belongsTo(UserParamType, { foreignKey: 'param_type_id' });
export default UserParam;

13
backend/models/index.js Normal file
View File

@@ -0,0 +1,13 @@
import User from './community/user.js';
import UserParam from './community/user_param.js';
import UserParamType from './type/user_param.js';
import Login from './logs/login.js';
const models = {
User,
UserParam,
UserParamType,
Login
};
export default models;

View File

@@ -0,0 +1,19 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const Login = sequelize.define('login', {
userId: {
type: DataTypes.INTEGER,
allowNull: false
},
timestamp: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
}, {
schema: 'logs',
tableName: 'login'
});
export default Login;

View File

@@ -0,0 +1,15 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
const UserParamType = sequelize.define('user_param_type', {
description: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'user_param',
schema: 'type',
underscored: true
});
export default UserParamType;

1998
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,20 @@
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.4",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"i18n": "^0.15.1",
"mysql2": "^3.10.3",
"nodemailer": "^6.9.14",
"pg": "^8.12.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.3",
"socket.io": "^4.7.5",
"uuid": "^10.0.0"
},
"devDependencies": {
"sequelize-cli": "^6.6.2"
}
}

View File

@@ -0,0 +1,11 @@
import { Router } from 'express';
import { register, login, forgotPassword, activateAccount } from '../controllers/authController.js';
const router = Router();
router.post('/register', register);
router.post('/login', login);
router.post('/forgot-password', forgotPassword);
router.post('/activate', activateAccount);
export default router;

View File

@@ -2,8 +2,7 @@ import http from 'http';
import { Server } from 'socket.io';
import amqp from 'amqplib/callback_api.js';
import app from './app.js';
import path from 'path';
import express from 'express';
import { syncDatabase } from './utils/syncDatabase.js';
const server = http.createServer(app);
const io = new Server(server);
@@ -11,14 +10,6 @@ const io = new Server(server);
const RABBITMQ_URL = 'amqp://localhost';
const QUEUE = 'chat_messages';
const __dirname = path.resolve();
const frontendPath = path.join(__dirname, 'path/to/your/frontend/build/folder');
app.use(express.static(frontendPath));
app.get('*', (req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'));
});
amqp.connect(RABBITMQ_URL, (err, connection) => {
if (err) {
throw err;
@@ -48,8 +39,14 @@ amqp.connect(RABBITMQ_URL, (err, connection) => {
});
});
server.listen(3001, () => {
console.log('Server is running on port 3001');
// Sync database before starting the server
syncDatabase().then(() => {
server.listen(3001, () => {
console.log('Server is running on port 3001');
});
}).catch(err => {
console.error('Failed to sync database:', err);
process.exit(1);
});
});
});

View File

@@ -0,0 +1,37 @@
import nodemailer from 'nodemailer';
import i18n from '../utils/i18n.js';
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
export const sendPasswordResetEmail = async (email, resetLink, language) => {
i18n.setLocale(language);
const mailOptions = {
from: process.env.SMTP_FROM,
to: email,
subject: i18n.__('password_reset_subject'),
text: i18n.__('password_reset_text', { resetLink })
};
await transporter.sendMail(mailOptions);
};
export const sendAccountActivationEmail = async (email, activationLink, username, resetToken, language) => {
i18n.setLocale(language);
const mailOptions = {
from: process.env.SMTP_FROM,
to: email,
subject: i18n.__('account_activation_subject'),
text: i18n.__('account_activation_text', { activationLink, username, resetToken }),
html: i18n.__('account_activation_html', { username, activationLink, resetToken })
};
await transporter.sendMail(mailOptions);
};

16
backend/utils/crypto.js Normal file
View File

@@ -0,0 +1,16 @@
import crypto from 'crypto';
const algorithm = 'aes-256-ctr';
const secretKey = process.env.SECRET_KEY;
export const encrypt = (text) => {
const cipher = crypto.createCipheriv(algorithm, secretKey, Buffer.alloc(16, 0));
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return encrypted.toString('hex');
};
export const decrypt = (hash) => {
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.alloc(16, 0));
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash, 'hex')), decipher.final()]);
return decrpyted.toString();
};

22
backend/utils/i18n.js Normal file
View File

@@ -0,0 +1,22 @@
import i18n from 'i18n';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
i18n.configure({
locales: ['en', 'de'],
directory: path.join(__dirname, '../locales'),
defaultLocale: 'de',
register: global,
autoReload: true,
syncFiles: true,
cookie: 'lang',
api: {
'__': 'translate',
'__n': 'translateN'
}
});
export default i18n;

View File

@@ -0,0 +1,10 @@
import UserParamType from '../models/type/user_param.js';
const initializeTypes = async () => {
await UserParamType.findOrCreate({
where: { description: 'language' },
defaults: { description: 'language' }
});
};
export default initializeTypes;

View File

@@ -0,0 +1,7 @@
{
"Password Reset": "Passwort zurücksetzen",
"Please click the following link to reset your password: {{resetLink}}": "Bitte klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen: {{resetLink}}",
"Account Activation": "Kontoaktivierung",
"Please click the following link to activate your account: {{activationLink}}": "Bitte klicken Sie auf den folgenden Link, um Ihr Konto zu aktivieren: {{activationLink}}",
"welcome": "Willkommen!"
}

View File

@@ -0,0 +1,7 @@
{
"Password Reset": "Password Reset",
"Please click the following link to reset your password: {{resetLink}}": "Please click the following link to reset your password: {{resetLink}}",
"Account Activation": "Account Activation",
"Please click the following link to activate your account: {{activationLink}}": "Please click the following link to activate your account: {{activationLink}}",
"welcome": "Welcome!"
}

View File

@@ -0,0 +1,26 @@
import { Sequelize } from 'sequelize';
import dotenv from 'dotenv';
dotenv.config();
const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASSWORD, {
host: process.env.DB_HOST,
dialect: 'postgres',
define: {
timestamps: false
}
});
const createSchemas = async () => {
await sequelize.query('CREATE SCHEMA IF NOT EXISTS community');
await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs');
await sequelize.query('CREATE SCHEMA IF NOT EXISTS type');
};
const initializeDatabase = async () => {
await createSchemas();
const models = await import('../models/index.js');
await sequelize.sync({ alter: true });
};
export { sequelize, initializeDatabase };

View File

@@ -0,0 +1,14 @@
import { initializeDatabase } from './sequelize.js';
import initializeTypes from './initializeTypes.js';
const syncDatabase = async () => {
try {
await initializeDatabase();
await initializeTypes();
console.log('All models were synchronized successfully.');
} catch (error) {
console.error('Unable to synchronize the database:', error);
}
};
export { syncDatabase };