Registration and activation
This commit is contained in:
@@ -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) => {
|
||||
|
||||
23
backend/config/config.json
Normal file
23
backend/config/config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
97
backend/controllers/authController.js
Normal file
97
backend/controllers/authController.js
Normal 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
6
backend/locales/de.json
Normal 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
6
backend/locales/en.json
Normal 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"
|
||||
}
|
||||
30
backend/migrations/add_trigger_for_hashedId.cjs
Normal file
30
backend/migrations/add_trigger_for_hashedId.cjs
Normal 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();
|
||||
`);
|
||||
}
|
||||
};
|
||||
49
backend/models/community/user.js
Normal file
49
backend/models/community/user.js
Normal 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;
|
||||
37
backend/models/community/user_param.js
Normal file
37
backend/models/community/user_param.js
Normal 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
13
backend/models/index.js
Normal 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;
|
||||
19
backend/models/logs/login.js
Normal file
19
backend/models/logs/login.js
Normal 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;
|
||||
15
backend/models/type/user_param.js
Normal file
15
backend/models/type/user_param.js
Normal 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
1998
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
11
backend/routers/authRouter.js
Normal file
11
backend/routers/authRouter.js
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
37
backend/services/emailService.js
Normal file
37
backend/services/emailService.js
Normal 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
16
backend/utils/crypto.js
Normal 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
22
backend/utils/i18n.js
Normal 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;
|
||||
10
backend/utils/initializeTypes.js
Normal file
10
backend/utils/initializeTypes.js
Normal 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;
|
||||
7
backend/utils/locales/de.json
Normal file
7
backend/utils/locales/de.json
Normal 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!"
|
||||
}
|
||||
7
backend/utils/locales/en.json
Normal file
7
backend/utils/locales/en.json
Normal 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!"
|
||||
}
|
||||
26
backend/utils/sequelize.js
Normal file
26
backend/utils/sequelize.js
Normal 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 };
|
||||
14
backend/utils/syncDatabase.js
Normal file
14
backend/utils/syncDatabase.js
Normal 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 };
|
||||
Reference in New Issue
Block a user