Compare commits

..

9 Commits

Author SHA1 Message Date
Alex
c42adc858f added password reset system 2025-02-26 21:45:16 +01:00
Alex
7c01b77445 backend: moved to correct plaintext mail parsing 2025-02-26 21:44:24 +01:00
Alex
a2886fc1e0 backend changed verification model 2025-02-26 21:42:49 +01:00
Alex
6b408d64a7 frontend: made message i18n compliant 2025-02-26 21:41:34 +01:00
Alex
3eeb35c768 frontend: fix missing usereditform role_id info 2025-02-26 21:41:00 +01:00
Alex
b7682f8dc3 moved to put from patch 2025-02-26 21:40:32 +01:00
Alex
dde3b3d47b frontend: fix validation 2025-02-26 21:39:42 +01:00
Alex
c607622185 nomenclature
nomenclature
2025-02-26 21:39:14 +01:00
Alex
2866917aef locale 2025-02-26 21:36:57 +01:00
39 changed files with 979 additions and 353 deletions

View File

@@ -65,46 +65,40 @@
*/ */
function validateField(name, value, required) { function validateField(name, value, required) {
if (value === null || (typeof value === 'string' && !value.trim() && !required)) return null; if (value === null || (typeof value === 'string' && !value.trim() && !required)) return null;
switch (name) { if (name.includes('membership_start_date')) {
case 'membership_start_date': return typeof value === 'string' && value.trim() ? null : $t('validation.date');
return typeof value === 'string' && value.trim() ? null : $t('validation.date'); } else if (name.includes('email')) {
case 'email': return typeof value === 'string' && /^\S+@\S+\.\S+$/.test(value)
return typeof value === 'string' && /^\S+@\S+\.\S+$/.test(value) ? null
? null : $t('validation.email');
: $t('validation.email'); } else if (name.includes('password')) {
case 'password': if (typeof value === 'string' && value.length < 8) {
case 'password2': return $t('validation.password');
if (typeof value === 'string' && value.length < 8) { }
return $t('validation.password'); if (otherPasswordValue && value !== otherPasswordValue) {
} return $t('validation.password_match');
if (otherPasswordValue && value !== otherPasswordValue) { }
return $t('validation.password_match'); return null;
} } else if (name.includes('phone')) {
return null; return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value)
case 'phone': ? null
return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value) : $t('validation.phone');
? null } else if (name.includes('zip_code')) {
: $t('validation.phone'); return typeof value === 'string' && /^\d{5}$/.test(value) ? null : $t('validation.zip_code');
case 'zip_code': } else if (name.includes('iban')) {
return typeof value === 'string' && /^\d{5}$/.test(value) return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
? null ? null
: $t('validation.zip_code'); : $t('validation.iban');
case 'iban': } else if (name.includes('bic')) {
return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value) return typeof value === 'string' && /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
? null ? null
: $t('validation.iban'); : $t('validation.bic');
case 'bic': } else if (name.includes('licence_number')) {
return typeof value === 'string' && return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value) } else {
? null return typeof value === 'string' && !value.trim() && required
: $t('validation.bic'); ? $t('validation.required')
case 'licence_number': : null;
return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
default:
return typeof value === 'string' && !value.trim() && required
? $t('validation.required')
: null;
} }
} }

View File

