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 };

View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"axios": "^1.7.2",
"dotenv": "^16.4.5",
"vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2",
"vue-router": "^4.0.13",
@@ -762,6 +763,15 @@
}
}
},
"node_modules/@vue/cli-service/node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/@vue/cli-shared-utils": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz",
@@ -2835,12 +2845,14 @@
}
},
"node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"dev": true,
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=10"
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {

View File

@@ -8,6 +8,7 @@
},
"dependencies": {
"axios": "^1.7.2",
"dotenv": "^16.4.5",
"vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2",
"vue-router": "^4.0.13",

View File

@@ -1,7 +1,7 @@
<template>
<div id="app">
<AppHeader />
<AppNavigation v-if="isLoggedIn" />
<AppNavigation v-if="isLoggedIn && user.active" />
<AppContent />
<AppFooter />
</div>
@@ -20,14 +20,18 @@ export default {
document.title = 'yourPart';
},
computed: {
...mapGetters(['isLoggedIn'])
...mapGetters(['isLoggedIn', 'user'])
},
components: {
AppHeader,
AppNavigation,
AppContent,
AppFooter
}
},
created() {
this.$store.dispatch('loadLoginState');
this.$i18n.locale = this.$store.getters.language;
},
};
</script>
@@ -37,5 +41,4 @@ export default {
flex-direction: column;
height: 100%;
}
</style>

View File

