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) {
|
||||
if (value === null || (typeof value === 'string' && !value.trim() && !required)) return null;
|
||||
switch (name) {
|
||||
case 'membership_start_date':
|
||||
if (name.includes('membership_start_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)
|
||||
? null
|
||||
: $t('validation.email');
|
||||
case 'password':
|
||||
case 'password2':
|
||||
} else if (name.includes('password')) {
|
||||
if (typeof value === 'string' && value.length < 8) {
|
||||
return $t('validation.password');
|
||||
}
|
||||
@@ -81,27 +79,23 @@
|
||||
return $t('validation.password_match');
|
||||
}
|
||||
return null;
|
||||
case 'phone':
|
||||
} else if (name.includes('phone')) {
|
||||
return typeof value === 'string' && /^\+?[0-9\s()-]{7,}$/.test(value)
|
||||
? null
|
||||
: $t('validation.phone');
|
||||
case 'zip_code':
|
||||
return typeof value === 'string' && /^\d{5}$/.test(value)
|
||||
? null
|
||||
: $t('validation.zip_code');
|
||||
case 'iban':
|
||||
} else if (name.includes('zip_code')) {
|
||||
return typeof value === 'string' && /^\d{5}$/.test(value) ? null : $t('validation.zip_code');
|
||||
} else if (name.includes('iban')) {
|
||||
return typeof value === 'string' && /^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(value)
|
||||
? null
|
||||
: $t('validation.iban');
|
||||
case 'bic':
|
||||
return typeof value === 'string' &&
|
||||
/^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
||||
} else if (name.includes('bic')) {
|
||||
return typeof value === 'string' && /^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$/.test(value)
|
||||
? null
|
||||
: $t('validation.bic');
|
||||
case 'licence_number':
|
||||
} else if (name.includes('licence_number')) {
|
||||
return typeof value === 'string' && value.length == 11 ? null : $t('validation.licence');
|
||||
|
||||
default:
|
||||
} else {
|
||||
return typeof value === 'string' && !value.trim() && required
|
||||
? $t('validation.required')
|
||||
: null;
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
let isUpdating = false,
|
||||
password = '',
|
||||
password2 = '';
|
||||
confirm_password = '';
|
||||
|
||||
/** @type {Object.<string, App.Locals['licence_categories']>} */
|
||||
$: groupedCategories = groupCategories(licence_categories);
|
||||
@@ -249,14 +249,14 @@
|
||||
label={$t('password')}
|
||||
placeholder={$t('placeholder.password')}
|
||||
bind:value={password}
|
||||
otherPasswordValue={password2}
|
||||
otherPasswordValue={confirm_password}
|
||||
/>
|
||||
<InputField
|
||||
name="password2"
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
label={$t('password_repeat')}
|
||||
label={$t('confirm_password')}
|
||||
placeholder={$t('placeholder.password')}
|
||||
bind:value={password2}
|
||||
bind:value={confirm_password}
|
||||
otherPasswordValue={password}
|
||||
/>
|
||||
<InputField
|
||||
@@ -269,7 +269,7 @@
|
||||
/>
|
||||
<InputField
|
||||
name="user[last_name]"
|
||||
label={$t('last_name')}
|
||||
label={$t('user.last_name')}
|
||||
bind:value={localUser.last_name}
|
||||
placeholder={$t('placeholder.last_name')}
|
||||
required={true}
|
||||
@@ -292,14 +292,14 @@
|
||||
<InputField
|
||||
name="user[phone]"
|
||||
type="tel"
|
||||
label={$t('phone')}
|
||||
label={$t('user.phone')}
|
||||
bind:value={localUser.phone}
|
||||
placeholder={$t('placeholder.phone')}
|
||||
/>
|
||||
<InputField
|
||||
name="user[dateofbirth]"
|
||||
type="date"
|
||||
label={$t('dateofbirth')}
|
||||
label={$t('user.dateofbirth')}
|
||||
bind:value={localUser.dateofbirth}
|
||||
placeholder={$t('placeholder.dateofbirth')}
|
||||
readonly={role_id === 0}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export default {
|
||||
userStatus: {
|
||||
1: 'Nicht verifiziert',
|
||||
2: 'Verifiziert',
|
||||
3: 'Aktiv',
|
||||
4: 'Passiv',
|
||||
5: 'Deaktiviert'
|
||||
2: 'Deaktiviert',
|
||||
3: 'Verifiziert',
|
||||
4: 'Aktiv',
|
||||
5: 'Passiv'
|
||||
},
|
||||
userRole: {
|
||||
0: 'Mitglied',
|
||||
@@ -51,6 +51,7 @@ export default {
|
||||
licence: 'Nummer zu kurz(11 Zeichen)'
|
||||
},
|
||||
server: {
|
||||
general: 'Allgemein',
|
||||
error: {
|
||||
invalid_json: 'JSON Daten sind ungültig',
|
||||
no_auth_token: 'Nicht authorisiert, fehlender oder ungültiger Auth-Token',
|
||||
@@ -68,6 +69,7 @@ export default {
|
||||
invalid_user_data: 'Nutzerdaten ungültig',
|
||||
user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
|
||||
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',
|
||||
safe_content: 'I see what you did there! Do not cross this line!',
|
||||
iban: 'Ungültig. Format: DE07123412341234123412',
|
||||
@@ -80,6 +82,7 @@ export default {
|
||||
required: 'Feld wird benötigt',
|
||||
image: 'Dies ist kein Bild',
|
||||
alphanum: 'beinhaltet ungültige Zeichen',
|
||||
user_disabled: 'Benutzer ist deaktiviert',
|
||||
alphaunicode: 'darf nur aus Buchstaben bestehen'
|
||||
}
|
||||
},
|
||||
@@ -109,7 +112,10 @@ export default {
|
||||
user: 'Nutzer',
|
||||
management: 'Mitgliederverwaltung',
|
||||
id: 'Mitgliedsnr',
|
||||
name: 'Name',
|
||||
first_name: 'Vorname',
|
||||
last_name: 'Nachname',
|
||||
phone: 'Telefonnummer',
|
||||
dateofbirth: 'Geburtstag',
|
||||
email: 'Email',
|
||||
status: 'Status',
|
||||
role: 'Nutzerrolle'
|
||||
@@ -140,6 +146,7 @@ export default {
|
||||
edit: 'Bearbeiten',
|
||||
delete: 'Löschen',
|
||||
search: 'Suche:',
|
||||
name: 'Name',
|
||||
mandate_date_signed: 'Mandatserteilungsdatum',
|
||||
licence_categories: 'Führerscheinklassen',
|
||||
subscription_model: 'Mitgliedschatfsmodell',
|
||||
@@ -156,16 +163,16 @@ export default {
|
||||
zip_code: 'PLZ',
|
||||
forgot_password: 'Passwort vergessen?',
|
||||
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',
|
||||
login: 'Anmeldung',
|
||||
profile: 'Profil',
|
||||
membership: 'Mitgliedschaft',
|
||||
bankaccount: 'Kontodaten',
|
||||
first_name: 'Vorname',
|
||||
last_name: 'Nachname',
|
||||
phone: 'Telefonnummer',
|
||||
dateofbirth: 'Geburtstag',
|
||||
status: 'Status',
|
||||
start: 'Beginn',
|
||||
end: 'Ende',
|
||||
@@ -177,7 +184,7 @@ export default {
|
||||
mandate_reference: 'SEPA Mandat',
|
||||
payments: 'Zahlungen',
|
||||
add_new: 'Neu',
|
||||
|
||||
email_sent: 'Email wurde gesendet..',
|
||||
// For payments section
|
||||
payment: {
|
||||
id: 'Zahlungs-Nr',
|
||||
@@ -185,7 +192,6 @@ export default {
|
||||
date: 'Datum',
|
||||
status: 'Status'
|
||||
},
|
||||
|
||||
// For subscription statuses
|
||||
subscriptionStatus: {
|
||||
pending: 'Ausstehend',
|
||||
|
||||
@@ -3,18 +3,18 @@ import { toRFC3339 } from './helpers';
|
||||
/**
|
||||
* Converts FormData to a nested object structure
|
||||
* @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) {
|
||||
/** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> } */
|
||||
const object = {};
|
||||
let password2 = '';
|
||||
let confirm_password = '';
|
||||
|
||||
console.log('Form data entries:');
|
||||
for (const [key, value] of formData.entries()) {
|
||||
console.log('Key:', key, 'Value:', value);
|
||||
if (key == 'password2') {
|
||||
password2 = String(value);
|
||||
if (key == 'confirm_password') {
|
||||
confirm_password = String(value);
|
||||
continue;
|
||||
}
|
||||
/** @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
|
||||
* @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
|
||||
*/
|
||||
export function processUserFormData(rawData) {
|
||||
@@ -130,8 +130,8 @@ export function processUserFormData(rawData) {
|
||||
console.dir(rawData.object.licence);
|
||||
if (
|
||||
rawData.object.password &&
|
||||
rawData.password2 &&
|
||||
rawData.object.password === rawData.password2 &&
|
||||
rawData.confirm_password &&
|
||||
rawData.object.password === rawData.confirm_password &&
|
||||
rawData.object.password.trim() !== ''
|
||||
) {
|
||||
processedData.user.password = rawData.object.password;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const actions = {
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestUpdateOptions = {
|
||||
method: 'PATCH',
|
||||
method: 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -45,7 +45,7 @@ export const actions = {
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: isCreating ? 'POST' : 'PATCH',
|
||||
method: isCreating ? 'POST' : 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -89,7 +89,7 @@ export const actions = {
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestOptions = {
|
||||
method: isCreating ? 'POST' : 'PATCH',
|
||||
method: isCreating ? 'POST' : 'PUT',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
<td>{user.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{$t('user.name')}</th>
|
||||
<th>{$t('name')}</th>
|
||||
<td>{user.first_name} {user.last_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -415,6 +415,7 @@
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
role_id={user.role_id}
|
||||
user={selectedUser}
|
||||
{subscriptions}
|
||||
{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 message}
|
||||
<h4 class="step-subtitle">{message}</h4>
|
||||
<h4 class="step-subtitle">{$t(message)}</h4>
|
||||
{/if}
|
||||
|
||||
<input type="hidden" name="next" value={$page.url.searchParams.get('next')} />
|
||||
@@ -53,7 +53,7 @@
|
||||
name="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 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 (
|
||||
UnverifiedStatus = iota + 1
|
||||
DisabledStatus
|
||||
VerifiedStatus
|
||||
ActiveStatus
|
||||
PassiveStatus
|
||||
DisabledStatus
|
||||
DelayedPaymentStatus
|
||||
SettledPaymentStatus
|
||||
AwaitingPaymentStatus
|
||||
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
|
||||
MailChangePasswordSubject = "Passwort Änderung angefordert"
|
||||
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
|
||||
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
|
||||
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
|
||||
@@ -63,6 +64,14 @@ var Licences = struct {
|
||||
T: "T",
|
||||
}
|
||||
|
||||
var VerificationTypes = struct {
|
||||
Email string
|
||||
Password string
|
||||
}{
|
||||
Email: "email",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
var Priviliges = struct {
|
||||
View int8
|
||||
Create int8
|
||||
@@ -75,11 +84,6 @@ var Priviliges = struct {
|
||||
Delete: 30,
|
||||
}
|
||||
|
||||
const PRIV_VIEW = 1
|
||||
const PRIV_ADD = 2
|
||||
const PRIV_EDIT = 4
|
||||
const PRIV_DELETE = 8
|
||||
|
||||
var MemberUpdateFields = map[string]bool{
|
||||
"Email": 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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -216,9 +216,18 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
|
||||
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)
|
||||
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
|
||||
}
|
||||
if !ok {
|
||||
@@ -233,7 +242,7 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
|
||||
logger.Error.Printf("jwtsecret: %v", config.Auth.JWTSecret)
|
||||
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -331,15 +340,26 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := uc.Service.VerifyUser(&token)
|
||||
verification, err := uc.Service.VerifyUser(&token, &constants.VerificationTypes.Email)
|
||||
if err != nil {
|
||||
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."})
|
||||
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)
|
||||
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")
|
||||
return cors.New(cors.Config{
|
||||
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"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
|
||||
@@ -5,9 +5,9 @@ import "time"
|
||||
type Verification struct {
|
||||
UpdatedAt time.Time
|
||||
CreatedAt time.Time
|
||||
EmailVerifiedAt *time.Time `gorm:"Default:NULL" json:"email_verified_at"`
|
||||
IDVerifiedAt *time.Time `gorm:"Default:NULL" json:"id_verified_at"`
|
||||
VerifiedAt *time.Time `gorm:"Default:NULL" json:"verified_at"`
|
||||
VerificationToken string `json:"token"`
|
||||
ID uint `gorm:"primaryKey"`
|
||||
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 (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/database"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -18,10 +17,12 @@ type UserRepositoryInterface interface {
|
||||
UpdateUser(user *models.User) (*models.User, error)
|
||||
GetUsers(where map[string]interface{}) (*[]models.User, error)
|
||||
GetUserByEmail(email string) (*models.User, error)
|
||||
SetVerificationToken(verification *models.Verification) (uint, 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
|
||||
SetUserStatus(id uint, status uint) error
|
||||
}
|
||||
|
||||
type UserRepository struct{}
|
||||
@@ -156,42 +157,3 @@ func (ur *UserRepository) GetUserByEmail(email string) (*models.User, error) {
|
||||
}
|
||||
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.POST("/users/register", userController.RegisterUser)
|
||||
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("/csp-report", middlewares.CSPReportHandling)
|
||||
|
||||
@@ -26,7 +27,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
|
||||
{
|
||||
userRouter.GET("/users/current", userController.CurrentUserHandler)
|
||||
userRouter.POST("/logout", userController.LogoutHandler)
|
||||
userRouter.PATCH("/users", userController.UpdateHandler)
|
||||
userRouter.PUT("/users", userController.UpdateHandler)
|
||||
userRouter.POST("/users", userController.RegisterUser)
|
||||
userRouter.GET("/users/all", userController.GetAllUsers)
|
||||
userRouter.DELETE("/users", userController.DeleteUser)
|
||||
@@ -36,7 +37,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
|
||||
membershipRouter.Use(middlewares.AuthMiddleware())
|
||||
{
|
||||
membershipRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions)
|
||||
membershipRouter.PATCH("/subscriptions", membershipcontroller.UpdateHandler)
|
||||
membershipRouter.PUT("/subscriptions", membershipcontroller.UpdateHandler)
|
||||
membershipRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription)
|
||||
membershipRouter.DELETE("/subscriptions", membershipcontroller.DeleteSubscription)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"os"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
|
||||
@@ -21,7 +22,7 @@ func NewEmailService(host string, port int, username string, password string) *E
|
||||
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.SetHeader("From", s.dialer.Username)
|
||||
msg.SetHeader("To", to)
|
||||
@@ -29,7 +30,12 @@ func (s *EmailService) SendEmail(to string, subject string, body string, replyTo
|
||||
if 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 {
|
||||
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")
|
||||
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
|
||||
body, err := ParseTemplate("mail_welcome.tmpl", data)
|
||||
htmlBody, err := ParseTemplate("mail_welcome.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send welcome mail")
|
||||
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 {
|
||||
@@ -157,12 +199,17 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
|
||||
}
|
||||
|
||||
subject := constants.MailRegistrationSubject
|
||||
body, err := ParseTemplate("mail_registration.tmpl", data)
|
||||
htmlBody, err := ParseTemplate("mail_registration.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send admin notification mail")
|
||||
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 {
|
||||
@@ -180,10 +227,15 @@ func (s *EmailService) RelayContactFormMessage(sender string, name string, messa
|
||||
WebsiteTitle: config.Site.WebsiteTitle,
|
||||
}
|
||||
subject := constants.MailContactSubject
|
||||
body, err := ParseTemplate("mail_contact_form.tmpl", data)
|
||||
htmlBody, err := ParseTemplate("mail_contact_form.tmpl", data)
|
||||
if err != nil {
|
||||
logger.Error.Print("Couldn't send contact form message mail")
|
||||
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
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/internal/repositories"
|
||||
"GoMembership/internal/utils"
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
|
||||
"github.com/alexedwards/argon2id"
|
||||
"gorm.io/gorm"
|
||||
@@ -18,13 +15,17 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
GetUserByID(id uint) (*models.User, error)
|
||||
GetUsers(where map[string]interface{}) (*[]models.User, error)
|
||||
VerifyUser(token *string) (*models.User, error)
|
||||
UpdateUser(user *models.User) (*models.User, 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 {
|
||||
@@ -81,7 +82,7 @@ func (service *UserService) UpdateUser(user *models.User) (*models.User, error)
|
||||
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)
|
||||
|
||||
@@ -89,49 +90,20 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
|
||||
user.CreatedAt = time.Now()
|
||||
user.UpdatedAt = time.Now()
|
||||
user.PaymentStatus = constants.AwaitingPaymentStatus
|
||||
// if user.Licence == nil {
|
||||
// user.Licence = &models.Licence{Status: constants.UnverifiedStatus}
|
||||
// }
|
||||
user.BankAccount.MandateDateSigned = time.Now()
|
||||
id, err := service.Repo.CreateUser(user)
|
||||
|
||||
id, err = service.Repo.CreateUser(user)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
user.ID = id
|
||||
|
||||
token, err := utils.GenerateVerificationToken()
|
||||
token, err = service.SetVerificationToken(&id, &constants.VerificationTypes.Email)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (service *UserService) GetUserByID(id uint) (*models.User, error) {
|
||||
|
||||
return repositories.GetUserByID(&id)
|
||||
}
|
||||
|
||||
@@ -146,35 +118,6 @@ func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.Us
|
||||
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 {
|
||||
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ type ValidationKeys struct {
|
||||
JwtGenerationFailed string
|
||||
Duplicate string
|
||||
InvalidUserID string
|
||||
PasswordAlreadyChanged string
|
||||
UserDisabled string
|
||||
}
|
||||
|
||||
type ValidationFields struct {
|
||||
General string
|
||||
ParentMemberShipID string
|
||||
SubscriptionModel string
|
||||
UserID string
|
||||
Login string
|
||||
Email string
|
||||
User string
|
||||
@@ -60,14 +61,15 @@ var Responses = struct {
|
||||
UserNotFoundWrongPassword: "server.validation.user_not_found_or_wrong_password",
|
||||
JwtGenerationFailed: "server.error.jwt_generation_failed",
|
||||
Duplicate: "server.validation.duplicate",
|
||||
UserDisabled: "server.validation.user_disabled",
|
||||
PasswordAlreadyChanged: "server.validation.password_already_changed",
|
||||
},
|
||||
Fields: ValidationFields{
|
||||
General: "general",
|
||||
ParentMemberShipID: "parent_membership_id",
|
||||
SubscriptionModel: "subscription_model",
|
||||
UserID: "user_id",
|
||||
Login: "login",
|
||||
Email: "email",
|
||||
User: "user",
|
||||
Login: "user.login",
|
||||
Email: "user.email",
|
||||
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>
|
||||
<html>
|
||||
<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>
|
||||
<html>
|
||||
<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>
|
||||
<html>
|
||||
<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>
|
||||
<html>
|
||||
<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