@@ -127,7 +127,7 @@
let isUpdating = false, let isUpdating = false,
password = '', password = '',
password2 = ''; confirm_password = '';
/** @type {Object.<string, App.Locals['licence_categories']>} */ /** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = groupCategories(licence_categories); $: groupedCategories = groupCategories(licence_categories);
@@ -249,14 +249,14 @@
label={$t('password')} label={$t('password')}
placeholder={$t('placeholder.password')} placeholder={$t('placeholder.password')}
bind:value={password} bind:value={password}
otherPasswordValue={password2} otherPasswordValue={confirm_password}
/> />
<InputField <InputField
name="password2" name="confirm_password"
type="password" type="password"
label={$t('password_repeat')} label={$t('confirm_password')}
placeholder={$t('placeholder.password')} placeholder={$t('placeholder.password')}
bind:value={password2} bind:value={confirm_password}
otherPasswordValue={password} otherPasswordValue={password}
/> />
<InputField <InputField
@@ -269,7 +269,7 @@
/> />
<InputField <InputField
name="user[last_name]" name="user[last_name]"
label={$t('last_name')} label={$t('user.last_name')}
bind:value={localUser.last_name} bind:value={localUser.last_name}
placeholder={$t('placeholder.last_name')} placeholder={$t('placeholder.last_name')}
required={true} required={true}
@@ -292,14 +292,14 @@
<InputField <InputField
name="user[phone]" name="user[phone]"
type="tel" type="tel"
label={$t('phone')} label={$t('user.phone')}
bind:value={localUser.phone} bind:value={localUser.phone}
placeholder={$t('placeholder.phone')} placeholder={$t('placeholder.phone')}
/> />
<InputField <InputField
name="user[dateofbirth]" name="user[dateofbirth]"
type="date" type="date"
label={$t('dateofbirth')} label={$t('user.dateofbirth')}
bind:value={localUser.dateofbirth} bind:value={localUser.dateofbirth}
placeholder={$t('placeholder.dateofbirth')} placeholder={$t('placeholder.dateofbirth')}
readonly={role_id === 0} readonly={role_id === 0}

View File

@@ -1,10 +1,10 @@
export default { export default {
userStatus: { userStatus: {
1: 'Nicht verifiziert', 1: 'Nicht verifiziert',
2: 'Verifiziert', 2: 'Deaktiviert',
3: 'Aktiv', 3: 'Verifiziert',
4: 'Passiv', 4: 'Aktiv',
5: 'Deaktiviert' 5: 'Passiv'
}, },
userRole: { userRole: {
0: 'Mitglied', 0: 'Mitglied',
@@ -51,6 +51,7 @@ export default {
licence: 'Nummer zu kurz(11 Zeichen)' licence: 'Nummer zu kurz(11 Zeichen)'
}, },
server: { server: {
general: 'Allgemein',
error: { error: {
invalid_json: 'JSON Daten sind ungültig', invalid_json: 'JSON Daten sind ungültig',
no_auth_token: 'Nicht authorisiert, fehlender oder ungültiger Auth-Token', no_auth_token: 'Nicht authorisiert, fehlender oder ungültiger Auth-Token',
@@ -68,6 +69,7 @@ export default {
invalid_user_data: 'Nutzerdaten ungültig', invalid_user_data: 'Nutzerdaten ungültig',
user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort', user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.', email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.',
password_already_changed: 'Das Passwort wurde schon geändert.',
alphanumunicode: 'beinhaltet nicht erlaubte Zeichen', alphanumunicode: 'beinhaltet nicht erlaubte Zeichen',
safe_content: 'I see what you did there! Do not cross this line!', safe_content: 'I see what you did there! Do not cross this line!',
iban: 'Ungültig. Format: DE07123412341234123412', iban: 'Ungültig. Format: DE07123412341234123412',
@@ -80,6 +82,7 @@ export default {
required: 'Feld wird benötigt', required: 'Feld wird benötigt',
image: 'Dies ist kein Bild', image: 'Dies ist kein Bild',
alphanum: 'beinhaltet ungültige Zeichen', alphanum: 'beinhaltet ungültige Zeichen',
user_disabled: 'Benutzer ist deaktiviert',
alphaunicode: 'darf nur aus Buchstaben bestehen' alphaunicode: 'darf nur aus Buchstaben bestehen'
} }
}, },
@@ -109,7 +112,10 @@ export default {
user: 'Nutzer', user: 'Nutzer',
management: 'Mitgliederverwaltung', management: 'Mitgliederverwaltung',
id: 'Mitgliedsnr', id: 'Mitgliedsnr',
name: 'Name', first_name: 'Vorname',
last_name: 'Nachname',
phone: 'Telefonnummer',
dateofbirth: 'Geburtstag',
email: 'Email', email: 'Email',
status: 'Status', status: 'Status',
role: 'Nutzerrolle' role: 'Nutzerrolle'
@@ -140,6 +146,7 @@ export default {
edit: 'Bearbeiten', edit: 'Bearbeiten',
delete: 'Löschen', delete: 'Löschen',
search: 'Suche:', search: 'Suche:',
name: 'Name',
mandate_date_signed: 'Mandatserteilungsdatum', mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen', licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell', subscription_model: 'Mitgliedschatfsmodell',
@@ -156,16 +163,16 @@ export default {
zip_code: 'PLZ', zip_code: 'PLZ',
forgot_password: 'Passwort vergessen?', forgot_password: 'Passwort vergessen?',
password: 'Passwort', password: 'Passwort',
password_repeat: 'Passwort wiederholen', confirm_password: 'Passwort wiederholen',
password_changed: 'Passwort wurde erfolgreich geändert.',
change_password: 'Passwort ändern',
password_change_requested:
'Passwortänderungsanfrage wurde gesendet.. Bitte überprüfen Sie Ihr Postfach.',
company: 'Firma', company: 'Firma',
login: 'Anmeldung', login: 'Anmeldung',
profile: 'Profil', profile: 'Profil',
membership: 'Mitgliedschaft', membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten', bankaccount: 'Kontodaten',
first_name: 'Vorname',
last_name: 'Nachname',
phone: 'Telefonnummer',
dateofbirth: 'Geburtstag',
status: 'Status', status: 'Status',
start: 'Beginn', start: 'Beginn',
end: 'Ende', end: 'Ende',
@@ -177,7 +184,7 @@ export default {
mandate_reference: 'SEPA Mandat', mandate_reference: 'SEPA Mandat',
payments: 'Zahlungen', payments: 'Zahlungen',
add_new: 'Neu', add_new: 'Neu',
email_sent: 'Email wurde gesendet..',
// For payments section // For payments section
payment: { payment: {
id: 'Zahlungs-Nr', id: 'Zahlungs-Nr',
@@ -185,7 +192,6 @@ export default {
date: 'Datum', date: 'Datum',
status: 'Status' status: 'Status'
}, },
// For subscription statuses // For subscription statuses
subscriptionStatus: { subscriptionStatus: {
pending: 'Ausstehend', pending: 'Ausstehend',

View File

@@ -3,18 +3,18 @@ import { toRFC3339 } from './helpers';
/** /**
* Converts FormData to a nested object structure * Converts FormData to a nested object structure
* @param {FormData} formData - The FormData object to convert * @param {FormData} formData - The FormData object to convert
* @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']>, password2: string }} Nested object representation of the form data * @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']>, confirm_password: string }} Nested object representation of the form data
*/ */
export function formDataToObject(formData) { export function formDataToObject(formData) {
/** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> } */ /** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> } */
const object = {}; const object = {};
let password2 = ''; let confirm_password = '';
console.log('Form data entries:'); console.log('Form data entries:');
for (const [key, value] of formData.entries()) { for (const [key, value] of formData.entries()) {
console.log('Key:', key, 'Value:', value); console.log('Key:', key, 'Value:', value);
if (key == 'password2') { if (key == 'confirm_password') {
password2 = String(value); confirm_password = String(value);
continue; continue;
} }
/** @type {string[]} */ /** @type {string[]} */
@@ -56,12 +56,12 @@ export function formDataToObject(formData) {
} }
} }
return { object: object, password2: password2 }; return { object: object, confirm_password: confirm_password };
} }
/** /**
* Processes the raw form data into the expected user data structure * Processes the raw form data into the expected user data structure
* @param {{ object: Partial<App.Locals['user']>, password2: string} } rawData - The raw form data object * @param {{ object: Partial<App.Locals['user']>, confirm_password: string} } rawData - The raw form data object
* @returns {{ user: Partial<App.Locals['user']> }} Processed user data * @returns {{ user: Partial<App.Locals['user']> }} Processed user data
*/ */
export function processUserFormData(rawData) { export function processUserFormData(rawData) {
@@ -130,8 +130,8 @@ export function processUserFormData(rawData) {
console.dir(rawData.object.licence); console.dir(rawData.object.licence);
if ( if (
rawData.object.password && rawData.object.password &&
rawData.password2 && rawData.confirm_password &&
rawData.object.password === rawData.password2 && rawData.object.password === rawData.confirm_password &&
rawData.object.password.trim() !== '' rawData.object.password.trim() !== ''
) { ) {
processedData.user.password = rawData.object.password; processedData.user.password = rawData.object.password;

View File

@@ -38,7 +38,7 @@ export const actions = {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestUpdateOptions = { const requestUpdateOptions = {
method: 'PATCH', method: 'PUT',
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -45,7 +45,7 @@ export const actions = {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestOptions = { const requestOptions = {
method: isCreating ? 'POST' : 'PATCH', method: isCreating ? 'POST' : 'PUT',
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -89,7 +89,7 @@ export const actions = {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestOptions = { const requestOptions = {
method: isCreating ? 'POST' : 'PATCH', method: isCreating ? 'POST' : 'PUT',
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@@ -210,7 +210,7 @@
<td>{user.id}</td> <td>{user.id}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('user.name')}</th> <th>{$t('name')}</th>
<td>{user.first_name} {user.last_name}</td> <td>{user.first_name} {user.last_name}</td>
</tr> </tr>
<tr> <tr>
@@ -415,6 +415,7 @@
<Modal on:close={close}> <Modal on:close={close}>
<UserEditForm <UserEditForm
{form} {form}
role_id={user.role_id}
user={selectedUser} user={selectedUser}
{subscriptions} {subscriptions}
{licence_categories} {licence_categories}

View File

@@ -0,0 +1,17 @@
<script>
import { page } from '$app/state';
import { t } from 'svelte-i18n';
let message = '';
if (page.url.search) {
message = page.url.search.split('=')[1].replaceAll('%20', ' ');
}
</script>
<div class="container">
<div class="content">
<h1 class="step-title title">{$t('email_sent')}</h1>
<h4 class="step-subtitle normal">
{$t(message)}
</h4>
</div>
</div>

View File

@@ -36,7 +36,7 @@
{/if} {/if}
{#if message} {#if message}
<h4 class="step-subtitle">{message}</h4> <h4 class="step-subtitle">{$t(message)}</h4>
{/if} {/if}
<input type="hidden" name="next" value={$page.url.searchParams.get('next')} /> <input type="hidden" name="next" value={$page.url.searchParams.get('next')} />
@@ -53,7 +53,7 @@
name="password" name="password"
placeholder={$t('placeholder.password')} placeholder={$t('placeholder.password')}
/> />
<a href="/auth/password/request-change" class="forgot-password">{$t('forgot_password')}?</a> <a href="/auth/password/change" class="forgot-password">{$t('forgot_password')}?</a>
</div> </div>
</div> </div>
<div class="btn-container"> <div class="btn-container">

View File

@@ -0,0 +1,33 @@
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ fetch, request }) => {
const formData = await request.formData();
const email = String(formData.get('email'));
/** @type {RequestInit} */
const requestInitOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: email })
};
const res = await fetch(`${BASE_API_URI}/users/password/request-change/`, requestInitOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
// redirect the user
throw redirect(302, `/auth/confirming?message=${response.message}`);
}
};

View File

@@ -0,0 +1,45 @@
<script>
import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
/** @type {import('./$types').ActionData} */
export let form;
/** @type {import('./$types').SubmitFunction} */
const handleRequestChange = async () => {
return async ({ result }) => {
await applyAction(result);
};
};
</script>
<div class="container">
<form class="content" method="POST" use:enhance={handleRequestChange}>
<h1 class="step-title">{$t('forgot_password')}</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive={{ key: error.id }}
out:send={{ key: error.id }}
>
{$t(error.key)}
</h4>
{/each}
{/if}
<div class="input-box">
<span class="label">{$t('user.email')}:</span>
<input
class="input"
type="email"
name="email"
id="email"
placeholder={$t('placeholder.email')}
required
/>
</div>
<button class="button-dark">{$t('confirm')}</button>
</form>
</div>

View File

@@ -0,0 +1,49 @@
import { BASE_API_URI } from '$lib/utils/constants';
import { formatError } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ fetch, request }) => {
const formData = await request.formData();
const password = String(formData.get('user[password]')).trim();
const confirmPassword = String(formData.get('confirm_password')).trim();
let token = String(formData.get('token'));
const userID = String(formData.get('user_id'));
// Some validations
/** @type {string | Array<{field: string, key: string}> | Record<string, {key: string}>} */
const fieldsError = [];
if (password.length < 8) {
fieldsError.push({ field: 'user.user', key: 'validation.password' });
}
if (confirmPassword !== password) {
fieldsError.push({ field: 'user.user', key: 'validation.password_match' });
}
if (Object.keys(fieldsError).length > 0) {
return fail(400, { errors: formatError(fieldsError) });
}
/** @type {RequestInit} */
const requestInitOptions = {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: token, password: password })
};
const res = await fetch(`${BASE_API_URI}/users/password/change/${userID}/`, requestInitOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
// redirect the user
throw redirect(302, `/auth/login?message=${response.message}`);
}
};

View File

@@ -0,0 +1,76 @@
<script>
import { applyAction, enhance } from '$app/forms';
import { page } from '$app/state';
import { receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import InputField from '$lib/components/InputField.svelte';
import { onMount } from 'svelte';
let password = '',
confirm_password = '';
/** @type{string | null} */
let token = null;
onMount(() => {
token = page.url.searchParams.get('token');
console.log(token);
if (!token) {
form ||= { errors: [] }; // Ensure form exists with an errors array
form.errors.push({
field: 'server.general',
key: 'server.error.no_auth_token',
id: Math.random() * 1000
});
}
});
/** @type {import('./$types').ActionData} */
export let form;
/** @type {import('./$types').SubmitFunction} */
const handleChange = async () => {
return async ({ result }) => {
await applyAction(result);
};
};
</script>
<div class="container">
<form class="content" method="POST" use:enhance={handleChange}>
<h1 class="step-title title">{$t('change_password')}</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive={{ key: error.id }}
out:send={{ key: error.id }}
>
{$t(error.key)}
</h4>
{/each}
{/if}
<input type="hidden" name="user_id" value={page.params.id} />
<input type="hidden" name="token" value={token} />
<InputField
name="user[password]"
type="password"
label={$t('password')}
placeholder={$t('placeholder.password')}
bind:value={password}
otherPasswordValue={confirm_password}
/>
<InputField
name="confirm_password"
type="password"
label={$t('confirm_password')}
placeholder={$t('placeholder.password')}
bind:value={confirm_password}
otherPasswordValue={password}
/>
<button class="button-dark">{$t('change_password')} </button>
</form>
</div>

View File

@@ -2,17 +2,18 @@ package constants
const ( const (
UnverifiedStatus = iota + 1 UnverifiedStatus = iota + 1
DisabledStatus
VerifiedStatus VerifiedStatus
ActiveStatus ActiveStatus
PassiveStatus PassiveStatus
DisabledStatus
DelayedPaymentStatus DelayedPaymentStatus
SettledPaymentStatus SettledPaymentStatus
AwaitingPaymentStatus AwaitingPaymentStatus
MailVerificationSubject = "Nur noch ein kleiner Schritt!" MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailRegistrationSubject = "Neues Mitglied hat sich registriert" MailChangePasswordSubject = "Passwort Änderung angefordert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V." MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailContactSubject = "Jemand hat das Kontaktformular gefunden" MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
) )
var Roles = struct { var Roles = struct {
@@ -63,6 +64,14 @@ var Licences = struct {
T: "T", T: "T",
} }
var VerificationTypes = struct {
Email string
Password string
}{
Email: "email",
Password: "password",
}
var Priviliges = struct { var Priviliges = struct {
View int8 View int8
Create int8 Create int8
@@ -75,11 +84,6 @@ var Priviliges = struct {
Delete: 30, Delete: 30,
} }
const PRIV_VIEW = 1
const PRIV_ADD = 2
const PRIV_EDIT = 4
const PRIV_DELETE = 8
var MemberUpdateFields = map[string]bool{ var MemberUpdateFields = map[string]bool{
"Email": true, "Email": true,
"Phone": true, "Phone": true,

View File

@@ -0,0 +1,103 @@
package controllers
import (
"GoMembership/internal/constants"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) {
// Expected data from the user
var input struct {
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
// find user
db_user, err := uc.Service.GetUserByEmail(input.Email)
if err != nil {
utils.RespondWithError(c, err, "couldn't get user by email", http.StatusNotFound, "user.user", "user.email")
return
}
// check if user may change the password
if db_user.Status <= constants.DisabledStatus {
utils.RespondWithError(c, errors.ErrNotAuthorized, "User password change request denied, user is disabled", http.StatusForbidden, errors.Responses.Fields.Login, errors.Responses.Keys.UserDisabled)
return
}
// create token
token, err := uc.Service.HandlePasswordChangeRequest(db_user)
if err != nil {
utils.RespondWithError(c, err, "couldn't handle password change request", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
// send email
if err := uc.EmailService.SendChangePasswordEmail(db_user, &token); err != nil {
utils.RespondWithError(c, err, "Couldn't send change password email", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusAccepted, gin.H{
"message": "password_change_requested",
})
}
func (uc *UserController) ChangePassword(c *gin.Context) {
// Expected data from the user
var input struct {
Password string `json:"password" binding:"required"`
Token string `json:"token" binding:"required"`
}
userIDint, err := strconv.Atoi(c.Param("id"))
if err != nil {
utils.RespondWithError(c, err, "Invalid user ID", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.InvalidUserID)
return
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
verification, err := uc.Service.VerifyUser(&input.Token, &constants.VerificationTypes.Password)
if err != nil || uint(userIDint) != verification.UserID {
if err == errors.ErrAlreadyVerified {
utils.RespondWithError(c, err, "User already verified", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.PasswordAlreadyChanged)
} else {
utils.RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
}
return
}
user, err := uc.Service.GetUserByID(verification.UserID)
if err != nil {
utils.RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.UserNotFoundWrongPassword)
return
}
user.Status = constants.ActiveStatus
user.Verification = *verification
user.ID = verification.UserID
user.Password = input.Password
_, err = uc.Service.UpdateUser(user)
if err != nil {
utils.RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "password_changed",
})
}

View File

@@ -204,7 +204,7 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
utils.RespondWithError(c, err, "Error in LoginHandler", http.StatusBadRequest, "general", "server.validation.invalid_json") utils.RespondWithError(c, err, "Invalid JSON or malformed request", http.StatusBadRequest, errors.Responses.Fields.General, errors.Responses.Keys.Invalid)
return return
} }
@@ -216,9 +216,18 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
return return
} }
if user.Status <= constants.DisabledStatus {
utils.RespondWithError(c, fmt.Errorf("User banned from login %v %v", user.FirstName, user.LastName),
"Login Error; user is disabled",
http.StatusNotAcceptable,
errors.Responses.Fields.Login,
errors.Responses.Keys.UserDisabled)
return
}
ok, err := user.PasswordMatches(input.Password) ok, err := user.PasswordMatches(input.Password)
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Login Error; password comparisson failed", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError) utils.RespondWithError(c, err, "Login Error; password comparisson failed", http.StatusInternalServerError, errors.Responses.Fields.Login, errors.Responses.Keys.InternalServerError)
return return
} }
if !ok { if !ok {
@@ -233,7 +242,7 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
logger.Error.Printf("jwtsecret: %v", config.Auth.JWTSecret) logger.Error.Printf("jwtsecret: %v", config.Auth.JWTSecret)
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "") token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Error generating token in LoginHandler", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.JwtGenerationFailed) utils.RespondWithError(c, err, "Error generating token in LoginHandler", http.StatusInternalServerError, errors.Responses.Fields.Login, errors.Responses.Keys.JwtGenerationFailed)
return return
} }
@@ -331,15 +340,26 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) {
return return
} }
user, err := uc.Service.VerifyUser(&token) verification, err := uc.Service.VerifyUser(&token, &constants.VerificationTypes.Email)
if err != nil { if err != nil {
logger.Error.Printf("Cannot verify user: %v", err) logger.Error.Printf("Cannot verify user: %v", err)
c.HTML(http.StatusUnauthorized, "verification_error.html", gin.H{"ErrorMessage": "Emailadresse wurde schon bestätigt. Sollte dies nicht der Fall sein, wende Dich bitte an info@carsharing-hasloh.de."}) c.HTML(http.StatusUnauthorized, "verification_error.html", gin.H{"ErrorMessage": "Emailadresse wurde schon bestätigt. Sollte dies nicht der Fall sein, wende Dich bitte an info@carsharing-hasloh.de."})
return return
} }
logger.Info.Printf("VerificationMailHandler User: %#v", user.Email)
user, err := uc.Service.GetUserByID(verification.UserID)
if err != nil {
utils.RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.UserNotFoundWrongPassword)
return
}
user.Status = constants.ActiveStatus
user.Verification = *verification
user.ID = verification.UserID
uc.Service.UpdateUser(user)
logger.Info.Printf("Verified User: %#v", user.Email)
uc.EmailService.SendWelcomeEmail(user) uc.EmailService.SendWelcomeEmail(user)
c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName}) c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName})
} }