@@ -46,4 +46,9 @@ button:hover {
.rc-partner {
color: #0000ff;
font-weight: bold;
}
.link {
color: #F9A22C;
cursor: pointer;
}

View File

@@ -43,12 +43,13 @@ export default {
<style lang="scss" scoped>
@import '../assets/styles.scss';
nav {
nav > ul{
display: flex;
justify-content: space-between;
background-color: #343a40;
color: white;
padding: 10px;
flex-direction: row;
}
ul {
list-style-type: none;

View File

@@ -1,9 +1,10 @@
<template>
<div v-if="visible" :class="['dialog-overlay', { 'non-modal': !modal }]" @click.self="handleOverlayClick">
<div class="dialog" :class="{ minimized: minimized }" :style="{ width: dialogWidth, height: dialogHeight }" v-if="!minimized">
<div class="dialog" :class="{ minimized: minimized }" :style="{ width: dialogWidth, height: dialogHeight }"
v-if="!minimized">
<div class="dialog-header">
<span v-if="icon" class="dialog-icon">
<img :src="'/images/icons/' + icon" alt="Icon" />
<img :src="icon" alt="Icon" />
</span>
<span class="dialog-title">{{ isTitleTranslated ? $t(title) : title }}</span>
<span v-if="!modal" class="dialog-minimize" @click="minimize">_</span>
@@ -13,9 +14,8 @@
<slot></slot>
</div>
<div class="dialog-footer">
<button v-for="button in buttons" :key="button.text" @click="buttonClick(button.text)"
class="dialog-button">
{{ button.text }}
<button v-for="button in buttons" :key="button.text" @click="buttonClick(button.action)" class="dialog-button">
{{ isTitleTranslated ? $t(button.text) : button.text }}
</button>
</div>
</div>
@@ -40,7 +40,7 @@ export default {
},
buttons: {
type: Array,
default: () => [{ text: 'Ok' }]
default: () => [{ text: 'Ok', action: 'close' }]
},
modal: {
type: Boolean,
@@ -87,7 +87,7 @@ export default {
methods: {
open() {
this.visible = true;
if (!this.modal) {
if (this.modal === false) {
this.$store.dispatch('dialogs/addOpenDialog', {
status: 'open',
dialog: this
@@ -98,9 +98,11 @@ export default {
this.visible = false;
this.$store.dispatch('dialogs/removeOpenDialog', this.name);
},
buttonClick(buttonText) {
this.$emit(buttonText.toLowerCase());
this.close();
buttonClick(action) {
this.$emit(action);
if (action === 'close') {
this.close(); // Close dialog after button click if action is close
}
},
handleOverlayClick() {
if (!this.modal) {
@@ -186,7 +188,7 @@ export default {
.dialog-body {
flex-grow: 1;
padding: 10px;
padding: 20px;
overflow-y: auto;
}
@@ -197,4 +199,18 @@ export default {
border-top: 1px solid #ddd;
}
.dialog-button {
margin-left: 10px;
padding: 10px 20px;
cursor: pointer;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
transition: background 0.3s;
}
.dialog-button:hover {
background: #0056b3;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<DialogWidget ref="dialog" title="passwordReset.title" :show-close=true :buttons="buttons" @close="closeDialog" name="PasswordReset">
<div>
<label>{{ $t("passwordReset.email") }} <input type="email" v-model="email" required /></label>
</div>
</DialogWidget>
</template>
<script>
import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue';
export default {
name: 'PasswordResetDialog',
components: {
DialogWidget,
},
data() {
return {
email: '',
buttons: [{ text: this.$t("passwordReset.reset") }]
};
},
methods: {
open() {
this.$refs.dialog.open();
},
closeDialog() {
this.$refs.dialog.close();
},
async resetPassword() {
try {
await apiClient.post('/api/users/requestPasswordReset', {
email: this.email
});
this.$refs.dialog.close();
alert(this.$t("passwordReset.success"));
} catch (error) {
console.error('Error resetting password:', error);
alert(this.$t("passwordReset.failure"));
}
}
}
};
</script>

View File

@@ -0,0 +1,134 @@
<template>
<DialogWidget ref="dialog" title="register.title" :show-close="true" :buttons="buttons" :modal="true"
@close="closeDialog" @register="register" width="35em" height="33em" name="RegisterDialog"
:isTitleTranslated="true">
<div class="form-content">
<div>
<label>{{ $t("register.email") }}<input type="email" v-model="email" /></label>
</div>
<div>
<label>{{ $t("register.username") }}<input type="text" v-model="username" /></label>
</div>
<div>
<label>{{ $t("register.password") }}<input type="password" v-model="password" /></label>
</div>
<div>
<label>{{ $t("register.repeatPassword") }}<input type="password" v-model="repeatPassword" /></label>
</div>
<div>
<label>{{ $t("register.language") }}<select v-model="language">
<option value="en">{{ $t("register.languages.en") }}</option>
<option value="de">{{ $t("register.languages.de") }}</option>
</select></label>
</div>
</div>
<ErrorDialog ref="errorDialog" />
</DialogWidget>
</template>
<script>
import { mapActions } from 'vuex';
import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
export default {
name: 'RegisterDialog',
components: {
DialogWidget,
ErrorDialog,
},
data() {
return {
email: '',
username: '',
password: '',
repeatPassword: '',
language: this.getBrowserLanguage(),
buttons: [
{ text: 'register.close', action: 'close' },
{ text: 'register.register', action: 'register', disabled: !this.canRegister }
]
};
},
computed: {
canRegister() {
return this.password && this.repeatPassword && this.password === this.repeatPassword;
}
},
watch: {
canRegister(newValue) {
this.buttons[1].disabled = !newValue;
}
},
methods: {
...mapActions(['login']),
getBrowserLanguage() {
const browserLanguage = navigator.language || navigator.languages[0];
if (browserLanguage.startsWith('de')) {
return 'de';
} else {
return 'en';
}
},
open() {
this.$refs.dialog.open();
},
closeDialog() {
this.$refs.dialog.close();
},
async register() {
if (!this.canRegister) {
console.log('pw-fehler');
this.$refs.errorDialog.open('tr:register.passwordMismatch');
return;
}
try {
const response = await apiClient.post('/api/auth/register', {
email: this.email,
username: this.username,
password: this.password,
language: this.language
});
if (response.status === 201) {
console.log(response.data);
this.login(response.data);
this.$refs.dialog.close();
this.$router.push('/activate');
} else {
this.$refs.errorDialog.open("tr:register.failure");
}
} catch (error) {
if (error.response && error.response.status === 409) {
this.$refs.errorDialog.open('tr:register.' + error.response.data.error);
} else {
console.error('Error registering user:', error);
this.$refs.errorDialog.open('tr:register.' + error.response.data.error);
}
}
}
}
};
</script>
<style scoped>
.form-content>div {
margin-bottom: 1em;
}
label {
display: block;
margin-bottom: 0.5em;
}
input[type="email"],
input[type="text"],
input[type="password"],
select {
width: 100%;
padding: 0.5em;
box-sizing: border-box;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<DialogWidget ref="dialog" title="randomchat.title" icon="dice24.png" :show-close="true" :buttons="buttons"
:modal="false" :isTitleTranslated="true" @close="closeDialog">
<DialogWidget ref="dialog" title="randomchat.title" icon="dice24.png" :show-close=true :buttons="buttons"
:modal=false :isTitleTranslated=true @close="closeDialog" name="RandomChat">
<div v-if="chatIsRunning" class="randomchat">
<div class="headline">
{{ $t("randomchat.agerange") }}

View File

@@ -2,11 +2,11 @@
<DialogWidget
ref="dialog"
title="dataPrivacy.title"
isTitleTranslated=true
:isTitleTranslated=true
icon="privacy24.png"
:show-close="true"
:show-close=true
:buttons="[{ text: 'Ok' }]"
:modal="false"
:modal=false
@close="closeDialog"
@ok="handleOk"
>

View File

@@ -0,0 +1,52 @@
<template>
<DialogWidget ref="dialog" title="error.title" :show-close="true" :buttons="buttons" :modal="true" width="25em"
height="15em" name="ErrorDialog" :isTitleTranslated=true>
<div class="error-content">
<p>{{ translatedErrorMessage }}</p>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
export default {
name: 'ErrorDialog',
components: {
DialogWidget,
},
data() {
return {
errorMessage: '',
buttons: [
{ text: 'error.close', action: 'close' }
]
};
},
computed: {
translatedErrorMessage() {
if (this.errorMessage.startsWith('tr:')) {
return this.$t(this.errorMessage.substring(3));
}
return this.errorMessage;
}
},
methods: {
open(message) {
this.errorMessage = message;
this.$refs.dialog.open();
},
close() {
this.$refs.dialog.close();
}
}
};
</script>
<style scoped>
.error-content {
padding: 1em;
color: red;
text-align: center;
}
</style>

View File

@@ -2,11 +2,11 @@
<DialogWidget
ref="dialog"
title="imprint.title"
isTitleTranslated=true
:isTitleTranslated=true
icon="imprint24.png"
:show-close="true"
:buttons="[{ text: 'Ok' }]"
:modal="false"
:modal=false
@close="closeDialog"
@ok="handleOk"
>

View File

@@ -1,16 +1,23 @@
import { createI18n } from 'vue-i18n';
import store from '../store/index.js';
import enGeneral from './locales/en/general.json';
import enHeader from './locales/en/header.json';
import enNavigation from './locales/en/navigation.json';
import enHome from './locales/en/home.json';
import enChat from './locales/en/chat.json';
import enRegister from './locales/en/register.json';
import enError from './locales/en/error.json';
import enActivate from './locales/en/activate.json';
import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json';
import deNavigation from './locales/de/navigation.json';
import deHome from './locales/de/home.json';
import deChat from './locales/de/chat.json';
import deRegister from './locales/de/register.json';
import deError from './locales/de/error.json';
import deActivate from './locales/de/activate.json';
const messages = {
en: {
@@ -19,6 +26,9 @@ const messages = {
...enNavigation,
...enHome,
...enChat,
...enRegister,
...enError,
...enActivate,
},
de: {
...deGeneral,
@@ -26,11 +36,14 @@ const messages = {
...deNavigation,
...deHome,
...deChat,
...deRegister,
...deError,
...deActivate,
}
};
const i18n = createI18n({
locale: 'de',
locale: store.state.language,
fallbackLocale: 'de',
messages
});

View File

@@ -0,0 +1,9 @@
{
"activate": {
"title": "Zugang aktivieren",
"message": "Hallo {username}. Bitte gib hier den Code ein, den wir Dir per Email zugesendet haben.",
"token": "Token:",
"submit": "Absenden",
"failure": "Die Aktivierung war nicht erfolgreich."
}
}

View File

@@ -0,0 +1,6 @@
{
"error": {
"title": "Fehler aufgetreten",
"close": "Schließen"
}
}

View File

@@ -9,7 +9,9 @@
"name": "Login-Name",
"namedescription": "Gib hier Deinen Benutzernamen ein",
"password": "Paßwort",
"passworddescription": "Gib hier Dein Paßwort ein"
"passworddescription": "Gib hier Dein Paßwort ein",
"lostpassword": "Paßwort vergessen",
"register": "Bei yourPart registrieren"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"register": {
"title": "Bei yourPart registrieren",
"email": "Email-Adresse",
"username": "Benutzername",
"password": "Paßwort",
"repeatPassword": "Paßwort wiederholen",
"language": "Sprache",
"languages": {
"en": "Englisch",
"de": "Deutsch"
},
"register": "Registrieren",
"close": "Schließen",
"failure": "Es ist ein Fehler aufgetreten.",
"success": "Du wurdest erfolgreich registriert. Bitte schaue jetzt in Dein Email-Postfach zum aktivieren Deines Zugangs.",
"passwordMismatch": "Die Paßwörter stimmen nicht überein.",
"emailinuse": "Die Email-Adresse wird bereits verwendet.",
"usernameinuse": "Der Benutzername ist nicht verfügbar."
}
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -0,0 +1,5 @@
{
"error": {
}
}

View File

@@ -2,7 +2,7 @@
"home": {
"nologin": {
"welcome": "Welcome at yourPart",
"description": "<platzhalter>",
"description": "---platzhalter---",
"randomchat": "Random chat",
"startrandomchat": "Start random chat"
}

View File

@@ -0,0 +1,3 @@
{
}

View File

@@ -5,6 +5,18 @@ import router from './router';
import './assets/styles.scss';
import i18n from './i18n';
function getBrowserLanguage() {
const browserLanguage = navigator.language || navigator.languages[0];
console.log(browserLanguage);
if (browserLanguage.startsWith('de')) {
return 'de';
} else {
return 'en';
}
}
store.dispatch('setLanguage', getBrowserLanguage());
const app = createApp(App);
app.use(store);

View File

@@ -1,11 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import ActivateView from '../views/auth/ActivateView.vue';
const routes = [
{
path: '/',
name: 'Home',
component: HomeView
},
{
path: '/activate',
name: 'Activate page',
component: ActivateView
}
];
@@ -18,8 +24,10 @@ router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.getters.isLoggedIn) {
next('/');
} else {
} else if (!store.user.active) {
next();
} else {
next('/activate');
}
} else {
next();

View File

@@ -4,16 +4,17 @@ import dialogs from './modules/dialogs';
const store = createStore({
state: {
isLoggedIn: false,
user: null
user: null,
language: navigator.language.startsWith('de') ? 'de' : 'en',
},
mutations: {
login(state, user) {
dologin(state, user) {
state.isLoggedIn = true;
state.user = user;
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('user', JSON.stringify(user));
},
logout(state) {
dologout(state) {
state.isLoggedIn = false;
state.user = null;
localStorage.removeItem('isLoggedIn');
@@ -30,22 +31,29 @@ const store = createStore({
const user = userData;
state.isLoggedIn = isLoggedIn;
state.user = user;
},
setLanguage(state, language) {
state.language = language;
}
},
actions: {
login({ commit }, user) {
commit('login', user);
commit('dologin', user);
},
logout({ commit }) {
commit('logout');
commit('dologout');
},
loadLoginState({ commit }) {
commit('loadLoginState');
}
},
setLanguage({ commit }, language) {
commit('setLanguage', language);
},
},
getters: {
isLoggedIn: state => state.isLoggedIn,
user: state => state.user
user: state => state.user,
language: state => state.language,
},
modules: {
dialogs,

View File

@@ -0,0 +1,10 @@
import axios from 'axios';
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
headers: {
'Content-Type': 'application/json'
}
});
export default apiClient;

View File

@@ -0,0 +1,95 @@
<template>
<div class="activate-container">
<h1>{{ $t('activate.title') }}</h1>
<p v-if="user">{{ $t('activate.message', { username: user.username }) }}</p>
<form @submit.prevent="activateAccount">
<div>
<label>{{ $t('activate.token') }}</label>
<input type="text" v-model="token" required />
</div>
<div>
<button type="submit">{{ $t('activate.submit') }}</button>
</div>
</form>
<ErrorDialog ref="errorDialog" />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
export default {
name: 'ActivateView',
data() {
return {
token: this.$route.query.token || ''
};
},
components: {
ErrorDialog,
},
computed: {
...mapGetters(['user'])
},
methods: {
async activateAccount() {
try {
const response = await apiClient.post('/api/auth/activate', { token: this.token });
if (response.status === 200) {
this.user.active = true;
this.$router.push('/'); // Redirect to login after activation
}
} catch (error) {
console.error('Error activating account:', error);
this.$refs.errorDialog.open(this.$t('activate.failure'));
}
}
}
};
</script>
<style scoped>
.activate-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 2em;
box-sizing: border-box;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
label {
display: block;
margin-bottom: 0.5em;
}
input[type="text"] {
width: 100%;
padding: 0.5em;
margin-bottom: 1em;
box-sizing: border-box;
}
button {
padding: 0.5em 1em;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background-color: #0056b3;
}
</style>

View File

@@ -2,12 +2,22 @@
<div>
<h1>Welcome to Home (Logged In)</h1>
<p>Here are your exclusive features.</p>
<button @click="handleLogout">Logout</button>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
name: 'HomeLoggedInView',
methods: {
...mapActions(['logout']),
handleLogout() {
this.logout();
}
}
};
</script>

View File

@@ -12,18 +12,19 @@
<div>
<div>
<input data-object-name="user-name" size="20" type="text"
:placeholder="$t('home.nologin.login.name')" :title="$t('home.nologin.login.namedescription')">
:placeholder="$t('home.nologin.login.name')"
:title="$t('home.nologin.login.namedescription')">
</div>
<div>
<input data-object-name="password" size="20" type="password"
:placeholder="$t('home.nologin.login.password')" :title="$t('home.nologin.login.passworddescription')">
:placeholder="$t('home.nologin.login.password')"
:title="$t('home.nologin.login.passworddescription')">
</div>
<div>
<label id="o1p5irxv" name="o1p5irxv" class="Wt-valid" title=""><input id="ino1p5irxv"
data-object-name="remember-me" name="ino1p5irxv" type="checkbox"
onchange="var e=event||window.event,o=this;Wt._p_.update(o,'s53',e,true);"><span
id="to1p5irxv" name="to1p5irxv" style="white-space:normal;">Eingeloggt bleiben
(ACHTUNG!!! Dafür wird ein Cookie gesetzt!)</span></label>
id="to1p5irxv" name="to1p5irxv" style="white-space:normal;">Eingeloggt bleiben</span></label>
</div>
</div>
<div class="Wt-buttons">
@@ -32,30 +33,41 @@
class="Wt-btn with-label">Einloggen</button>
</div>
<div class="Wt-buttons">
<span id="o1p5iry0" data-object-name="lost-password"
onclick="var e=event||window.event,o=this;if(o.classList.contains('Wt-disabled')){Wt4_9_1.cancelEvent(e);return;}Wt._p_.update(o,'s57',e,true);">Ich
habe mein Paßwort vergessen</span> | <span id="o1p5iry1" data-object-name="register"
onclick="var e=event||window.event,o=this;if(o.classList.contains('Wt-disabled')){Wt4_9_1.cancelEvent(e);return;}Wt._p_.update(o,'s58',e,true);">Ich
möchte mich neu anmelden</span>
<span id="o1p5iry0" data-object-name="lost-password" @click="openPasswordResetDialog"
class="link">{{
$t('home.nologin.login.lostpassword') }}</span> | <span id="o1p5iry1"
@click="openRegisterDialog" class="link">{{ $t('home.nologin.login.register') }}</span>
</div>
</div>
</div>
<div class="mascot"><img src="/images/mascot/mascot_female.png" /></div>
<RandomChatDialog ref="randomChatDialog" />
<RegisterDialog ref="registerDialog" />
<PasswordResetDialog ref="passwordResetDialog" />
</div>
</template>
<script>
import RandomChatDialog from '@/dialogues/chat/RandomChatDialog.vue';
import RegisterDialog from '@/dialogues/auth/RegisterDialog.vue';
import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue';
export default {
export default {
name: 'HomeNoLoginView',
components: {
RandomChatDialog,
RegisterDialog,
PasswordResetDialog,
},
methods: {
openRandomChat() {
this.$refs.randomChatDialog.open();
},
openRegisterDialog() {
this.$refs.registerDialog.open();
},
openPasswordResetDialog() {
this.$refs.passwordResetDialog.open();
}
}
};
@@ -68,33 +80,38 @@ export default {
justify-content: center;
overflow: hidden;
gap: 2em;
height:100%;
height: 100%;
}
.home-structure > div {
.home-structure>div {
flex: 1;
text-align: center;
display: flex;
}
.mascot {
justify-content: center;
align-items: center;
background-color: #fdf1db;
}
.actions {
display: flex;
flex-direction: column;
gap: 2em;
}
.actions > div {
.actions>div {
flex: 1;
background-color: #fdf1db;
align-items: center;
justify-content:center;
justify-content: center;
display: flex;
color: #7E471B;
flex-direction: column;
}
.actions > div > h2 {
.actions>div>h2 {
color: #F9A22C;
}
</style>
</style>