Compare commits
9 Commits
f55ef5cf70
...
c42adc858f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c42adc858f | ||
|
|
7c01b77445 | ||
|
|
a2886fc1e0 | ||
|
|
6b408d64a7 | ||
|
|
3eeb35c768 | ||
|
|
b7682f8dc3 | ||
|
|
dde3b3d47b | ||
|
|
c607622185 | ||
|
|
2866917aef |
@@ -65,15 +65,13 @@
|
|||||||
*/
|
*/
|
||||||
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');
|
||||||
case 'email':
|
} else if (name.includes('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');
|
||||||
case 'password':
|
} else if (name.includes('password')) {
|
||||||
case 'password2':
|
|
||||||
if (typeof value === 'string' && value.length < 8) {
|
if (typeof value === 'string' && value.length < 8) {
|
||||||
return $t('validation.password');
|
return $t('validation.password');
|
||||||
}
|
}
|
||||||
@@ -81,27 +79,23 @@
|
|||||||
return $t('validation.password_match');
|
return $t('validation.password_match');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
case 'phone':
|
} else if (name.includes('phone')) {
|
||||||
return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value)
|
return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value)
|
||||||
? null
|
? null
|
||||||
: $t('validation.phone');
|
: $t('validation.phone');
|
||||||
case 'zip_code':
|
} else if (name.includes('zip_code')) {
|
||||||
return typeof value === 'string' && /^\d{5}$/.test(value)
|
return typeof value === 'string' && /^\d{5}$/.test(value) ? null : $t('validation.zip_code');
|
||||||
? null
|
} else if (name.includes('iban')) {
|
||||||
: $t('validation.zip_code');
|
|
||||||
case 'iban':
|
|
||||||
return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
|
return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
|
||||||
? null
|
? null
|
||||||
: $t('validation.iban');
|
: $t('validation.iban');
|
||||||
case 'bic':
|
} else if (name.includes('bic')) {
|
||||||
return typeof value === 'string' &&
|
return typeof value === 'string' && /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
||||||
/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
|
||||||
? null
|
? null
|
||||||
: $t('validation.bic');
|
: $t('validation.bic');
|
||||||
case 'licence_number':
|
} else if (name.includes('licence_number')) {
|
||||||
return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
|
return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
|
||||||
|
} else {
|
||||||
default:
|
|
||||||
return typeof value === 'string' && !value.trim() && required
|
return typeof value === 'string' && !value.trim() && required
|
||||||
? $t('validation.required')
|
? $t('validation.required')
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
17
frontend/src/routes/auth/confirming/+page.svelte
Normal file
17
frontend/src/routes/auth/confirming/+page.svelte
Normal 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>
|
||||||
@@ -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">
|
||||||
|
|||||||
33
frontend/src/routes/auth/password/change/+page.server.js
Normal file
33
frontend/src/routes/auth/password/change/+page.server.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
45
frontend/src/routes/auth/password/change/+page.svelte
Normal file
45
frontend/src/routes/auth/password/change/+page.svelte
Normal 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>
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
76
frontend/src/routes/auth/password/change/[id]/+page.svelte
Normal file
76
frontend/src/routes/auth/password/change/[id]/+page.svelte
Normal 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>
|
||||||
@@ -2,14 +2,15 @@ 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!"
|
||||||
|
MailChangePasswordSubject = "Passwort Änderung angefordert"
|
||||||
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
|
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
|
||||||
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
|
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
|
||||||
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
|
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
|
||||||
@@ -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,
|
||||||
|
|||||||
103
internal/controllers/user_Password.go
Normal file
103
internal/controllers/user_Password.go
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
10
internal/repositories/user_permissions.go
Normal file
10
internal/repositories/user_permissions.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
57
internal/repositories/user_verification.go
Normal file
57
internal/repositories/user_verification.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
21
internal/services/user_password.go
Normal file
21
internal/services/user_password.go
Normal 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)
|
||||||
|
|
||||||
|
}
|
||||||
5
internal/services/user_permissions.go
Normal file
5
internal/services/user_permissions.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
func (s *UserService) SetUserStatus(id uint, status uint) error {
|
||||||
|
return s.Repo.SetUserStatus(id, status)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
60
internal/services/user_verification.go
Normal file
60
internal/services/user_verification.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
167
templates/email/mail_change_password.tmpl
Normal file
167
templates/email/mail_change_password.tmpl
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
background-color: #f2f5f7;
|
||||||
|
color: #242424;
|
||||||
|
font-family: Optima, Candara, "Noto Sans", 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
|
||||||
|
> </i
|
||||||
|
><!
|
||||||
|
[endif]--></span
|
||||||
|
><span>
|
||||||
|
Passwort ändern
|
||||||
|
</span
|
||||||
|
><span
|
||||||
|
><!--[if mso
|
||||||
|
]><i
|
||||||
|
style="letter-spacing: 32px; mso-font-width: -100%"
|
||||||
|
hidden
|
||||||
|
> </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>
|
||||||
13
templates/email/mail_change_password.txt.tmpl
Normal file
13
templates/email/mail_change_password.txt.tmpl
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
11
templates/email/mail_contact_form.txt.tmpl
Normal file
11
templates/email/mail_contact_form.txt.tmpl
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
29
templates/email/mail_registration.txt.tmpl
Normal file
29
templates/email/mail_registration.txt.tmpl
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
24
templates/email/mail_verification.txt.tmpl
Normal file
24
templates/email/mail_verification.txt.tmpl
Normal 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
|
||||||
@@ -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>
|
||||||
|
|||||||
56
templates/email/mail_welcome.txt.tmpl
Normal file
56
templates/email/mail_welcome.txt.tmpl
Normal 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
|
||||||
Reference in New Issue
Block a user