View File

@@ -13,7 +13,7 @@ func CORSMiddleware() gin.HandlerFunc {
logger.Info.Print("Applying CORS") logger.Info.Print("Applying CORS")
return cors.New(cors.Config{ return cors.New(cors.Config{
AllowOrigins: strings.Split(config.Site.AllowOrigins, ","), AllowOrigins: strings.Split(config.Site.AllowOrigins, ","),
AllowMethods: []string{"GET", "POST", "PATCH", "OPTIONS"}, // "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowMethods: []string{"GET", "POST", "PATCH", "PUT"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "X-CSRF-Token"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With", "X-CSRF-Token"},
ExposeHeaders: []string{"Content-Length"}, ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true, AllowCredentials: true,

View File

@@ -5,9 +5,9 @@ import "time"
type Verification struct { type Verification struct {
UpdatedAt time.Time UpdatedAt time.Time
CreatedAt time.Time CreatedAt time.Time
EmailVerifiedAt *time.Time `gorm:"Default:NULL" json:"email_verified_at"` VerifiedAt *time.Time `gorm:"Default:NULL" json:"verified_at"`
IDVerifiedAt *time.Time `gorm:"Default:NULL" json:"id_verified_at"`
VerificationToken string `json:"token"` VerificationToken string `json:"token"`
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
UserID uint `gorm:"unique;" json:"user_id"` UserID uint `gorm:"unique;" json:"user_id"`
Type string
} }

View File

@@ -0,0 +1,10 @@
package repositories
import (
"GoMembership/internal/database"
"GoMembership/internal/models"
)
func (r *UserRepository) SetUserStatus(id uint, status uint) error {
return database.DB.Model(&models.User{}).Where("id = ?", id).Update("status", status).Error
}

View File

@@ -3,7 +3,6 @@ package repositories
import ( import (
"gorm.io/gorm" "gorm.io/gorm"
"GoMembership/internal/constants"
"GoMembership/internal/database" "GoMembership/internal/database"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -18,10 +17,12 @@ type UserRepositoryInterface interface {
UpdateUser(user *models.User) (*models.User, error) UpdateUser(user *models.User) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error) GetUsers(where map[string]interface{}) (*[]models.User, error)
GetUserByEmail(email string) (*models.User, error) GetUserByEmail(email string) (*models.User, error)
SetVerificationToken(verification *models.Verification) (uint, error)
IsVerified(userID *uint) (bool, error) IsVerified(userID *uint) (bool, error)
GetVerificationOfToken(token *string) (*models.Verification, error) GetVerificationOfToken(token *string, verificationType *string) (*models.Verification, error)
SetVerificationToken(verification *models.Verification) (token string, err error)
DeleteVerification(id uint, verificationType string) error
DeleteUser(id uint) error DeleteUser(id uint) error
SetUserStatus(id uint, status uint) error
} }
type UserRepository struct{} type UserRepository struct{}
@@ -156,42 +157,3 @@ func (ur *UserRepository) GetUserByEmail(email string) (*models.User, error) {
} }
return &user, nil return &user, nil
} }
func (ur *UserRepository) IsVerified(userID *uint) (bool, error) {
var user models.User
result := database.DB.Select("status").First(&user, userID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return false, gorm.ErrRecordNotFound
}
return false, result.Error
}
return user.Status != constants.UnverifiedStatus, nil
}
func (ur *UserRepository) GetVerificationOfToken(token *string) (*models.Verification, error) {
var emailVerification models.Verification
result := database.DB.Where("verification_token = ?", token).First(&emailVerification)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &emailVerification, nil
}
func (ur *UserRepository) SetVerificationToken(verification *models.Verification) (uint, error) {
// Use GORM to insert or update the Verification record
result := database.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{"verification_token", "created_at"}),
}).Create(&verification)
if result.Error != nil {
return 0, result.Error
}
return verification.ID, nil
}

View File

@@ -0,0 +1,57 @@
package repositories
import (
"GoMembership/internal/constants"
"GoMembership/internal/database"
"GoMembership/internal/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func (ur *UserRepository) IsVerified(userID *uint) (bool, error) {
var user models.User
result := database.DB.Select("status").First(&user, userID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return false, gorm.ErrRecordNotFound
}
return false, result.Error
}
return user.Status > constants.DisabledStatus, nil
}
func (ur *UserRepository) GetVerificationOfToken(token *string, verificationType *string) (*models.Verification, error) {
var emailVerification models.Verification
result := database.DB.Where("verification_token = ? AND type = ?", token, verificationType).First(&emailVerification)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &emailVerification, nil
}
func (ur *UserRepository) SetVerificationToken(verification *models.Verification) (token string, err error) {
result := database.DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{"verification_token", "created_at", "type"}),
}).Create(&verification)
if result.Error != nil {
return "", result.Error
}
return verification.VerificationToken, nil
}
func (ur *UserRepository) DeleteVerification(id uint, verificationType string) error {
result := database.DB.Where("user_id = ? AND type = ?", id, verificationType).Delete(&models.Verification{})
if result.Error != nil {
return result.Error
}
return nil
}

View File

@@ -11,7 +11,8 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
router.GET("/users/verify", userController.VerifyMailHandler) router.GET("/users/verify", userController.VerifyMailHandler)
router.POST("/users/register", userController.RegisterUser) router.POST("/users/register", userController.RegisterUser)
router.POST("/users/contact", contactController.RelayContactRequest) router.POST("/users/contact", contactController.RelayContactRequest)
router.POST("/users/password/request-change", userController.RequestPasswordChangeHandler)
router.PATCH("/users/password/change/:id", userController.ChangePassword)
router.POST("/users/login", userController.LoginHandler) router.POST("/users/login", userController.LoginHandler)
router.POST("/csp-report", middlewares.CSPReportHandling) router.POST("/csp-report", middlewares.CSPReportHandling)
@@ -26,7 +27,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
{ {
userRouter.GET("/users/current", userController.CurrentUserHandler) userRouter.GET("/users/current", userController.CurrentUserHandler)
userRouter.POST("/logout", userController.LogoutHandler) userRouter.POST("/logout", userController.LogoutHandler)
userRouter.PATCH("/users", userController.UpdateHandler) userRouter.PUT("/users", userController.UpdateHandler)
userRouter.POST("/users", userController.RegisterUser) userRouter.POST("/users", userController.RegisterUser)
userRouter.GET("/users/all", userController.GetAllUsers) userRouter.GET("/users/all", userController.GetAllUsers)
userRouter.DELETE("/users", userController.DeleteUser) userRouter.DELETE("/users", userController.DeleteUser)
@@ -36,7 +37,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
membershipRouter.Use(middlewares.AuthMiddleware()) membershipRouter.Use(middlewares.AuthMiddleware())
{ {
membershipRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions) membershipRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions)
membershipRouter.PATCH("/subscriptions", membershipcontroller.UpdateHandler) membershipRouter.PUT("/subscriptions", membershipcontroller.UpdateHandler)
membershipRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription) membershipRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription)
membershipRouter.DELETE("/subscriptions", membershipcontroller.DeleteSubscription) membershipRouter.DELETE("/subscriptions", membershipcontroller.DeleteSubscription)
} }

View File

@@ -3,6 +3,7 @@ package services
import ( import (
"bytes" "bytes"
"html/template" "html/template"
"os"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
@@ -21,7 +22,7 @@ func NewEmailService(host string, port int, username string, password string) *E
return &EmailService{dialer: dialer} return &EmailService{dialer: dialer}
} }
func (s *EmailService) SendEmail(to string, subject string, body string, replyTo string) error { func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT string, replyTo string) error {
msg := gomail.NewMessage() msg := gomail.NewMessage()
msg.SetHeader("From", s.dialer.Username) msg.SetHeader("From", s.dialer.Username)
msg.SetHeader("To", to) msg.SetHeader("To", to)
@@ -29,7 +30,12 @@ func (s *EmailService) SendEmail(to string, subject string, body string, replyTo
if replyTo != "" { if replyTo != "" {
msg.SetHeader("REPLY_TO", replyTo) msg.SetHeader("REPLY_TO", replyTo)
} }
msg.SetBody("text/html", body) if bodyTXT != "" {
msg.SetBody("text/plain", bodyTXT)
}
msg.AddAlternative("text/html", body)
msg.WriteTo(os.Stdout)
if err := s.dialer.DialAndSend(msg); err != nil { if err := s.dialer.DialAndSend(msg); err != nil {
logger.Error.Printf("Could not send email to %s: %v", to, err) logger.Error.Printf("Could not send email to %s: %v", to, err)
@@ -79,7 +85,38 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
logger.Error.Print("Couldn't send verification mail") logger.Error.Print("Couldn't send verification mail")
return err return err
} }
return s.SendEmail(user.Email, subject, body, "") return s.SendEmail(user.Email, subject, body, "", "")
}
func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
data := struct {
FirstName string
LastName string
Token string
BASEURL string
UserID uint
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
}
subject := constants.MailChangePasswordSubject
htmlBody, err := ParseTemplate("mail_change_password.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
plainBody, err := ParseTemplate("mail_change_password.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
} }
@@ -108,12 +145,17 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
} }
subject := constants.MailWelcomeSubject subject := constants.MailWelcomeSubject
body, err := ParseTemplate("mail_welcome.tmpl", data) htmlBody, err := ParseTemplate("mail_welcome.tmpl", data)
if err != nil { if err != nil {
logger.Error.Print("Couldn't send welcome mail") logger.Error.Print("Couldn't send welcome mail")
return err return err
} }
return s.SendEmail(user.Email, subject, body, "") plainBody, err := ParseTemplate("mail_welcome.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
} }
func (s *EmailService) SendRegistrationNotification(user *models.User) error { func (s *EmailService) SendRegistrationNotification(user *models.User) error {
@@ -157,12 +199,17 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
} }
subject := constants.MailRegistrationSubject subject := constants.MailRegistrationSubject
body, err := ParseTemplate("mail_registration.tmpl", data) htmlBody, err := ParseTemplate("mail_registration.tmpl", data)
if err != nil { if err != nil {
logger.Error.Print("Couldn't send admin notification mail") logger.Error.Print("Couldn't send admin notification mail")
return err return err
} }
return s.SendEmail(config.Recipients.UserRegistration, subject, body, "") plainBody, err := ParseTemplate("mail_registration.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
} }
func (s *EmailService) RelayContactFormMessage(sender string, name string, message string) error { func (s *EmailService) RelayContactFormMessage(sender string, name string, message string) error {
@@ -180,10 +227,15 @@ func (s *EmailService) RelayContactFormMessage(sender string, name string, messa
WebsiteTitle: config.Site.WebsiteTitle, WebsiteTitle: config.Site.WebsiteTitle,
} }
subject := constants.MailContactSubject subject := constants.MailContactSubject
body, err := ParseTemplate("mail_contact_form.tmpl", data) htmlBody, err := ParseTemplate("mail_contact_form.tmpl", data)
if err != nil { if err != nil {
logger.Error.Print("Couldn't send contact form message mail") logger.Error.Print("Couldn't send contact form message mail")
return err return err
} }
return s.SendEmail(config.Recipients.ContactForm, subject, body, sender) plainBody, err := ParseTemplate("mail_contact_form.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(config.Recipients.ContactForm, subject, htmlBody, plainBody, sender)
} }

View File

@@ -0,0 +1,21 @@
package services
import (
"GoMembership/internal/constants"
"GoMembership/internal/models"
)
func (s *UserService) HandlePasswordChangeRequest(user *models.User) (token string, err error) {
// Deactivate user and reset Verification
if err := s.SetUserStatus(user.ID, constants.DisabledStatus); err != nil {
return "", err
}
if err := s.RevokeVerification(&user.ID, constants.VerificationTypes.Password); err != nil {
return "", err
}
// Generate a token
return s.SetVerificationToken(&user.ID, &constants.VerificationTypes.Password)
}

View File

@@ -0,0 +1,5 @@
package services
func (s *UserService) SetUserStatus(id uint, status uint) error {
return s.Repo.SetUserStatus(id, status)
}

View File

@@ -1,15 +1,12 @@
package services package services
import ( import (
"net/http"
"strings" "strings"
"GoMembership/internal/constants" "GoMembership/internal/constants"
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"gorm.io/gorm" "gorm.io/gorm"
@@ -18,13 +15,17 @@ import (
) )
type UserServiceInterface interface { type UserServiceInterface interface {
RegisterUser(user *models.User) (uint, string, error) RegisterUser(user *models.User) (id uint, token string, err error)
GetUserByEmail(email string) (*models.User, error) GetUserByEmail(email string) (*models.User, error)
GetUserByID(id uint) (*models.User, error) GetUserByID(id uint) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error) GetUsers(where map[string]interface{}) (*[]models.User, error)
VerifyUser(token *string) (*models.User, error)
UpdateUser(user *models.User) (*models.User, error) UpdateUser(user *models.User) (*models.User, error)
DeleteUser(lastname string, id uint) error DeleteUser(lastname string, id uint) error
SetUserStatus(id uint, status uint) error
VerifyUser(token *string, verificationType *string) (*models.Verification, error)
SetVerificationToken(id *uint, verificationType *string) (string, error)
RevokeVerification(id *uint, verificationType string) error
HandlePasswordChangeRequest(user *models.User) (token string, err error)
} }
type UserService struct { type UserService struct {
@@ -81,7 +82,7 @@ func (service *UserService) UpdateUser(user *models.User) (*models.User, error)
return updatedUser, nil return updatedUser, nil
} }
func (service *UserService) RegisterUser(user *models.User) (uint, string, error) { func (service *UserService) RegisterUser(user *models.User) (id uint, token string, err error) {
setPassword(user.Password, user) setPassword(user.Password, user)
@@ -89,49 +90,20 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
user.CreatedAt = time.Now() user.CreatedAt = time.Now()
user.UpdatedAt = time.Now() user.UpdatedAt = time.Now()
user.PaymentStatus = constants.AwaitingPaymentStatus user.PaymentStatus = constants.AwaitingPaymentStatus
// if user.Licence == nil {
// user.Licence = &models.Licence{Status: constants.UnverifiedStatus}
// }
user.BankAccount.MandateDateSigned = time.Now() user.BankAccount.MandateDateSigned = time.Now()
id, err := service.Repo.CreateUser(user) id, err = service.Repo.CreateUser(user)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
user.ID = id token, err = service.SetVerificationToken(&id, &constants.VerificationTypes.Email)
token, err := utils.GenerateVerificationToken()
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
logger.Info.Printf("TOKEN: %v", token)
// Check if user is already verified
verified, err := service.Repo.IsVerified(&user.ID)
if err != nil {
return 0, "", err
}
if verified {
return 0, "", errors.ErrAlreadyVerified
}
// Prepare the Verification record
verification := models.Verification{
UserID: user.ID,
VerificationToken: token,
}
if _, err = service.Repo.SetVerificationToken(&verification); err != nil {
return http.StatusInternalServerError, "", err
}
return id, token, nil return id, token, nil
} }
func (service *UserService) GetUserByID(id uint) (*models.User, error) { func (service *UserService) GetUserByID(id uint) (*models.User, error) {
return repositories.GetUserByID(&id) return repositories.GetUserByID(&id)
} }
@@ -146,35 +118,6 @@ func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.Us
return service.Repo.GetUsers(where) return service.Repo.GetUsers(where)
} }
func (service *UserService) VerifyUser(token *string) (*models.User, error) {
verification, err := service.Repo.GetVerificationOfToken(token)
if err != nil {
return nil, err
}
// Check if the user is already verified
verified, err := service.Repo.IsVerified(&verification.UserID)
if err != nil {
return nil, err
}
user, err := repositories.GetUserByID(&verification.UserID)
if err != nil {
return nil, err
}
if verified {
return user, errors.ErrAlreadyVerified
}
// Update user status to active
t := time.Now()
verification.EmailVerifiedAt = &t
user.Status = constants.VerifiedStatus
user.Verification = *verification
user.ID = verification.UserID
service.Repo.UpdateUser(user)
return user, nil
}
func setPassword(plaintextPassword string, u *models.User) error { func setPassword(plaintextPassword string, u *models.User) error {
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams) hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil { if err != nil {

View File

@@ -0,0 +1,60 @@
package services
import (
"GoMembership/internal/models"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"time"
)
func (s *UserService) SetVerificationToken(id *uint, verificationType *string) (string, error) {
token, err := utils.GenerateVerificationToken()
if err != nil {
return "", err
}
// Check if user is already verified
verified, err := s.Repo.IsVerified(id)
if err != nil {
return "", err
}
if verified {
return "", errors.ErrAlreadyVerified
}
// Prepare the Verification record
verification := models.Verification{
UserID: *id,
VerificationToken: token,
Type: *verificationType,
}
return s.Repo.SetVerificationToken(&verification)
}
func (s *UserService) RevokeVerification(id *uint, verificationType string) error {
return s.Repo.DeleteVerification(*id, verificationType)
}
func (service *UserService) VerifyUser(token *string, verificationType *string) (*models.Verification, error) {
verification, err := service.Repo.GetVerificationOfToken(token, verificationType)
if err != nil {
return nil, err
}
// Check if the user is already verified
verified, err := service.Repo.IsVerified(&verification.UserID)
if err != nil {
return nil, err
}
if verified {
return nil, errors.ErrAlreadyVerified
}
t := time.Now()
verification.VerifiedAt = &t
// Update user status to active
return verification, nil
}

View File

@@ -81,21 +81,6 @@ func FilterAllowedStructFields(input interface{}, existing interface{}, allowedF
} else { } else {
originField.Set(fieldValue) originField.Set(fieldValue)
} }
// If the slice contains structs, recursively filter each element
// if fieldValue.Type().Elem().Kind() == reflect.Struct {
// for j := 0; j < fieldValue.Len(); j++ {
// err := FilterAllowedStructFields(
// fieldValue.Index(j).Addr().Interface(),
// originField.Index(j).Addr().Interface(),
// allowedFields,
// fullKey,
// )
// if err != nil {
// return err
// }
// }
// }
continue continue
} }

View File

@@ -12,13 +12,14 @@ type ValidationKeys struct {
JwtGenerationFailed string JwtGenerationFailed string
Duplicate string Duplicate string
InvalidUserID string InvalidUserID string
PasswordAlreadyChanged string
UserDisabled string
} }
type ValidationFields struct { type ValidationFields struct {
General string General string
ParentMemberShipID string ParentMemberShipID string
SubscriptionModel string SubscriptionModel string
UserID string
Login string Login string
Email string Email string
User string User string
@@ -60,14 +61,15 @@ var Responses = struct {
UserNotFoundWrongPassword: "server.validation.user_not_found_or_wrong_password", UserNotFoundWrongPassword: "server.validation.user_not_found_or_wrong_password",
JwtGenerationFailed: "server.error.jwt_generation_failed", JwtGenerationFailed: "server.error.jwt_generation_failed",
Duplicate: "server.validation.duplicate", Duplicate: "server.validation.duplicate",
UserDisabled: "server.validation.user_disabled",
PasswordAlreadyChanged: "server.validation.password_already_changed",
}, },
Fields: ValidationFields{ Fields: ValidationFields{
General: "general", General: "general",
ParentMemberShipID: "parent_membership_id", ParentMemberShipID: "parent_membership_id",
SubscriptionModel: "subscription_model", SubscriptionModel: "subscription_model",
UserID: "user_id", Login: "user.login",
Login: "login", Email: "user.email",
Email: "email", User: "user.user",
User: "user",
}, },
} }

View File

@@ -0,0 +1,167 @@
<!doctype html>
<html>
<body>
<div
style="
background-color: #f2f5f7;
color: #242424;
font-family: Optima, Candara, &quot;Noto Sans&quot;, source-sans-pro,
sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: 0.15008px;
line-height: 1.5;
margin: 0;
padding: 32px 0;
min-height: 100%;
width: 100%;
"
>
<table
align="center"
width="100%"
style="margin: 0 auto; max-width: 600px; background-color: #ffffff"
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
>
<tbody>
<tr style="width: 100%">
<td>
<div style="padding: 24px 24px 24px 24px; text-align: center">
<a
href="{{.BASEURL}}"
style="text-decoration: none"
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg"
style="
outline: none;
border: none;
text-decoration: none;
vertical-align: middle;
display: inline-block;
max-width: 100%;
"
/></a>
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
Moin {{.FirstName}} {{.LastName}} 👋,
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
wir haben die Aufforderung erhalten, Dein Passwort zu ändern. Solltest Du
dies nicht angefordert haben, ignoriere diese E-Mail einfach.
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Ansonsten kannst Du Dein Passwort jetzt ändern, indem Du hier auf den Link klickst:
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href=" {{.BASEURL}}/auth/password/change/{{.UserID}}?token={{.Token}}"
style="
color: #ffffff;
font-size: 26px;
font-weight: bold;
background-color: #3e9bfc;
border-radius: 4px;
display: block;
padding: 16px 32px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 32px;
mso-font-width: -100%;
mso-text-raise: 48;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>
Passwort ändern
</span
><span
><!--[if mso
]><i
style="letter-spacing: 32px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div
style="
font-weight: normal;
text-align: center;
padding: 24px 24px 0px 24px;
"
>
Alternativ kannst Du auch diesen Link in Deinem Browser öffnen:
</div>
<div
style="
font-weight: bold;
text-align: center;
padding: 4px 24px 16px 24px;
"
>
{{.BASEURL}}/auth/password/change/{{.UserID}}?token={{.Token}}
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Mit Freundlichen Grüßen,
</div>
<div
style="
font-weight: bold;
text-align: left;
padding: 16px 24px 16px 24px;
"
>
Der Vorstand
</div>
<div style="padding: 16px 24px 16px 24px">
<img
alt=""
src="{{.BASEURL}}/images/favicon_hu5543b2b337a87a169e2c722ef0122802_211442_96x0_resize_lanczos_3.png"
height="80"
width="80"
style="
outline: none;
border: none;
text-decoration: none;
object-fit: cover;
height: 80px;
width: 80px;
max-width: 100%;
display: inline-block;
vertical-align: middle;
text-align: center;
border-radius: 80px;
"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
Moin {{.FirstName}} {{.LastName}} 👋,
wir haben die Aufforderung erhalten, Dein Passwort zu ändern. Solltest Du
dies nicht angefordert haben, ignoriere diese E-Mail einfach.
Ansonsten kannst Du Dein Passwort jetzt ändern, indem Du hier auf den Link klickst:
Passwort ändern:
{{.BASEURL}}/auth/password/change/{{.UserID}}?token={{.Token}}
Mit Freundlichen Grüßen,
Der Vorstand

View File

@@ -1,14 +1,3 @@
Moin Du Vorstand 👋,
Eine neue Kontaktanfrage!
{{.Name}} hat geschrieben
Hier ist die Nachricht:
{{.Message}}
Mit freundlichen Grüßen,
Dein untertänigster Wolkenrechner
<!doctype html> <!doctype html>
<html> <html>
<body> <body>

View File

@@ -0,0 +1,11 @@
Moin Du Vorstand 👋,
Eine neue Kontaktanfrage!
{{.Name}} hat geschrieben
Hier ist die Nachricht:
{{.Message}}
Mit freundlichen Grüßen,
Dein untertänigster Wolkenrechner

View File

@@ -1,32 +1,3 @@
Moin Du Vorstand 👋,
Ein neues Mitglied!!!<br />{{.FirstName}} {{.LastName}} hat sich registriert.
Hier sind die Daten:
---------------------
Das gebuchtes Modell:
Name: {{.MembershipModel}}
Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}}
Persönliche Daten:
{{if .Company}}
Firma: {{.Company}}
{{end}}
Name: {{.FirstName}} {{.LastName}}
Mitgliedsnr: {{.MembershipID}}
Adresse: {{.Address}},
{{.ZipCode}} {{.City}}
Geburtsdatum: {{.DateOfBirth}}
Email: {{.Email}}
Telefon: {{.Phone}}
IBAN: {{.IBAN}}
Mit freundlichen Grüßen,
Dein untertänigster Wolkenrechner
<!doctype html> <!doctype html>
<html> <html>
<body> <body>

View File

@@ -0,0 +1,29 @@
Moin Du Vorstand 👋,
Ein neues Mitglied!!!<br />{{.FirstName}} {{.LastName}} hat sich registriert.
Hier sind die Daten:
---------------------
Das gebuchtes Modell:
Name: {{.MembershipModel}}
Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}}
Persönliche Daten:
{{if .Company}}
Firma: {{.Company}}
{{end}}
Name: {{.FirstName}} {{.LastName}}
Mitgliedsnr: {{.MembershipID}}
Adresse: {{.Address}},
{{.ZipCode}} {{.City}}
Geburtsdatum: {{.DateOfBirth}}
Email: {{.Email}}
Telefon: {{.Phone}}
IBAN: {{.IBAN}}
Mit freundlichen Grüßen,
Dein untertänigster Wolkenrechner

View File

@@ -1,27 +1,3 @@
Moin {{.FirstName}} {{.LastName}} 👋,
herzlich willkommen beim Dörpsmobil Hasloh e.V.! Vielen Dank für
Ihre Registrierung und Ihre Unterstützung unseres Projekts.
Um die Registrierung abschließen zu können bestätigen Sie bitte
noch Ihre Emailadresse indem Sie hier klicken:
E-Mail Adresse bestätigen
{{.BASEURL}}/users/verify?token={{.Token}}
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die
gemeinsame Zeit mit Ihnen!
Sollte es Probleme geben, möchten wir uns gerne jetzt schon
dafür entschuldigen, wenden Sie sich gerne an uns, wir werden
uns sofort darum kümmern, versprochen! Antworten Sie einfach auf
diese E-Mail.
Mit Freundlichen Grüßen,
Der Vorstand
<!doctype html> <!doctype html>
<html> <html>
<body> <body>

View File

@@ -0,0 +1,24 @@
Moin {{.FirstName}} {{.LastName}} 👋,
herzlich willkommen beim Dörpsmobil Hasloh e.V.! Vielen Dank für
Ihre Registrierung und Ihre Unterstützung unseres Projekts.
Um die Registrierung abschließen zu können bestätigen Sie bitte
noch Ihre Emailadresse indem Sie hier klicken:
E-Mail Adresse bestätigen
{{.BASEURL}}/users/verify?token={{.Token}}
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die
gemeinsame Zeit mit Ihnen!
Sollte es Probleme geben, möchten wir uns gerne jetzt schon
dafür entschuldigen, wenden Sie sich gerne an uns, wir werden
uns sofort darum kümmern, versprochen! Antworten Sie einfach auf
diese E-Mail.
Mit Freundlichen Grüßen,
Der Vorstand

View File

@@ -1,59 +1,3 @@
Moin {{.FirstName}} {{if .Company}}({{.Company}}){{end}}👋,
wir freuen uns sehr, dich als neues Mitglied bei Carsharing
Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur
erfolgreichen E-Mail-Verifikation und willkommen in unserem
Verein!
Hier einige wichtige Informationen für dich:
Deine Mitgliedsnummer: {{.MembershipID}}
Dein gebuchtes Modell:
Name: {{.MembershipModel}}
Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}}
Mitgliedsbeitrag: Solange wir noch kein
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
Führerscheinverifikation: Weitere Informationen zur Verifikation
deines Führerscheins folgen in Kürze. Du musst nichts weiter tun,
wir werden uns bei dir melden, sobald es notwendig ist.
Moqo App:
Wir werden die Moqo App nutzen,
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest,
schaue Dir gerne dieses kurze Video an:
Moqo App Nutzung
https://www.youtube.com/shorts/ZMKUX0uyOps
Dörpsmobil:
Wir sind nicht alleine sondern Mitglied in einem Schleswig-Holstein
weiten Netz an gemeinnützigen Carsharing Anbietern. Für mehr
Informationen zu diesem Netzwerk haben wir auch ein Video vorbereitet:
Dörpsmobil SH
https://www.youtube.com/watch?v=NSch-2F-ru0
Für mehr Informationen besuche gerne unsere Webseite:
Carsharing-Hasloh.de
{{.BASEURL}}
Solltest du Fragen haben oder Unterstützung benötigen, kannst
du dich jederzeit an unsere Vorsitzende wenden:
Anke Freitag
E-Mail: vorstand@carsharing-hasloh.de
Telefon: +49 176 5013 4256
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen.
Mit freundlichen Grüßen,
Dein Carsharing Hasloh Team
<!doctype html> <!doctype html>
<html> <html>
<body> <body>

View File

@@ -0,0 +1,56 @@
Moin {{.FirstName}} {{if .Company}}({{.Company}}){{end}}👋,
wir freuen uns sehr, dich als neues Mitglied bei Carsharing
Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur
erfolgreichen E-Mail-Verifikation und willkommen in unserem
Verein!
Hier einige wichtige Informationen für dich:
Deine Mitgliedsnummer: {{.MembershipID}}
Dein gebuchtes Modell:
Name: {{.MembershipModel}}
Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}}
Mitgliedsbeitrag: Solange wir noch kein
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
Führerscheinverifikation: Weitere Informationen zur Verifikation
deines Führerscheins folgen in Kürze. Du musst nichts weiter tun,
wir werden uns bei dir melden, sobald es notwendig ist.
Moqo App:
Wir werden die Moqo App nutzen,
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest,
schaue Dir gerne dieses kurze Video an:
Moqo App Nutzung
https://www.youtube.com/shorts/ZMKUX0uyOps
Dörpsmobil:
Wir sind nicht alleine sondern Mitglied in einem Schleswig-Holstein
weiten Netz an gemeinnützigen Carsharing Anbietern. Für mehr
Informationen zu diesem Netzwerk haben wir auch ein Video vorbereitet:
Dörpsmobil SH
https://www.youtube.com/watch?v=NSch-2F-ru0
Für mehr Informationen besuche gerne unsere Webseite:
Carsharing-Hasloh.de
{{.BASEURL}}
Solltest du Fragen haben oder Unterstützung benötigen, kannst
du dich jederzeit an unsere Vorsitzende wenden:
Anke Freitag
E-Mail: vorstand@carsharing-hasloh.de
Telefon: +49 176 5013 4256
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen.
Mit freundlichen Grüßen,
Dein Carsharing Hasloh Team