Compare commits

..

30 Commits

Author SHA1 Message Date
Alex
9d25fa005c gorm model fixes 2025-05-20 12:41:21 +02:00
Alex
20a693a80c Setting Consents userid null upon deletion 2025-05-14 18:25:53 +02:00
Alex
242d37713d unscoped delete 2025-05-14 15:36:20 +02:00
Alex
4b83be6ed8 templates and constraints 2025-05-14 15:27:10 +02:00
Alex
8fcb73f24d logging 2025-05-14 12:36:46 +02:00
Alex
11740cb503 fixes in prod 2025-05-14 12:29:47 +02:00
Alex
06f8078b17 chg: mail templates 2025-05-14 12:13:12 +02:00
Alex
18f5dadb06 wip 2025-04-10 15:40:22 +02:00
Alex
87f08dd3be subscription_model -> subscription 2025-03-24 18:00:57 +01:00
Alex
2af4575ff2 new db management 2025-03-24 17:46:25 +01:00
Alex
560623788a refactor 2025-03-24 17:46:11 +01:00
Alex
5d55f5a8d9 add new db constraints and foreignKey mode 2025-03-24 17:45:55 +01:00
Alex
28dfe7ecde refactoring 2025-03-24 17:45:33 +01:00
Alex
741145b960 added Opponent 2025-03-24 17:44:45 +01:00
Alex
490b29295f subscription_validation 2025-03-15 00:14:51 +01:00
Alex
ded5e6ceb1 tests 2025-03-15 00:14:31 +01:00
Alex
b81804439e moved to flat json handling 2025-03-15 00:14:21 +01:00
Alex
9a9af9f002 returning safeUsers now 2025-03-15 00:13:53 +01:00
Alex
cd495584b0 model work 2025-03-15 00:13:23 +01:00
Alex
bbead3c43b backend errors typo 2025-03-15 00:12:59 +01:00
Alex
ce18324391 backend: add car 2025-03-15 00:12:46 +01:00
Alex
c9d5a88dbf frontend: add car handling 2025-03-15 00:12:00 +01:00
Alex
380fee09c1 add: carEditForm 2025-03-15 00:11:41 +01:00
Alex
e35524132e add: car processing 2025-03-15 00:11:27 +01:00
Alex
af000aa4bc add: car date handline 2025-03-15 00:11:10 +01:00
Alex
90ed0925ca new defaults (car) 2025-03-15 00:10:47 +01:00
Alex
7c0a6fedb5 locale 2025-03-15 00:10:25 +01:00
Alex
0e6edc8e65 moved to default subscription in editform 2025-03-15 00:10:16 +01:00
Alex
45a219625a mod: supporter summary 2025-03-12 17:14:20 +01:00
Alex
ee10389f1d changed supporter default to passiv 2025-03-12 17:13:44 +01:00
66 changed files with 3467 additions and 1284 deletions

58
frontend/src/app.d.ts vendored
View File

@@ -17,7 +17,7 @@ interface Membership {
start_date: string | ''; start_date: string | '';
end_date: string | ''; end_date: string | '';
parent_member_id: number | -1; parent_member_id: number | -1;
subscription_model: Subscription; subscription: Subscription;
} }
interface BankAccount { interface BankAccount {
@@ -51,7 +51,6 @@ interface User {
last_name: string | ''; last_name: string | '';
password: string | ''; password: string | '';
phone: string | ''; phone: string | '';
notes: string | '';
address: string | ''; address: string | '';
zip_code: string | ''; zip_code: string | '';
city: string | ''; city: string | '';
@@ -60,11 +59,51 @@ interface User {
role_id: number | -1; role_id: number | -1;
dateofbirth: string | ''; dateofbirth: string | '';
company: string | ''; company: string | '';
profile_picture: string | ''; membership: Membership | null;
payment_status: number | -1; bank_account: BankAccount | null;
membership: Membership; licence: Licence | null;
bank_account: BankAccount; notes: string | '';
licence: Licence; }
interface Car {
id: number | -1;
name: string | '';
status: number | 0;
brand: string | '';
model: string | '';
price: number | 0;
rate: number | 0;
start_date: string | '';
end_date: string | '';
color: string | '';
licence_plate: string | '';
location: Location | null;
damages: Damage[] | null;
insurances: Insurance[] | null;
notes: string | '';
}
interface Location {
latitude: number | 0;
longitude: number | 0;
}
interface Damage {
id: number | -1;
name: string | '';
opponent: User | null;
driver_id: number | -1;
insurance: Insurance | null;
date: string | '';
notes: string | '';
}
interface Insurance {
id: number | -1;
company: string | '';
reference: string | '';
start_date: string | '';
end_date: string | '';
notes: string | ''; notes: string | '';
} }
@@ -74,6 +113,7 @@ declare global {
interface Locals { interface Locals {
user: User; user: User;
users: User[]; users: User[];
cars: Cars[];
subscriptions: Subscription[]; subscriptions: Subscription[];
licence_categories: LicenceCategory[]; licence_categories: LicenceCategory[];
} }
@@ -84,6 +124,10 @@ declare global {
licence: Licence; licence: Licence;
licenceCategory: LicenceCategory; licenceCategory: LicenceCategory;
bankAccount: BankAccount; bankAccount: BankAccount;
car: Car;
insurance: Insurance;
location: Location;
damage: Damage;
} }
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}

View File

@@ -0,0 +1,682 @@
<script>
import InputField from '$lib/components/InputField.svelte';
import SmallLoader from '$lib/components/SmallLoader.svelte';
import { createEventDispatcher } from 'svelte';
import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n';
import { defaultDamage, defaultInsurance, defaultOpponent } from '$lib/utils/defaults';
import { PERMISSIONS } from '$lib/utils/constants';
import Modal from './Modal.svelte';
import UserEditForm from './UserEditForm.svelte';
const dispatch = createEventDispatcher();
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form;
/** @type {App.Locals['user'] } */
export let editor;
/** @type {App.Locals['users'] } */
export let users;
/** @type {App.Types['car']} */
export let car;
$: console.log(
'damage.opponent changed:',
car?.damages.map((d) => d.opponent)
);
$: console.log(
'damage.insurance changed:',
car?.damages.map((d) => d.insurance)
);
// TODO: Remove when working
// $: if (car.damages.length > 0 && !car.damages.every((d) => d.insurance && d.opponent)) {
// car.damages = car.damages.map((damage) => ({
// ...damage,
// insurance: damage.insurance ?? defaultInsurance(),
// opponent: damage.opponent ?? defaultOpponent()
// }));
// }
let initialized = false; // Prevents infinite loops
// Ensure damages have default values once `car` is loaded
$: if (car && !initialized) {
car = {
...car,
damages:
car.damages?.map((damage) => ({
...damage,
insurance: damage.insurance ?? defaultInsurance(),
opponent: damage.opponent ?? defaultOpponent()
})) || []
};
initialized = true; // Prevents re-running
}
$: isLoading = car === undefined || editor === undefined;
let isUpdating = false;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
/** @type {number | null} */
let editingUserIndex = null;
const TABS = ['car.car', 'insurance', 'car.damages'];
let activeTab = TABS[0];
/** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = async () => {
isUpdating = true;
return async ({ result }) => {
isUpdating = false;
if (result.type === 'success' || result.type === 'redirect') {
dispatch('close');
} else {
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
}
await applyAction(result);
};
};
</script>
{#if isLoading}
<SmallLoader width={30} message={$t('loading.car_data')} />
{:else if editor && car}
<form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}>
<input name="car[id]" type="hidden" bind:value={car.id} />
<h1 class="step-title" style="text-align: center;">
{car.id ? $t('car.edit') : $t('car.create')}
</h1>
{#if form?.errors}
{#each form?.errors as error (error.id)}
<h4
class="step-subtitle warning"
in:receive|global={{ key: error.id }}
out:send|global={{ key: error.id }}
>
{$t(error.field) + ': ' + $t(error.key)}
</h4>
{/each}
{/if}
<div class="button-container">
{#each TABS as tab}
<button
type="button"
class="button-dark"
class:active={activeTab === tab}
on:click={() => (activeTab = tab)}
>
{$t(tab)}
</button>
{/each}
</div>
<div class="tab-content" style="display: {activeTab === 'car.car' ? 'block' : 'none'}">
<InputField
name="car[name]"
label={$t('name')}
bind:value={car.name}
placeholder={$t('placeholder.car_name')}
readonly={readonlyUser}
/>
<InputField
name="car[brand]"
label={$t('car.brand')}
bind:value={car.brand}
placeholder={$t('placeholder.car_brand')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[model]"
label={$t('car.model')}
bind:value={car.model}
placeholder={$t('placeholder.car_model')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[color]"
label={$t('color')}
bind:value={car.color}
placeholder={$t('placeholder.car_color')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[licence_plate]"
label={$t('car.licence_plate')}
bind:value={car.licence_plate}
placeholder={$t('placeholder.car_licence_plate')}
required={true}
toUpperCase={true}
readonly={readonlyUser}
/>
<InputField
name="car[price]"
type="number"
label={$t('price')}
bind:value={car.price}
readonly={readonlyUser}
/>
<InputField
name="car[rate]"
type="number"
label={$t('car.leasing_rate')}
bind:value={car.rate}
readonly={readonlyUser}
/>
<InputField
name="car[start_date]"
type="date"
label={$t('car.start_date')}
bind:value={car.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[end_date]"
type="date"
label={$t('car.end_date')}
bind:value={car.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[notes]"
type="textarea"
label={$t('notes')}
bind:value={car.notes}
placeholder={$t('placeholder.notes', {
values: { name: car.name || car.brand + ' ' + car.model }
})}
rows={10}
/>
</div>
<div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}">
<div class="accordion">
{#each car.insurances as insurance, index}
<input hidden value={insurance?.id} name="car[insurances][{index}][id]" />
<details class="accordion-item" open={index === car.insurances.length - 1}>
<summary class="accordion-header">
{insurance.company ? insurance.company : ''}
{insurance.reference ? ' (' + insurance.reference + ')' : ''}
</summary>
<div class="accordion-content">
<InputField
name="car[insurances][{index}][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][reference]"
label={$t('insurance_reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurances][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.insurance_deletion', {
values: {
name: insurance.company + ' (' + insurance.reference + ')'
}
})
)
) {
car.insurances = car.insurances.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
<div class="button-group">
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.insurances = [...car.insurances, defaultInsurance()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
</div>
<div class="tab-content" style="display: {activeTab === 'car.damages' ? 'block' : 'none'}">
<div class="accordion">
{#each car.damages as damage, index (damage.id)}
<input type="hidden" name="car[damages][{index}][id]" value={damage.id} />
<details class="accordion-item" open={index === car.damages.length - 1}>
<summary class="accordion-header">
<span class="nav-badge">
{damage.name} -
{damage.opponent.first_name}
{damage.opponent.last_name}
</span>
</summary>
<div class="accordion-content">
<InputField
name="car[damages][{index}][date]"
type="date"
label={$t('date')}
bind:value={damage.date}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][name]"
label={$t('car.damages')}
bind:value={damage.name}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][driver_id]"
type="select"
label={$t('user.member')}
options={users
?.filter((u) => u.role_id > 0)
.map((u) => ({
value: u.id,
label: `${u.first_name} ${u.last_name}`,
color: '--subtext1'
})) || []}
bind:value={damage.driver_id}
readonly={readonlyUser}
/>
<h4>{$t('user.opponent')}</h4>
<input
hidden
name={`car[damages][${index}][opponent][id]`}
value={car.damages[index].opponent.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][email]`}
value={car.damages[index].opponent.email}
/>
<input
hidden
name={`car[damages][${index}][opponent][first_name]`}
value={car.damages[index].opponent.first_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][last_name]`}
value={damage.opponent.last_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][phone]`}
value={damage.opponent.phone}
/>
<input
hidden
name={`car[damages][${index}][opponent][address]`}
value={damage.opponent.address}
/>
<input
hidden
name={`car[damages][${index}][opponent][city]`}
value={damage.opponent.city}
/>
<input
hidden
name={`car[damages][${index}][opponent][zip_code]`}
value={damage.opponent.zip_code}
/>
<input
hidden
name={`car[damages][${index}][opponent][notes]`}
value={damage.opponent.notes}
/>
<input
hidden
name={`car[damages][${index}][opponent][role_id]`}
value={damage.opponent.role_id}
/>
<input
hidden
name={`car[damages][${index}][opponent][status]`}
value={damage.opponent.status}
/>
<input
hidden
name={`car[damages][${index}][opponent][dateofbirth]`}
value={damage.opponent.dateofbirth}
/>
<input
hidden
name={`car[damages][${index}][opponent][company]`}
value={damage.opponent.company}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][id]`}
value={damage.opponent.bank_account.id}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_date_signed]`}
value={damage.opponent.bank_account.mandate_date_signed}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bank]`}
value={damage.opponent.bank_account.bank}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][account_holder_name]`}
value={damage.opponent.bank_account.account_holder_name}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][iban]`}
value={damage.opponent.bank_account.iban}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][bic]`}
value={damage.opponent.bank_account.bic}
/>
<input
hidden
name={`car[damages][${index}][opponent][bank_account][mandate_reference]`}
value={damage.opponent.bank_account.mandate_reference}
/>
<details class="accordion-item">
<summary class="accordion-header">
<span class="nav-badge">
{#if damage.opponent?.first_name}
{damage.opponent.first_name} {damage.opponent.last_name}
{:else}
{$t('not_set')}
{/if}
</span>
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('email')}</th>
<td>{damage.opponent?.email || '-'}</td>
</tr>
<tr>
<th>{$t('phone')}</th>
<td>{damage.opponent?.phone || '-'}</td>
</tr>
<tr>
<th>{$t('address')}</th>
<td>{damage.opponent?.address || '-'}</td>
</tr>
<tr>
<th>{$t('city')}</th>
<td>{damage.opponent?.city || '-'}</td>
</tr>
<tr>
<th>{$t('zip_code')}</th>
<td>{damage.opponent?.zip_code || '-'}</td>
</tr>
</tbody>
</table>
<div class="button-group">
<button
type="button"
class="btn primary"
on:click={() => {
if (!damage.opponent) {
damage.opponent = defaultOpponent();
}
editingUserIndex = index;
}}
>
<i class="fas fa-edit"></i>
{damage.opponent?.id ? $t('edit') : $t('edit')}
</button>
</div>
</div>
</details>
<input
hidden
name={`car[damages][${index}][insurance][id]`}
value={damage.insurance.id}
/>
<input hidden name={`car[damages][${index}][insurance][start_date]`} value="" />
<input hidden name={`car[damages][${index}][insurance][end_date]`} value="" />
<InputField
name="car[damages][{index}][insurance][company]"
label={$t('insurance')}
bind:value={damage.insurance.company}
placeholder={$t('placeholder.company')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][insurance][reference]"
label={$t('insurance_reference')}
bind:value={damage.insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
readonly={readonlyUser}
/>
<InputField
name="car[damages][{index}][notes]"
type="textarea"
label={$t('notes')}
bind:value={damage.notes}
placeholder={$t('placeholder.notes')}
rows={10}
/>
{#if hasPrivilige(editor, PERMISSIONS.Delete)}
<button
type="button"
class="btn btn-delete danger"
on:click={() => {
if (
confirm(
$t('dialog.damage_deletion', {
values: {
name: damage.name
}
})
)
) {
car.damages = car.damages.filter((_, i) => i !== index);
}
}}
>
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
{/if}
</div>
</details>
{/each}
</div>
{#if hasPrivilige(editor, PERMISSIONS.Create)}
<button
type="button"
class="btn primary"
on:click={() => {
car.damages = [...car.damages, defaultDamage()];
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
<div class="button-container">
{#if isUpdating}
<SmallLoader width={30} message={$t('loading.updating')} />
{:else}
<button type="button" class="button-dark" on:click={() => dispatch('cancel')}>
{$t('cancel')}</button
>
<button type="submit" class="button-dark">{$t('confirm')}</button>
{/if}
</div>
</form>
{/if}
{#if editingUserIndex !== null}
<Modal on:close={close}>
<UserEditForm
{form}
submit_form={false}
subscriptions={null}
licence_categories={null}
{editor}
bind:user={car.damages[editingUserIndex].opponent}
on:cancel={() => (editingUserIndex = null)}
on:close={() => {
car.damages = car.damages;
editingUserIndex = null;
}}
/>
</Modal>
{/if}
<style>
.accordion-item {
border: none;
background: var(--surface0);
margin-bottom: 0.5rem;
border-radius: 8px;
overflow: hidden;
}
.accordion-header {
display: flex;
padding: 1rem;
cursor: pointer;
font-family: 'Roboto Mono', monospace;
color: var(--text);
background: var(--surface1);
transition: background-color 0.2s ease-in-out;
}
.accordion-header:hover {
background: var(--surface2);
}
.accordion-content {
padding: 1rem;
background: var(--surface0);
border-top: 1px solid var(--surface1);
}
.accordion-content .table {
width: 100%;
border-collapse: collapse;
font-family: 'Roboto Mono', monospace;
}
.accordion-content .table th,
.accordion-content .table td {
padding: 0.75rem;
border-bottom: 1px solid #2f2f2f;
text-align: left;
}
.accordion-content .table th {
color: var(--subtext1);
}
.accordion-content .table td {
color: var(--text);
}
.button-container button.active {
background-color: var(--mauve);
border-color: var(--mauve);
color: var(--base);
}
.btn-delete {
margin-left: auto;
}
.tab-content {
padding: 1rem;
border-radius: 0 0 3px 3px;
background-color: var(--surface0);
border: 1px solid var(--surface1);
margin-top: 1rem;
}
.tab-content h4 {
text-align: center;
padding: 0.75rem;
margin: 1rem 0;
color: var(--lavender);
font-family: 'Roboto Mono', monospace;
font-weight: 500;
letter-spacing: 0.5px;
}
.button-container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
margin-top: 1rem;
width: 100%;
}
.button-container button {
flex: 1 1 0;
min-width: 120px;
max-width: calc(50% - 5px);
background-color: var(--surface1);
color: var(--text);
border: 1px solid var(--overlay0);
transition: all 0.2s ease-in-out;
}
.button-container button:hover {
background-color: var(--surface2);
border-color: var(--lavender);
}
@media (max-width: 480px) {
.button-container button {
flex-basis: 100%;
max-width: none;
}
}
</style>

View File

@@ -50,9 +50,9 @@
let inputValue = target.value; let inputValue = target.value;
if (toUpperCase) { if (toUpperCase) {
inputValue = inputValue.toUpperCase(); inputValue = inputValue.toUpperCase();
target.value = inputValue; // Update the input field value
} }
value = inputValue; target.value = inputValue; // Update the input field value
value = inputValue.trim();
} }
} }

View File

@@ -5,6 +5,7 @@
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers'; import { receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { defaultSubscription } from '$lib/utils/defaults';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -17,20 +18,8 @@
/** @type {App.Types['subscription'] | null} */ /** @type {App.Types['subscription'] | null} */
export let subscription; export let subscription;
/** @type {App.Types['subscription']} */
const blankSubscription = {
id: 0,
name: '',
details: '',
conditions: '',
monthly_fee: 0,
hourly_rate: 0,
included_hours_per_year: 0,
included_hours_per_month: 0
};
console.log('Opening subscription modal with:', subscription); console.log('Opening subscription modal with:', subscription);
$: subscription = subscription || { ...blankSubscription }; $: subscription = subscription || { ...defaultSubscription() };
$: isLoading = subscription === undefined || user === undefined; $: isLoading = subscription === undefined || user === undefined;
let isUpdating = false; let isUpdating = false;
@@ -55,7 +44,7 @@
<form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}> <form class="content" action="?/updateSubscription" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={subscription.id} /> <input name="susbscription[id]" type="hidden" bind:value={subscription.id} />
<h1 class="step-title" style="text-align: center;"> <h1 class="step-title" style="text-align: center;">
{subscription.id ? $t('subscription.edit') : $t('subscription.create')} {subscription.id ? $t('subscriptions.edit') : $t('subscriptions.create')}
</h1> </h1>
{#if form?.errors} {#if form?.errors}
{#each form?.errors as error (error.id)} {#each form?.errors as error (error.id)}
@@ -71,7 +60,7 @@
<div class="tab-content" style="display: block"> <div class="tab-content" style="display: block">
<InputField <InputField
name="subscription[name]" name="subscription[name]"
label={$t('subscription.name')} label={$t('subscriptions.name')}
bind:value={subscription.name} bind:value={subscription.name}
placeholder={$t('placeholder.subscription_name')} placeholder={$t('placeholder.subscription_name')}
required={true} required={true}
@@ -88,7 +77,7 @@
<InputField <InputField
name="subscription[conditions]" name="subscription[conditions]"
type="textarea" type="textarea"
label={$t('subscription.conditions')} label={$t('subscriptions.conditions')}
bind:value={subscription.conditions} bind:value={subscription.conditions}
placeholder={$t('placeholder.subscription_conditions')} placeholder={$t('placeholder.subscription_conditions')}
readonly={subscription.id > 0} readonly={subscription.id > 0}
@@ -96,7 +85,7 @@
<InputField <InputField
name="subscription[monthly_fee]" name="subscription[monthly_fee]"
type="number" type="number"
label={$t('subscription.monthly_fee')} label={$t('subscriptions.monthly_fee')}
bind:value={subscription.monthly_fee} bind:value={subscription.monthly_fee}
placeholder={$t('placeholder.subscription_monthly_fee')} placeholder={$t('placeholder.subscription_monthly_fee')}
required={true} required={true}
@@ -105,7 +94,7 @@
<InputField <InputField
name="subscription[hourly_rate]" name="subscription[hourly_rate]"
type="number" type="number"
label={$t('subscription.hourly_rate')} label={$t('subscriptions.hourly_rate')}
bind:value={subscription.hourly_rate} bind:value={subscription.hourly_rate}
required={true} required={true}
readonly={subscription.id > 0} readonly={subscription.id > 0}
@@ -113,14 +102,14 @@
<InputField <InputField
name="subscription[included_hours_per_year]" name="subscription[included_hours_per_year]"
type="number" type="number"
label={$t('subscription.included_hours_per_year')} label={$t('subscriptions.included_hours_per_year')}
bind:value={subscription.included_hours_per_year} bind:value={subscription.included_hours_per_year}
readonly={subscription.id > 0} readonly={subscription.id > 0}
/> />
<InputField <InputField
name="included_hours_per_month" name="included_hours_per_month"
type="number" type="number"
label={$t('subscription.included_hours_per_month')} label={$t('subscriptions.included_hours_per_month')}
bind:value={subscription.included_hours_per_month} bind:value={subscription.included_hours_per_month}
readonly={subscription.id > 0} readonly={subscription.id > 0}
/> />

View File

@@ -6,22 +6,29 @@
import { hasPrivilige, receive, send } from '$lib/utils/helpers'; import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { PERMISSIONS } from '$lib/utils/constants'; import { PERMISSIONS } from '$lib/utils/constants';
import { defaultLicence } from '$lib/utils/defaults'; // import { defaultBankAccount, defaultLicence, defaultMembership } from '$lib/utils/defaults';
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */ /** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form; export let form;
/** @type {App.Locals['subscriptions']}*/ /** @type {App.Locals['subscriptions'] | null}*/
export let subscriptions; export let subscriptions;
/** @type {App.Locals['user']} */ /** @type {App.Locals['user']} */
export let user; export let user;
// Ensure licence is initialized before passing to child export let submit_form = true;
$: if (user && !user.licence) {
user.licence = defaultLicence();
}
// Ensure licence is initialized before passing to child
// $: if (user && !user.licence) {
// user.licence = defaultLicence();
// }
// $: if (user && !user.membership) {
// user.membership = defaultMembership();
// }
// $: if (user && !user.bank_account) {
// user.bank_account = defaultBankAccount();
// }
/** @type {App.Locals['user']} */ /** @type {App.Locals['user']} */
export let editor; export let editor;
@@ -29,8 +36,10 @@
// $: isNewUser = user === null; // $: isNewUser = user === null;
$: isLoading = user === undefined; $: isLoading = user === undefined;
$: if (user != null) {
/** @type {App.Locals['licence_categories']} */ console.log(user);
}
/** @type {App.Locals['licence_categories'] | null} */
export let licence_categories; export let licence_categories;
const userStatusOptions = [ const userStatusOptions = [
@@ -41,6 +50,7 @@
{ value: 5, label: $t('userStatus.5'), color: '--red' } // Red for "Deaktiviert" { value: 5, label: $t('userStatus.5'), color: '--red' } // Red for "Deaktiviert"
]; ];
const userRoleOptions = [ const userRoleOptions = [
{ value: -1, label: $t('userRole.-1'), color: '--red' }, // Red for "Opponent"
{ value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert" { value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert"
{ value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert" { value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert"
{ value: 2, label: $t('userRole.2'), color: '--green' }, // Light green for "Verifiziert" { value: 2, label: $t('userRole.2'), color: '--green' }, // Light green for "Verifiziert"
@@ -60,24 +70,26 @@
]; ];
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const TABS = hasPrivilige(user, PERMISSIONS.Member) /** @type { (keyof user)[] } */
? ['profile', 'licence', 'membership', 'bankaccount'] const TABS = ['membership', 'licence', 'bank_account'];
: ['profile', 'bankaccount', 'membership'];
let activeTab = TABS[0]; let activeTab = 'profile';
let isUpdating = false, let isUpdating = false,
password = '', password = '',
confirm_password = ''; confirm_password = '';
/** @type {Object.<string, App.Locals['licence_categories']>} */ /** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = groupCategories(licence_categories); $: groupedCategories = licence_categories ? groupCategories(licence_categories) : {};
$: subscriptionModelOptions = subscriptions.map((sub) => ({ $: subscriptionOptions = subscriptions
value: sub?.name ?? '', ? subscriptions.map((sub) => ({
label: sub?.name ?? '' value: sub?.name ?? '',
})); label: sub?.name ?? ''
$: selectedSubscriptionModel = }))
subscriptions.find((sub) => sub?.name === user.membership?.subscription_model.name) || null; : [];
$: selectedSubscription = subscriptions
? subscriptions.find((sub) => sub?.name === user.membership?.subscription.name) || null
: null;
/** /**
* creates groups of categories depending on the first letter * creates groups of categories depending on the first letter
* @param {App.Locals['licence_categories']} categories - the categories to sort and group * @param {App.Locals['licence_categories']} categories - the categories to sort and group
@@ -99,17 +111,25 @@
); );
} }
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */ /** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = async () => { const handleUpdate = ({ cancel }) => {
if (!submit_form) {
cancel();
dispatch('close');
return;
}
isUpdating = true; isUpdating = true;
return async ({ result }) => { return async ({ result }) => {
isUpdating = false; isUpdating = false;
if (result.type === 'success' || result.type === 'redirect') { if (result.type === 'success' || result.type === 'redirect') {
dispatch('close'); dispatch('close');
} else { } else {
document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' }); document.querySelector('.modal .container')?.scrollTo({ top: 0, behavior: 'smooth' });
} }
await applyAction(result); console.log('submitting');
return submit_form ? await applyAction(result) : undefined;
}; };
}; };
</script> </script>
@@ -117,7 +137,18 @@
{#if isLoading} {#if isLoading}
<SmallLoader width={30} message={$t('loading.user_data')} /> <SmallLoader width={30} message={$t('loading.user_data')} />
{:else if user} {:else if user}
<form class="content" action="?/updateUser" method="POST" use:enhance={handleUpdate}> <form
class="content"
action="?/updateUser"
method="POST"
use:enhance={handleUpdate}
on:submit={(/** @type{SubmitEvent}*/ e) => {
if (!submit_form) {
e.preventDefault();
dispatch('close');
}
}}
>
<input name="user[id]" type="hidden" bind:value={user.id} /> <input name="user[id]" type="hidden" bind:value={user.id} />
<h1 class="step-title" style="text-align: center;"> <h1 class="step-title" style="text-align: center;">
{user.id ? $t('user.edit') : $t('user.create')} {user.id ? $t('user.edit') : $t('user.create')}
@@ -145,26 +176,38 @@
{/if} {/if}
<div class="button-container"> <div class="button-container">
<button
type="button"
class="button-dark"
class:active={activeTab === 'profile'}
on:click={() => (activeTab = 'profile')}
>
{$t('profile')}
</button>
{#each TABS as tab} {#each TABS as tab}
<button {#if user[tab] != null}
type="button" <button
class="button-dark" type="button"
class:active={activeTab === tab} class="button-dark"
on:click={() => (activeTab = tab)} class:active={activeTab === tab}
> on:click={() => (activeTab = tab)}
{$t(tab)} >
</button> {$t('user.' + tab)}
</button>
{/if}
{/each} {/each}
</div> </div>
<div class="tab-content" style="display: {activeTab === 'profile' ? 'block' : 'none'}"> <div class="tab-content" style="display: {activeTab === 'profile' ? 'block' : 'none'}">
<InputField {#if hasPrivilige(user, PERMISSIONS.Member)}
name="user[status]" <InputField
type="select" name="user[status]"
label={$t('status')} type="select"
bind:value={user.status} label={$t('status')}
options={userStatusOptions} bind:value={user.status}
readonly={readonlyUser} options={userStatusOptions}
/> readonly={readonlyUser}
/>
{/if}
{#if hasPrivilige(editor, PERMISSIONS.Super)} {#if hasPrivilige(editor, PERMISSIONS.Super)}
<InputField <InputField
name="user[role_id]" name="user[role_id]"
@@ -229,16 +272,14 @@
bind:value={user.phone} bind:value={user.phone}
placeholder={$t('placeholder.phone')} placeholder={$t('placeholder.phone')}
/> />
{#if hasPrivilige(user, PERMISSIONS.Member)} <InputField
<InputField name="user[dateofbirth]"
name="user[dateofbirth]" type="date"
type="date" label={$t('user.dateofbirth')}
label={$t('user.dateofbirth')} bind:value={user.dateofbirth}
bind:value={user.dateofbirth} placeholder={$t('placeholder.dateofbirth')}
placeholder={$t('placeholder.dateofbirth')} readonly={readonlyUser}
readonly={readonlyUser} />
/>
{/if}
<InputField <InputField
name="user[address]" name="user[address]"
label={$t('address')} label={$t('address')}
@@ -271,7 +312,7 @@
{/if} {/if}
</div> </div>
{#if hasPrivilige(user, PERMISSIONS.Member)} {#if hasPrivilige(user, PERMISSIONS.Member) && user.licence}
<div class="tab-content" style="display: {activeTab === 'licence' ? 'block' : 'none'}"> <div class="tab-content" style="display: {activeTab === 'licence' ? 'block' : 'none'}">
<InputField <InputField
name="user[licence][status]" name="user[licence][status]"
@@ -342,130 +383,137 @@
</div> </div>
</div> </div>
{/if} {/if}
<div class="tab-content" style="display: {activeTab === 'membership' ? 'block' : 'none'}"> {#if user.membership}
<InputField <div
name="user[membership][status]" class="tab-content"
type="select" style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}"
label={$t('status')} >
bind:value={user.membership.status} <InputField
options={membershipStatusOptions} name="user[membership][status]"
readonly={readonlyUser} type="select"
/> label={$t('status')}
<InputField bind:value={user.membership.status}
name="user[membership][subscription_model][name]" options={membershipStatusOptions}
type="select" readonly={readonlyUser}
label={$t('subscription.subscription')} />
bind:value={user.membership.subscription_model.name} <InputField
options={subscriptionModelOptions} name="user[membership][subscription][name]"
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)} type="select"
/> label={$t('subscriptions.subscription')}
<div class="subscription-info"> bind:value={user.membership.subscription.name}
{#if hasPrivilige(user, PERMISSIONS.Member)} options={subscriptionOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/>
<div class="subscription-info">
{#if hasPrivilige(user, PERMISSIONS.Member)}
<div class="subscription-column">
<p>
<strong>{$t('subscriptions.monthly_fee')}:</strong>
{selectedSubscription?.monthly_fee || '-'}
</p>
<p>
<strong>{$t('subscriptions.hourly_rate')}:</strong>
{selectedSubscription?.hourly_rate || '-'}
</p>
{#if selectedSubscription?.included_hours_per_year}
<p>
<strong>{$t('subscriptions.included_hours_per_year')}:</strong>
{selectedSubscription?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscription?.included_hours_per_month}
<p>
<strong>{$t('subscriptions.included_hours_per_month')}:</strong>
{selectedSubscription?.included_hours_per_month}
</p>
{/if}
</div>
{/if}
<div class="subscription-column"> <div class="subscription-column">
<p> <p>
<strong>{$t('subscription.monthly_fee')}:</strong> <strong>{$t('details')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'} {selectedSubscription?.details || '-'}
</p> </p>
<p> {#if selectedSubscription?.conditions}
<strong>{$t('subscription.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'}
</p>
{#if selectedSubscriptionModel?.included_hours_per_year}
<p> <p>
<strong>{$t('subscription.included_hours_per_year')}:</strong> <strong>{$t('subscriptions.conditions')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year} {selectedSubscription?.conditions}
</p>
{/if}
{#if selectedSubscriptionModel?.included_hours_per_month}
<p>
<strong>{$t('subscription.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month}
</p> </p>
{/if} {/if}
</div> </div>
{/if}
<div class="subscription-column">
<p>
<strong>{$t('details')}:</strong>
{selectedSubscriptionModel?.details || '-'}
</p>
{#if selectedSubscriptionModel?.conditions}
<p>
<strong>{$t('subscription.conditions')}:</strong>
{selectedSubscriptionModel?.conditions}
</p>
{/if}
</div> </div>
</div>
<InputField
name="user[membership][start_date]"
type="date"
label={$t('start')}
bind:value={user.membership.start_date}
placeholder={$t('placeholder.start_date')}
readonly={readonlyUser}
/>
<InputField
name="user[membership][end_date]"
type="date"
label={$t('end')}
bind:value={user.membership.end_date}
placeholder={$t('placeholder.end_date')}
readonly={readonlyUser}
/>
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField <InputField
name="user[membership][parent_member_id]" name="user[membership][start_date]"
type="number" type="date"
label={$t('parent_member_id')} label={$t('start')}
bind:value={user.membership.parent_member_id} bind:value={user.membership.start_date}
placeholder={$t('placeholder.parent_member_id')} placeholder={$t('placeholder.start_date')}
readonly={readonlyUser} readonly={readonlyUser}
/> />
{/if} <InputField
</div> name="user[membership][end_date]"
<div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}"> type="date"
<InputField label={$t('end')}
name="user[bank_account][account_holder_name]" bind:value={user.membership.end_date}
label={$t('bank_account_holder')} placeholder={$t('placeholder.end_date')}
bind:value={user.bank_account.account_holder_name} readonly={readonlyUser}
placeholder={$t('placeholder.bank_account_holder')} />
/> {#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField <InputField
name="user[bank_account][bank_name]" name="user[membership][parent_member_id]"
label={$t('bank_name')} type="number"
bind:value={user.bank_account.bank} label={$t('parent_member_id')}
placeholder={$t('placeholder.bank_name')} bind:value={user.membership.parent_member_id}
/> placeholder={$t('placeholder.parent_member_id')}
<InputField readonly={readonlyUser}
name="user[bank_account][iban]" />
label={$t('iban')} {/if}
bind:value={user.bank_account.iban} </div>
placeholder={$t('placeholder.iban')} {/if}
toUpperCase={true} {#if user.bank_account}
/> <div class="tab-content" style="display: {activeTab === 'bank_account' ? 'block' : 'none'}">
<InputField <InputField
name="user[bank_account][bic]" name="user[bank_account][account_holder_name]"
label={$t('bic')} label={$t('bank_account_holder')}
bind:value={user.bank_account.bic} bind:value={user.bank_account.account_holder_name}
placeholder={$t('placeholder.bic')} placeholder={$t('placeholder.bank_account_holder')}
toUpperCase={true} />
/> <InputField
<InputField name="user[bank_account][bank_name]"
name="user[bank_account][mandate_reference]" label={$t('bank_name')}
label={$t('mandate_reference')} bind:value={user.bank_account.bank}
bind:value={user.bank_account.mandate_reference} placeholder={$t('placeholder.bank_name')}
placeholder={$t('placeholder.mandate_reference')} />
readonly={readonlyUser} <InputField
/> name="user[bank_account][iban]"
<InputField label={$t('iban')}
name="user[bank_account][mandate_date_signed]" bind:value={user.bank_account.iban}
label={$t('mandate_date_signed')} placeholder={$t('placeholder.iban')}
type="date" toUpperCase={true}
bind:value={user.bank_account.mandate_date_signed} />
readonly={true} <InputField
/> name="user[bank_account][bic]"
</div> label={$t('bic')}
bind:value={user.bank_account.bic}
placeholder={$t('placeholder.bic')}
toUpperCase={true}
/>
<InputField
name="user[bank_account][mandate_reference]"
label={$t('mandate_reference')}
bind:value={user.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')}
readonly={readonlyUser}
/>
<InputField
name="user[bank_account][mandate_date_signed]"
label={$t('mandate_date_signed')}
type="date"
bind:value={user.bank_account.mandate_date_signed}
readonly={true}
/>
</div>
{/if}
<div class="button-container"> <div class="button-container">
{#if isUpdating} {#if isUpdating}
<SmallLoader width={30} message={$t('loading.updating')} /> <SmallLoader width={30} message={$t('loading.updating')} />

View File

@@ -7,6 +7,7 @@ export default {
5: 'Passiv' 5: 'Passiv'
}, },
userRole: { userRole: {
'-1': 'Unfallgegner',
0: 'Sponsor', 0: 'Sponsor',
1: 'Mitglied', 1: 'Mitglied',
2: 'Betrachter', 2: 'Betrachter',
@@ -14,6 +15,12 @@ export default {
8: 'Administrator' 8: 'Administrator'
}, },
placeholder: { placeholder: {
car_name: 'Hat das Fahrzeug einen Namen?',
car_brand: 'Fahrzeughersteller eingeben...',
car_model: 'Fahrzeugmodell eingeben...',
car_color: 'Fahrzeugfarbe eingeben...',
car_licence_plate: 'Fahrzeugkennzeichen eingeben...',
insurance_reference: 'Versicherungsnummer eingeben...',
password: 'Passwort eingeben...', password: 'Passwort eingeben...',
email: 'Emailadresse eingeben...', email: 'Emailadresse eingeben...',
company: 'Firmennamen eingeben...', company: 'Firmennamen eingeben...',
@@ -69,7 +76,7 @@ export default {
validation: { validation: {
invalid: 'ungültig', invalid: 'ungültig',
invalid_user_id: 'Nutzer ID ungültig', invalid_user_id: 'Nutzer ID ungültig',
invalid_subscription_model: 'Model nicht gefunden', invalid_subscription: 'Model nicht gefunden',
user_not_found: '{field} konnte nicht gefunden werden', user_not_found: '{field} konnte nicht gefunden werden',
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',
@@ -124,6 +131,7 @@ export default {
edit: 'Nutzer bearbeiten', edit: 'Nutzer bearbeiten',
create: 'Nutzer erstellen', create: 'Nutzer erstellen',
user: 'Nutzer', user: 'Nutzer',
member: 'Mitglied',
management: 'Mitgliederverwaltung', management: 'Mitgliederverwaltung',
id: 'Mitgliedsnr', id: 'Mitgliedsnr',
first_name: 'Vorname', first_name: 'Vorname',
@@ -131,11 +139,14 @@ export default {
phone: 'Telefonnummer', phone: 'Telefonnummer',
dateofbirth: 'Geburtstag', dateofbirth: 'Geburtstag',
email: 'Email', email: 'Email',
membership: 'Mitgliedschaft',
bank_account: 'Kontodaten',
status: 'Status', status: 'Status',
role: 'Nutzerrolle', role: 'Nutzerrolle',
supporter: 'Sponsor' supporter: 'Sponsor',
opponent: 'Unfallgegner'
}, },
subscription: { subscriptions: {
name: 'Modellname', name: 'Modellname',
edit: 'Modell bearbeiten', edit: 'Modell bearbeiten',
create: 'Modell erstellen', create: 'Modell erstellen',
@@ -147,32 +158,63 @@ export default {
included_hours_per_year: 'Inkludierte Stunden pro Jahr', included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: 'Inkludierte Stunden pro Monat' included_hours_per_month: 'Inkludierte Stunden pro Monat'
}, },
car: {
car: 'Fahrzeug',
model: 'Modell',
brand: 'Marke',
licence_plate: 'Kennzeichen',
edit: 'Fahrzeug bearbeiten',
create: 'Fahrzeug hinzufügen',
damages: 'Schäden',
start_date: 'Anschaffungsdatum',
end_date: 'Leasingende',
leasing_rate: 'Leasingrate'
},
insurances: {
edit: 'Daten bearbeiten',
create: 'Versicherung erstellen'
},
loading: { loading: {
user_data: 'Lade Nutzerdaten', user_data: 'Lade Nutzerdaten',
subscription_data: 'Lade Modelldaten', subscription_data: 'Lade Modelldaten',
insurance_data: 'Lade Versicherungsdaten',
car_data: 'Lade Fahrzeugdaten',
please_wait: 'Bitte warten...', please_wait: 'Bitte warten...',
updating: 'Aktualisiere...' updating: 'Aktualisiere...'
}, },
dialog: { dialog: {
user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?', user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?',
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?', subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?',
car_deletion: 'Soll das Fahrzeug {name} wirklich gelöscht werden?',
insurance_deletion: 'Soll die Versicherung {name} wirklich gelöscht werden?',
damage_deletion: 'Soll der Schaden {name} wirklich gelöscht werden?',
backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?' backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?'
}, },
cancel: 'Abbrechen', cancel: 'Abbrechen',
confirm: 'Bestätigen', confirm: 'Bestätigen',
actions: 'Aktionen', actions: 'Aktionen',
create: 'Hinzufügen',
edit: 'Bearbeiten', edit: 'Bearbeiten',
delete: 'Löschen', delete: 'Löschen',
not_set: 'Nicht gesetzt',
noone: 'Niemand',
search: 'Suche:', search: 'Suche:',
name: 'Name', name: 'Name',
date: 'Datum',
price: 'Preis',
color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren', grant_backend_access: 'Backend Zugriff gewähren',
no_insurance: 'Keine Versicherung',
supporter: 'Sponsoren', supporter: 'Sponsoren',
mandate_date_signed: 'Mandatserteilungsdatum', mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen', licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell', subscription: 'Mitgliedschatfsmodell',
licence: 'Führerschein', licence: 'Führerschein',
licence_number: 'Führerscheinnummer', licence_number: 'Führerscheinnummer',
insurance: 'Versicherung',
insurance_reference: 'Versicherungsnummer',
issued_date: 'Ausgabedatum', issued_date: 'Ausgabedatum',
month: 'Monat',
expiration_date: 'Ablaufdatum', expiration_date: 'Ablaufdatum',
country: 'Land', country: 'Land',
details: 'Details', details: 'Details',
@@ -191,8 +233,7 @@ export default {
company: 'Firma', company: 'Firma',
login: 'Anmeldung', login: 'Anmeldung',
profile: 'Profil', profile: 'Profil',
membership: 'Mitgliedschaft', cars: 'Fahrzeuge',
bankaccount: 'Kontodaten',
status: 'Status', status: 'Status',
start: 'Beginn', start: 'Beginn',
end: 'Ende', end: 'Ende',

View File

@@ -30,7 +30,7 @@ export default {
bic: 'Enter BIC (for non-German accounts)...', bic: 'Enter BIC (for non-German accounts)...',
mandate_reference: 'Enter SEPA mandate reference...', mandate_reference: 'Enter SEPA mandate reference...',
notes: 'Your notes about {name}...', notes: 'Your notes about {name}...',
licence_number: 'On the drivers license under field 5', licence_number: 'On the drivers licence under field 5',
issued_date: 'Issue date under field 4a', issued_date: 'Issue date under field 4a',
expiration_date: 'Expiration date under field 4b', expiration_date: 'Expiration date under field 4b',
issuing_country: 'Issuing country', issuing_country: 'Issuing country',
@@ -69,7 +69,7 @@ export default {
validation: { validation: {
invalid: 'Invalid', invalid: 'Invalid',
invalid_user_id: 'Invalid user ID', invalid_user_id: 'Invalid user ID',
invalid_subscription_model: 'Model not found', invalid_subscription: 'Model not found',
user_not_found: '{field} could not be found', user_not_found: '{field} could not be found',
invalid_user_data: 'Invalid user data', invalid_user_data: 'Invalid user data',
user_not_found_or_wrong_password: 'Does not exist or wrong password', user_not_found_or_wrong_password: 'Does not exist or wrong password',
@@ -81,7 +81,7 @@ export default {
bic: 'Invalid. Format: BELADEBEXXX', bic: 'Invalid. Format: BELADEBEXXX',
email: 'Invalid format', email: 'Invalid format',
number: 'Is not a number', number: 'Is not a number',
euDriversLicence: 'Is not a European drivers license', euDriversLicence: 'Is not a European drivers licence',
lte: 'Is too large/new', lte: 'Is too large/new',
gt: 'Is too small/old', gt: 'Is too small/old',
required: 'Field is required', required: 'Field is required',
@@ -128,7 +128,7 @@ export default {
role: 'User Role', role: 'User Role',
supporter: 'Sponsor' supporter: 'Sponsor'
}, },
subscription: { subscriptions: {
name: 'Model Name', name: 'Model Name',
edit: 'Edit Model', edit: 'Edit Model',
create: 'Create Model', create: 'Create Model',
@@ -159,10 +159,10 @@ export default {
name: 'Name', name: 'Name',
supporter: 'Sponsors', supporter: 'Sponsors',
mandate_date_signed: 'Mandate Signing Date', mandate_date_signed: 'Mandate Signing Date',
licence_categories: 'Drivers License Categories', licence_categories: 'Drivers licence Categories',
subscription_model: 'Membership Model', subscription: 'Membership Model',
licence: 'Drivers License', licence: 'Drivers licence',
licence_number: 'Drivers License Number', licence_number: 'Drivers licence Number',
issued_date: 'Issue Date', issued_date: 'Issue Date',
expiration_date: 'Expiration Date', expiration_date: 'Expiration Date',
country: 'Country', country: 'Country',
@@ -182,7 +182,7 @@ export default {
login: 'Login', login: 'Login',
profile: 'Profile', profile: 'Profile',
membership: 'Membership', membership: 'Membership',
bankaccount: 'Bank Account', bank_account: 'Bank Account',
status: 'Status', status: 'Status',
start: 'Start', start: 'Start',
end: 'End', end: 'End',

View File

@@ -10,5 +10,3 @@ export const PERMISSIONS = {
Delete: 4, Delete: 4,
Super: 8 Super: 8
}; };
export const SUPPORTER_SUBSCRIPTION_MODEL_NAME = 'Keins';

View File

@@ -1,7 +1,5 @@
// src/lib/utils/defaults.js // src/lib/utils/defaults.js
import { SUPPORTER_SUBSCRIPTION_MODEL_NAME } from './constants';
/** /**
* @returns {App.Types['subscription']} * @returns {App.Types['subscription']}
*/ */
@@ -28,7 +26,7 @@ export function defaultMembership() {
start_date: '', start_date: '',
end_date: '', end_date: '',
parent_member_id: 0, parent_member_id: 0,
subscription_model: defaultSubscription() subscription: defaultSubscription()
}; };
} }
@@ -79,8 +77,6 @@ export function defaultUser() {
company: '', company: '',
dateofbirth: '', dateofbirth: '',
notes: '', notes: '',
profile_picture: '',
payment_status: 0,
status: 1, status: 1,
role_id: 1, role_id: 1,
membership: defaultMembership(), membership: defaultMembership(),
@@ -93,27 +89,83 @@ export function defaultUser() {
* @returns {App.Locals['user']} * @returns {App.Locals['user']}
*/ */
export function defaultSupporter() { export function defaultSupporter() {
let supporter = { let supporter = defaultUser();
id: 0, supporter.status = 5;
email: '', supporter.role_id = 0;
first_name: '', supporter.licence = null;
last_name: '', supporter.membership = null;
password: '',
phone: '',
address: '',
zip_code: '',
city: '',
company: '',
dateofbirth: '',
notes: '',
profile_picture: '',
payment_status: 0,
status: 1,
role_id: 0,
membership: defaultMembership(),
licence: defaultLicence(),
bank_account: defaultBankAccount()
};
supporter.membership.subscription_model.name = SUPPORTER_SUBSCRIPTION_MODEL_NAME;
return supporter; return supporter;
} }
/**
* @returns {App.Locals['user']}
*/
export function defaultOpponent() {
let opponent = defaultUser();
opponent.status = 5;
opponent.role_id = -1;
opponent.licence = null;
opponent.membership = null;
return opponent;
}
/**
* @returns {App.Types['location']}
*/
export function defaultLocation() {
return {
latitude: 0,
longitude: 0
};
}
/**
* @returns {App.Types['damage']}
*/
export function defaultDamage() {
return {
id: 0,
name: '',
opponent: defaultOpponent(),
driver_id: -1,
insurance: defaultInsurance(),
date: '',
notes: ''
};
}
/**
* @returns {App.Types['insurance']}
*/
export function defaultInsurance() {
return {
id: 0,
company: '',
reference: '',
start_date: '',
end_date: '',
notes: ''
};
}
/**
* @returns {App.Types['car']}
*/
export function defaultCar() {
return {
id: 0,
name: '',
status: 0,
brand: '',
model: '',
price: 0,
rate: 0,
start_date: '',
end_date: '',
color: '',
licence_plate: '',
location: defaultLocation(),
damages: [],
insurances: [],
notes: ''
};
}

View File

@@ -72,7 +72,7 @@ export function isEmpty(obj) {
* @returns string * @returns string
*/ */
export function toRFC3339(dateString) { export function toRFC3339(dateString) {
if (!dateString) dateString = '0001-01-01T00:00:00.000Z'; if (!dateString || dateString == '') dateString = '0001-01-01T00:00:00.000Z';
const date = new Date(dateString); const date = new Date(dateString);
return date.toISOString(); return date.toISOString();
} }
@@ -88,6 +88,31 @@ export function fromRFC3339(dateString) {
return date.toISOString().split('T')[0]; return date.toISOString().split('T')[0];
} }
/**
*
* @param {App.Types['car']} car - The car object to format
*/
export function carDatesFromRFC3339(car) {
car.end_date = fromRFC3339(car.end_date);
car.start_date = fromRFC3339(car.start_date);
car.insurances?.forEach((insurance) => {
insurance.start_date = fromRFC3339(insurance.start_date);
insurance.end_date = fromRFC3339(insurance.end_date);
});
}
/**
*
* @param {App.Types['car']} car - The car object to format
*/
export function carDatesToRFC3339(car) {
car.end_date = toRFC3339(car.end_date);
car.start_date = toRFC3339(car.start_date);
car.insurances?.forEach((insurance) => {
insurance.start_date = toRFC3339(insurance.start_date);
insurance.end_date = toRFC3339(insurance.end_date);
});
}
/** /**
* *
* @param {App.Locals['user']} user - The user object to format * @param {App.Locals['user']} user - The user object to format

View File

@@ -1,12 +1,13 @@
import { defaultBankAccount, defaultMembership } from './defaults';
import { toRFC3339 } from './helpers'; 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']>, confirm_password: string }} Nested object representation of the form data * @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']> | Partial<App.Types['car']>, 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']> | Partial<App.Types['car']> } */
const object = {}; const object = {};
let confirm_password = ''; let confirm_password = '';
@@ -24,20 +25,18 @@ export function formDataToObject(formData) {
// console.log('Current object state:', JSON.stringify(current)); // console.log('Current object state:', JSON.stringify(current));
for (let i = 0; i < keys.length - 1; i++) { for (let i = 0; i < keys.length - 1; i++) {
/** const currentKey = keys[i];
* Create nested object if it doesn't exist const nextKey = keys[i + 1];
* @type {Record<string, any>} const isNextKeyArrayIndex = !isNaN(Number(nextKey));
* @description Ensures proper nesting structure for user data fields if (!current[currentKey]) {
* @example // If next key is a number, initialize an array, otherwise an object
* // For input name="user[membership][status]" current[currentKey] = isNextKeyArrayIndex ? [] : {};
* // Creates: { user: { membership: { status: value } } } }
*/
current[keys[i]] = current[keys[i]] || {};
/** /**
* Move to the next level of the object * Move to the next level of the object
* @type {Record<string, any>} * @type {Record<string, any>}
*/ */
current = current[keys[i]]; current = current[currentKey];
} }
const lastKey = keys[keys.length - 1]; const lastKey = keys[keys.length - 1];
@@ -50,7 +49,20 @@ export function formDataToObject(formData) {
current[lastKey].push(value); current[lastKey].push(value);
} }
} else { } else {
current[lastKey] = value; if (Array.isArray(current)) {
// If current is an array, lastKey should be the index
const index = parseInt(lastKey);
current[index] = current[index] || {};
if (keys.length > 2) {
// For nested properties within array elements
const propertyKey = keys[keys.length - 1];
current[index][propertyKey] = value;
} else {
current[index] = value;
}
} else {
current[lastKey] = value;
}
} }
} }
@@ -58,82 +70,86 @@ export function formDataToObject(formData) {
} }
/** /**
* Processes the raw form data into the expected user data structure * Processes the raw form data into the expected membership data structure
* @param {{ object: Partial<App.Locals['user']>, confirm_password: string} } rawData - The raw form data object * @param { App.Types['membership'] } membership - The raw form data object
* @returns {{ user: Partial<App.Locals['user']> }} Processed user data * @returns {App.Types['membership']} Processed membership data
*/ */
export function processUserFormData(rawData) { export function processMembershipFormData(membership) {
/** @type {{ user: Partial<App.Locals['user']> }} */ return {
id: Number(membership.id) || 0,
status: Number(membership.status),
start_date: toRFC3339(String(membership.start_date || '')),
end_date: toRFC3339(String(membership.end_date || '')),
parent_member_id: Number(membership.parent_member_id) || 0,
subscription: processSubscriptionFormData(membership.subscription)
};
}
/**
* Processes the raw form data into the expected licence data structure
* @param { App.Types['licence'] } licence - The raw form data object
* @returns {App.Types['licence']} Processed licence data
*/
export function processLicenceFormData(licence) {
return {
id: Number(licence?.id) || 0,
status: Number(licence?.status),
number: String(licence?.number || ''),
issued_date: toRFC3339(String(licence?.issued_date || '')),
expiration_date: toRFC3339(String(licence?.expiration_date || '')),
country: String(licence?.country || ''),
categories: licence?.categories || []
};
}
/**
* Processes the raw form data into the expected bank_account data structure
* @param { App.Types['bankAccount'] } bank_account - The raw form data object
* @returns {App.Types['bankAccount']} Processed bank_account data
*/
export function processBankAccountFormData(bank_account) {
{
return {
id: Number(bank_account?.id) || 0,
account_holder_name: String(bank_account?.account_holder_name || ''),
bank: String(bank_account?.bank || ''),
iban: String(bank_account?.iban || ''),
bic: String(bank_account?.bic || ''),
mandate_reference: String(bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(String(bank_account?.mandate_date_signed || ''))
};
}
}
/**
* Processes the raw form data into the expected user data structure
* @param { Partial<App.Locals['user']> } user - The raw form data object
* @returns {App.Locals['user']} Processed user data
*/
export function processUserFormData(user) {
/** @type {App.Locals['user']} */
let processedData = { let processedData = {
user: { id: Number(user.id) || 0,
id: Number(rawData.object.id) || 0, status: Number(user.status),
status: Number(rawData.object.status), role_id: Number(user.role_id),
role_id: Number(rawData.object.role_id), first_name: String(user.first_name),
first_name: String(rawData.object.first_name), last_name: String(user.last_name),
last_name: String(rawData.object.last_name), password: String(user.password) || '',
email: String(rawData.object.email), email: String(user.email),
phone: String(rawData.object.phone || ''), phone: String(user.phone || ''),
company: String(rawData.object.company || ''), company: String(user.company || ''),
dateofbirth: toRFC3339(String(rawData.object.dateofbirth || '')), dateofbirth: toRFC3339(String(user.dateofbirth || '')),
address: String(rawData.object.address || ''), address: String(user.address || ''),
zip_code: String(rawData.object.zip_code || ''), zip_code: String(user.zip_code || ''),
city: String(rawData.object.city || ''), city: String(user.city || ''),
notes: String(rawData.object.notes || ''), notes: String(user.notes || ''),
profile_picture: String(rawData.object.profile_picture || ''), membership: processMembershipFormData(user.membership ? user.membership : defaultMembership()),
licence: user.licence ? processLicenceFormData(user.licence) : null,
membership: { bank_account: processBankAccountFormData(
id: Number(rawData.object.membership?.id) || 0, user.bank_account ? user.bank_account : defaultBankAccount()
status: Number(rawData.object.membership?.status), )
start_date: toRFC3339(String(rawData.object.membership?.start_date || '')),
end_date: toRFC3339(String(rawData.object.membership?.end_date || '')),
parent_member_id: Number(rawData.object.membership?.parent_member_id) || 0,
subscription_model: {
id: Number(rawData.object.membership?.subscription_model?.id) || 0,
name: String(rawData.object.membership?.subscription_model?.name) || '',
details: String(rawData.object.membership?.subscription_model?.details) || '',
conditions: String(rawData.object.membership?.subscription_model?.conditions) || '',
hourly_rate: Number(rawData.object.membership?.subscription_model?.hourly_rate) || 0,
monthly_fee: Number(rawData.object.membership?.subscription_model?.monthly_fee) || 0,
included_hours_per_month:
Number(rawData.object.membership?.subscription_model?.included_hours_per_month) || 0,
included_hours_per_year:
Number(rawData.object.membership?.subscription_model?.included_hours_per_year) || 0
}
},
licence: {
id: Number(rawData.object.licence?.id) || 0,
status: Number(rawData.object.licence?.status),
number: String(rawData.object.licence?.number || ''),
issued_date: toRFC3339(String(rawData.object.licence?.issued_date || '')),
expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date || '')),
country: String(rawData.object.licence?.country || ''),
categories: rawData.object.licence?.categories || []
},
bank_account: {
id: Number(rawData.object.bank_account?.id) || 0,
account_holder_name: String(rawData.object.bank_account?.account_holder_name || ''),
bank: String(rawData.object.bank_account?.bank || ''),
iban: String(rawData.object.bank_account?.iban || ''),
bic: String(rawData.object.bank_account?.bic || ''),
mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(
String(rawData.object.bank_account?.mandate_date_signed || '')
)
}
}
}; };
// console.log('Categories: --------'); // console.log('Categories: --------');
// console.dir(rawData.object.licence); // console.dir(rawData.object.licence);
if (
rawData.object.password &&
rawData.confirm_password &&
rawData.object.password === rawData.confirm_password &&
rawData.object.password.trim() !== ''
) {
processedData.user.password = rawData.object.password;
}
const clean = JSON.parse(JSON.stringify(processedData), (key, value) => const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined value !== null && value !== '' ? value : undefined
); );
@@ -142,23 +158,21 @@ export function processUserFormData(rawData) {
} }
/** /**
* Processes the raw form data into the expected user data structure * Processes the raw form data into the expected subscription data structure
* @param {{ object: Partial<App.Types['subscription']>} } rawData - The raw form data object * @param {Partial<App.Types['subscription']>} subscription - The raw form data object
* @returns {{ subscription: Partial<App.Types['subscription']> }} Processed user data * @returns {App.Types['subscription']} Processed user data
*/ */
export function processSubscriptionFormData(rawData) { export function processSubscriptionFormData(subscription) {
/** @type {{ subscription: Partial<App.Types['subscription']> }} */ /** @type {Partial<App.Types['subscription']>} */
let processedData = { let processedData = {
subscription: { id: Number(subscription.id) || 0,
id: Number(rawData.object.id) || 0, name: String(subscription.name) || '',
name: String(rawData.object.name) || '', details: String(subscription.details) || '',
details: String(rawData.object.details) || '', conditions: String(subscription.conditions) || '',
conditions: String(rawData.object.conditions) || '', hourly_rate: Number(subscription.hourly_rate) || 0,
hourly_rate: Number(rawData.object.hourly_rate) || 0, monthly_fee: Number(subscription.monthly_fee) || 0,
monthly_fee: Number(rawData.object.monthly_fee) || 0, included_hours_per_month: Number(subscription.included_hours_per_month) || 0,
included_hours_per_month: Number(rawData.object.included_hours_per_month) || 0, included_hours_per_year: Number(subscription.included_hours_per_year) || 0
included_hours_per_year: Number(rawData.object.included_hours_per_year) || 0
}
}; };
const clean = JSON.parse(JSON.stringify(processedData), (key, value) => const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined value !== null && value !== '' ? value : undefined
@@ -166,3 +180,85 @@ export function processSubscriptionFormData(rawData) {
console.dir(clean); console.dir(clean);
return clean; return clean;
} }
/**
* Processes the raw form data into the expected insurance data structure
* @param {App.Types['insurance']} insurance - The raw form data object
* @returns {App.Types['insurance']} Processed user data
*/
export function processInsuranceFormData(insurance) {
return {
id: Number(insurance.id) || 0,
company: String(insurance.company) || '',
reference: String(insurance.reference) || '',
start_date: toRFC3339(String(insurance.start_date) || '') || '',
end_date: toRFC3339(String(insurance.end_date) || '') || '',
notes: String(insurance.notes) || ''
};
}
/**
* Processes the raw form data into the expected car data structure
* @param {Partial<App.Types['car']>} car - The raw form data object
* @returns {App.Types['car']} Processed user data
*/
export function processCarFormData(car) {
console.dir(car);
/** @type {App.Types['car']} */
let processedData = {
id: Number(car.id) || 0,
name: String(car.name) || '',
status: Number(car.status) || 0,
brand: String(car.brand) || '',
model: String(car.model) || '',
price: Number(car.price) || 0,
rate: Number(car.rate) || 0,
licence_plate: String(car.licence_plate),
start_date: 'start_date' in car ? toRFC3339(String(car.start_date) || '') : '',
end_date: 'end_date' in car ? toRFC3339(String(car.end_date) || '') : '',
color: String(car.color) || '',
notes: String(car.notes) || '',
location:
'location' in car
? {
latitude: Number(car.location?.latitude) || 0,
longitude: Number(car.location?.longitude) || 0
}
: {
latitude: 0,
longitude: 0
},
damages: /** @type {App.Types['damage'][]} */ ([]),
insurances: /** @type {App.Types['insurance'][]} */ ([])
};
car.insurances?.forEach((insurance) => {
processedData.insurances.push(processInsuranceFormData(insurance));
});
car.damages?.forEach((damage) => {
console.dir(damage);
processedData.damages.push(processDamageFormData(damage));
});
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(clean);
return clean;
}
/**
* Processes the raw form data into the expected damage data structure
* @param { App.Types['damage'] } damage - The raw form data object
* @returns {App.Types['damage']} Processed damage data
*/
export function processDamageFormData(damage) {
return {
id: Number(damage.id) || 0,
name: String(damage.name) || '',
opponent: processUserFormData(damage.opponent),
driver_id: Number(damage.driver_id) || 0,
insurance: processInsuranceFormData(damage.insurance),
date: toRFC3339(String(damage.date) || ''),
notes: String(damage.notes) || ''
};
}

View File

@@ -30,8 +30,21 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
const processedData = processUserFormData(rawData); /** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
// confirm password matches and is not empty. Otherwise set password to empty string
if (
rawData.object.password &&
rawData.confirm_password &&
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
) {
rawData.object.password = '';
}
const processedData = processUserFormData(rawData.object);
// const isCreating = !processedData.user.id || processedData.user.id === 0; // const isCreating = !processedData.user.id || processedData.user.id === 0;
// console.log('Is creating: ', isCreating); // console.log('Is creating: ', isCreating);

View File

@@ -2,6 +2,7 @@
export async function load({ data }) { export async function load({ data }) {
return { return {
users: data.users, users: data.users,
user: data.user user: data.user,
cars: data.cars
}; };
} }

View File

@@ -1,43 +1,63 @@
import { BASE_API_URI } from '$lib/utils/constants'; import { BASE_API_URI } from '$lib/utils/constants';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { userDatesFromRFC3339, refreshCookie } from '$lib/utils/helpers'; import { userDatesFromRFC3339, refreshCookie, carDatesFromRFC3339 } from '$lib/utils/helpers';
import { base } from '$app/paths'; import { base } from '$app/paths';
/** @type {import('./$types').LayoutServerLoad} */ /** @type {import('./$types').LayoutServerLoad} */
export async function load({ cookies, fetch, locals }) { export async function load({ cookies, fetch, locals }) {
const jwt = cookies.get('jwt'); const jwt = cookies.get('jwt');
try { try {
const response = await fetch(`${BASE_API_URI}/auth/users/`, { const [usersResponse, carsResponse] = await Promise.all([
credentials: 'include', fetch(`${BASE_API_URI}/auth/users`, {
headers: { credentials: 'include',
Cookie: `jwt=${jwt}` headers: { Cookie: `jwt=${jwt}` }
} }),
}); fetch(`${BASE_API_URI}/auth/cars`, {
if (!response.ok) { credentials: 'include',
// Clear the invalid JWT cookie headers: { Cookie: `jwt=${jwt}` }
})
]);
if (!usersResponse.ok || !carsResponse.ok) {
cookies.delete('jwt', { path: '/' }); cookies.delete('jwt', { path: '/' });
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`); throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`);
} }
const [usersData, carsData] = await Promise.all([usersResponse.json(), carsResponse.json()]);
// const response = await fetch(`${BASE_API_URI}/auth/users/`, {
// credentials: 'include',
// headers: {
// Cookie: `jwt=${jwt}`
// }
// });
// if (!response.ok) {
// // Clear the invalid JWT cookie
// cookies.delete('jwt', { path: '/' });
// throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users/`);
// }
const data = await response.json(); // const data = await response.json();
/** @type {App.Locals['users']}*/ /** @type {App.Locals['users']}*/
const users = data.users; const users = usersData.users;
/** @type {App.Types['car'][]} */
const cars = carsData.cars;
users.forEach((user) => { users.forEach((user) => {
userDatesFromRFC3339(user); userDatesFromRFC3339(user);
}); });
cars.forEach((car) => {
carDatesFromRFC3339(car);
});
locals.users = users; locals.users = users;
locals.cars = cars;
// Check if the server sent a new token // Check if the server sent a new token
const newToken = response.headers.get('Set-Cookie'); const newToken = usersResponse.headers.get('Set-Cookie');
refreshCookie(newToken, cookies); refreshCookie(newToken, cookies);
return { return {
subscriptions: locals.subscriptions, subscriptions: locals.subscriptions,
licence_categories: locals.licence_categories, licence_categories: locals.licence_categories,
users: locals.users, users: locals.users,
user: locals.user user: locals.user,
cars: locals.cars
}; };
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);

View File

@@ -7,6 +7,7 @@ import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/help
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { import {
formDataToObject, formDataToObject,
processCarFormData,
processSubscriptionFormData, processSubscriptionFormData,
processUserFormData processUserFormData
} from '$lib/utils/processing'; } from '$lib/utils/processing';
@@ -36,11 +37,24 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
const processedData = processUserFormData(rawData); /** @type {{object: App.Locals['user'], confirm_password: string}} */
const rawData = {
object: /** @type {App.Locals['user']} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
// confirm password matches and is not empty. Otherwise set password to empty string
if (
rawData.object.password &&
rawData.confirm_password &&
(rawData.object.password != rawData.confirm_password || rawData.object.password.trim() == '')
) {
rawData.object.password = '';
}
const user = processUserFormData(rawData.object);
console.dir(processedData.user.membership); console.dir(user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0; const isCreating = !user.id || user.id === 0;
console.log('Is creating: ', isCreating); console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/auth/users`; const apiURL = `${BASE_API_URI}/auth/users`;
@@ -52,7 +66,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify(user)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -81,10 +95,11 @@ export const actions = {
updateSubscription: async ({ request, fetch, cookies }) => { updateSubscription: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
const processedData = processSubscriptionFormData(rawData); const rawSubscription = /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object);
const subscription = processSubscriptionFormData(rawSubscription);
const isCreating = !processedData.subscription.id || processedData.subscription.id === 0; const isCreating = !subscription.id || subscription.id === 0;
console.log('Is creating: ', isCreating); console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/auth/subscriptions`; const apiURL = `${BASE_API_URI}/auth/subscriptions`;
@@ -96,7 +111,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify(subscription)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -112,6 +127,51 @@ export const actions = {
throw redirect(303, `${base}/auth/admin/users`); throw redirect(303, `${base}/auth/admin/users`);
}, },
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns Error data or redirects user to the home page or the previous page
*/
updateCar: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
console.dir(formData);
const rawCar = /**@type {Partial<App.Types['car']>} */ (formDataToObject(formData).object);
const car = processCarFormData(rawCar);
const isCreating = !car.id || car.id === 0;
console.log('Is creating: ', isCreating);
console.log('sending: ', JSON.stringify(car.damages));
const apiURL = `${BASE_API_URI}/auth/cars`;
/** @type {RequestInit} */
const requestOptions = {
method: isCreating ? 'POST' : 'PUT',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(car)
};
const res = await fetch(apiURL, requestOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
console.log('Server success response:', response);
console.log('Server opponent response:', response.damages[0]?.opponent);
console.log('Server insurance response:', response.damages[0]?.insurance);
throw redirect(303, `${base}/auth/admin/users`);
},
/** /**
* *
* @param request - The request object * @param request - The request object
@@ -123,8 +183,9 @@ export const actions = {
userDelete: async ({ request, fetch, cookies }) => { userDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
const processedData = processUserFormData(rawData); /** @type {Partial<App.Locals['user']>} */
const rawUser = /** @type {Partial<App.Locals['user']>} */ (rawFormData.object);
const apiURL = `${BASE_API_URI}/auth/users`; const apiURL = `${BASE_API_URI}/auth/users`;
@@ -136,7 +197,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify({ id: Number(rawUser.id) })
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -157,14 +218,15 @@ export const actions = {
* @param request - The request object * @param request - The request object
* @param fetch - Fetch object from sveltekit * @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object * @param cookies - SvelteKit's cookie object
* @param locals - The local object, housing current subscription
* @returns * @returns
*/ */
subscriptionDelete: async ({ request, fetch, cookies }) => { subscriptionDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawData = formDataToObject(formData);
const processedData = processSubscriptionFormData(rawData);
/** @type {Partial<App.Types['subscription']>} */
const subscription = rawData.object;
const apiURL = `${BASE_API_URI}/auth/subscriptions`; const apiURL = `${BASE_API_URI}/auth/subscriptions`;
@@ -176,7 +238,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify({ id: Number(subscription.id), name: subscription.name })
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -191,11 +253,59 @@ export const actions = {
console.log('Server success response:', response); console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`); throw redirect(303, `${base}/auth/admin/users`);
}, },
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns
*/
carDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
console.dir(formData);
const rawCar = formDataToObject(formData);
const apiURL = `${BASE_API_URI}/auth/cars`;
console.log('sending delete request to', JSON.stringify({ id: Number(rawCar.object.id) }));
/** @type {RequestInit} */
const requestOptions = {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify({ id: Number(rawCar.object.id) })
};
const res = await fetch(apiURL, requestOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json();
console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`);
},
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns
*/
grantBackendAccess: async ({ request, fetch, cookies }) => { grantBackendAccess: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
const processedData = processUserFormData(rawData); /** @type {App.Locals['user']} */
const rawUser = /** @type {App.Locals['user']} */ (rawFormData.object);
const processedData = processUserFormData(rawUser);
console.dir(processedData); console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`; const apiURL = `${BASE_API_URI}/auth/users/activate`;

View File

@@ -8,7 +8,13 @@
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers'; import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { PERMISSIONS } from '$lib/utils/constants'; import { PERMISSIONS } from '$lib/utils/constants';
import { defaultSupporter, defaultUser } from '$lib/utils/defaults'; import {
defaultCar,
defaultSubscription,
defaultSupporter,
defaultUser
} from '$lib/utils/defaults';
import CarEditForm from '$lib/components/CarEditForm.svelte';
/** @type {import('./$types').ActionData} */ /** @type {import('./$types').ActionData} */
export let form; export let form;
@@ -16,18 +22,17 @@
$: ({ $: ({
user = [], user = [],
users = [], users = [],
cars = [],
licence_categories = [], licence_categories = [],
subscriptions = [], subscriptions = [],
payments = [] payments = []
} = $page.data); } = $page.data);
let activeSection = 'members'; let activeSection = 'members';
/** @type{App.Locals['user'] | null} */
let selectedUser = null; /** @type{App.Types['car'] | App.Types['subscription'] | App.Locals['user'] | null} */
/** @type{App.Types['subscription'] | null} */ let selected = null;
let selectedSubscription = null;
let showSubscriptionModal = false;
let showUserModal = false;
let searchTerm = ''; let searchTerm = '';
$: members = users.filter((/** @type{App.Locals['user']} */ user) => { $: members = users.filter((/** @type{App.Locals['user']} */ user) => {
@@ -78,9 +83,7 @@
user.licence?.number?.toLowerCase() user.licence?.number?.toLowerCase()
].some((field) => field?.includes(term)); ].some((field) => field?.includes(term));
const subscriptionMatch = user.membership?.subscription_model?.name const subscriptionMatch = user.membership?.subscription?.name?.toLowerCase().includes(term);
?.toLowerCase()
.includes(term);
const licenceCategoryMatch = user.licence?.categories?.some((cat) => const licenceCategoryMatch = user.licence?.categories?.some((cat) =>
cat.category.toLowerCase().includes(term) cat.category.toLowerCase().includes(term)
@@ -95,29 +98,8 @@
}); });
}; };
/**
* Opens the edit modal for the selected user.
* @param {App.Locals['user']} user The user to edit.
*/
const openEditUserModal = (user) => {
selectedUser = user;
showUserModal = true;
};
/**
* Opens the edit modal for the selected subscription.
* @param {App.Types['subscription'] | null} subscription The user to edit.
*/
const openEditSubscriptionModal = (subscription) => {
selectedSubscription = subscription;
showSubscriptionModal = true;
};
const close = () => { const close = () => {
showUserModal = false; selected = null;
showSubscriptionModal = false;
selectedUser = null;
selectedSubscription = null;
if (form) { if (form) {
form.errors = []; form.errors = [];
} }
@@ -163,10 +145,20 @@
on:click={() => setActiveSection('subscriptions')} on:click={() => setActiveSection('subscriptions')}
> >
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
{$t('subscription.subscriptions')} {$t('subscriptions.subscriptions')}
<span class="nav-badge">{subscriptions.length}</span> <span class="nav-badge">{subscriptions.length}</span>
</button> </button>
</li> </li>
<li>
<button
class="nav-link {activeSection === 'cars' ? 'active' : ''}"
on:click={() => setActiveSection('cars')}
>
<i class="fas fa-car"></i>
{$t('cars')}
<span class="nav-badge">{cars.length}</span>
</button>
</li>
<li> <li>
<button <button
class="nav-link {activeSection === 'payments' ? 'active' : ''}" class="nav-link {activeSection === 'payments' ? 'active' : ''}"
@@ -214,7 +206,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultUser())}> <button
class="btn primary"
on:click={() => {
selected = defaultUser();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -278,8 +275,8 @@
<td>{user.email}</td> <td>{user.email}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('subscription.subscription')}</th> <th>{$t('subscriptions.subscription')}</th>
<td>{user.membership?.subscription_model?.name}</td> <td>{user.membership?.subscription?.name}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('status')}</th> <th>{$t('status')}</th>
@@ -288,7 +285,12 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> <button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -350,7 +352,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultSupporter())}> <button
class="btn primary"
on:click={() => {
selected = defaultSupporter();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -360,8 +367,7 @@
{#each filteredSupporters as user} {#each filteredSupporters as user}
<details class="accordion-item"> <details class="accordion-item">
<summary class="accordion-header"> <summary class="accordion-header">
{user.first_name} {user.company}
{user.last_name}
</summary> </summary>
<div class="accordion-content"> <div class="accordion-content">
<table class="table"> <table class="table">
@@ -371,8 +377,8 @@
<td>{user.first_name} {user.last_name}</td> <td>{user.first_name} {user.last_name}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('company')}</th> <th>{$t('phone')}</th>
<td>{user.company}</td> <td>{user.phone}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('user.email')}</th> <th>{$t('user.email')}</th>
@@ -385,42 +391,51 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> {#if hasPrivilige(user, PERMISSIONS.Update)}
<i class="fas fa-edit"></i> <button
{$t('edit')} class="btn primary"
</button> on:click={() => {
<form selected = user;
method="POST" }}
action="?/userDelete" >
use:enhance={() => { <i class="fas fa-edit"></i>
return async ({ result }) => { {$t('edit')}
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.user_deletion', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<input type="hidden" name="user[last_name]" value={user.last_name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button> </button>
</form> {/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form
method="POST"
action="?/userDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.user_deletion', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<input type="hidden" name="user[last_name]" value={user.last_name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div> </div>
</div> </div>
</details> </details>
@@ -428,9 +443,14 @@
</div> </div>
{:else if activeSection === 'subscriptions'} {:else if activeSection === 'subscriptions'}
<div class="section-header"> <div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2> <h2>{$t('subscriptions.subscriptions')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)} {#if hasPrivilige(user, PERMISSIONS.Super)}
<button class="btn primary" on:click={() => openEditSubscriptionModal(null)}> <button
class="btn primary"
on:click={() => {
selected = defaultSubscription();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -444,7 +464,7 @@
<span class="nav-badge" <span class="nav-badge"
>{members.filter( >{members.filter(
(/** @type{App.Locals['user']}*/ user) => (/** @type{App.Locals['user']}*/ user) =>
user.membership?.subscription_model?.name === subscription.name user.membership?.subscription?.name === subscription.name
).length}</span ).length}</span
> >
</summary> </summary>
@@ -452,7 +472,7 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<th>{$t('subscription.monthly_fee')}</th> <th>{$t('subscriptions.monthly_fee')}</th>
<td <td
>{subscription.monthly_fee !== -1 >{subscription.monthly_fee !== -1
? subscription.monthly_fee + '€' ? subscription.monthly_fee + '€'
@@ -460,7 +480,7 @@
> >
</tr> </tr>
<tr> <tr>
<th>{$t('subscription.hourly_rate')}</th> <th>{$t('subscriptions.hourly_rate')}</th>
<td <td
>{subscription.hourly_rate !== -1 >{subscription.hourly_rate !== -1
? subscription.hourly_rate + '€' ? subscription.hourly_rate + '€'
@@ -468,11 +488,11 @@
> >
</tr> </tr>
<tr> <tr>
<th>{$t('subscription.included_hours_per_year')}</th> <th>{$t('subscriptions.included_hours_per_year')}</th>
<td>{subscription.included_hours_per_year || 0}</td> <td>{subscription.included_hours_per_year || 0}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('subscription.included_hours_per_month')}</th> <th>{$t('subscriptions.included_hours_per_month')}</th>
<td>{subscription.included_hours_per_month || 0}</td> <td>{subscription.included_hours_per_month || 0}</td>
</tr> </tr>
<tr> <tr>
@@ -480,60 +500,170 @@
<td>{subscription.details || '-'}</td> <td>{subscription.details || '-'}</td>
</tr> </tr>
<tr> <tr>
<th>{$t('subscription.conditions')}</th> <th>{$t('subscriptions.conditions')}</th>
<td>{subscription.conditions || '-'}</td> <td>{subscription.conditions || '-'}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{#if hasPrivilige(user, PERMISSIONS.Super)} <div class="button-group">
<div class="button-group"> {#if hasPrivilige(user, PERMISSIONS.Super)}
<button <button
class="btn primary" class="btn primary"
on:click={() => openEditSubscriptionModal(subscription)} on:click={() => {
selected = subscription;
}}
> >
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
{#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription_model?.id === subscription.id)} {/if}
<form {#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription?.id === subscription.id)}
method="POST" <form
action="?/subscriptionDelete" method="POST"
use:enhance={() => { action="?/subscriptionDelete"
return async ({ result }) => { use:enhance={() => {
if (result.type === 'success' || result.type === 'redirect') { return async ({ result }) => {
await applyAction(result); if (result.type === 'success' || result.type === 'redirect') {
} else { await applyAction(result);
document } else {
.querySelector('.accordion-content') document
?.scrollTo({ top: 0, behavior: 'smooth' }); .querySelector('.accordion-content')
await applyAction(result); ?.scrollTo({ top: 0, behavior: 'smooth' });
} await applyAction(result);
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.subscription_deletion', {
values: {
name: subscription.name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
} }
}} };
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.subscription_deletion', {
values: {
name: subscription.name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="subscription[id]" value={subscription.id} />
<input type="hidden" name="subscription[name]" value={subscription.name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div>
</div>
</details>
{/each}
</div>
{:else if activeSection === 'cars'}
<div class="section-header">
<h2>{$t('cars')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)}
<button
class="btn primary"
on:click={() => {
selected = defaultCar();
}}
>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
{/if}
</div>
<div class="accordion">
{#each cars as car}
<details class="accordion-item">
<summary class="accordion-header">
{car.model + ' (' + car.licence_plate + ')'}
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('car.model')}</th>
<td>{car.brand + ' ' + car.model + ' (' + car.color + ')'}</td>
</tr>
<tr>
<th>{$t('price')}</th>
<td
>{car.price + '€'}{car.rate
? ' + ' + car.rate + '€/' + $t('month')
: ''}</td
> >
<input type="hidden" name="subscription[id]" value={subscription.id} /> </tr>
<input type="hidden" name="subscription[name]" value={subscription.name} /> <tr>
<button class="btn danger" type="submit"> <th>{$t('car.damages')}</th>
<i class="fas fa-trash"></i> <td>{car.damages?.length || 0}</td>
{$t('delete')} </tr>
</button> <tr>
</form> <th>{$t('insurance')}</th>
{/if} <td
</div> >{car.insurance
{/if} ? car.insurance.company + '(' + car.insurance.reference + ')'
: '-'}</td
>
</tr>
<tr>
<th>{$t('car.end_date')}</th>
<td>{car.end_date || '-'}</td>
</tr>
</tbody>
</table>
<div class="button-group">
{#if hasPrivilige(user, PERMISSIONS.Update)}
<button
class="btn primary"
on:click={() => {
selected = car;
}}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
{/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form
method="POST"
action="?/carDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
} else {
document
.querySelector('.accordion-content')
?.scrollTo({ top: 0, behavior: 'smooth' });
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.car_deletion', {
values: {
name: car.brand + ' ' + car.model + ' (' + car.licence_plate + ')'
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="car[id]" value={car.id} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
{/if}
</div>
</div> </div>
</details> </details>
{/each} {/each}
@@ -568,28 +698,34 @@
</div> </div>
</div> </div>
{#if showUserModal && selectedUser !== null} {#if selected && 'email' in selected}
// user
<Modal on:close={close}> <Modal on:close={close}>
<UserEditForm <UserEditForm
{form} {form}
editor={user} editor={user}
user={selectedUser} user={selected}
{subscriptions} {subscriptions}
{licence_categories} {licence_categories}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if showSubscriptionModal} {:else if selected && 'monthly_fee' in selected}
//subscription
<Modal on:close={close}> <Modal on:close={close}>
<SubscriptionEditForm <SubscriptionEditForm
{form} {form}
{user} {user}
subscription={selectedSubscription} subscription={selected}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if selected && 'brand' in selected}
<Modal on:close={close}>
<CarEditForm {form} editor={user} {users} car={selected} on:cancel={close} on:close={close} />
</Modal>
{/if} {/if}
<style> <style>

View File

@@ -18,7 +18,7 @@ func main() {
config.LoadConfig() config.LoadConfig()
db, err := database.Open(config.DB.Path, config.Recipients.AdminEmail) db, err := database.Open(config.DB.Path, config.Recipients.AdminEmail, config.Env == "development")
if err != nil { if err != nil {
logger.Error.Fatalf("Couldn't init database: %v", err) logger.Error.Fatalf("Couldn't init database: %v", err)
} }

View File

@@ -62,6 +62,16 @@ type SecurityConfig struct {
Burst int `json:"Burst" default:"60" envconfig:"BURST_LIMIT"` Burst int `json:"Burst" default:"60" envconfig:"BURST_LIMIT"`
} `json:"RateLimits"` } `json:"RateLimits"`
} }
type CompanyConfig struct {
Name string `json:"Name" envconfig:"COMPANY_NAME"`
Address string `json:"Address" envconfig:"COMPANY_ADDRESS"`
City string `json:"City" envconfig:"COMPANY_CITY"`
ZipCode string `json:"ZipCode" envconfig:"COMPANY_ZIPCODE"`
Country string `json:"Country" envconfig:"COMPANY_COUNTRY"`
SepaPrefix string `json:"SepaPrefix" envconfig:"COMPANY_SEPA_PREFIX"`
}
type Config struct { type Config struct {
Auth AuthenticationConfig `json:"auth"` Auth AuthenticationConfig `json:"auth"`
Site SiteConfig `json:"site"` Site SiteConfig `json:"site"`
@@ -72,6 +82,7 @@ type Config struct {
DB DatabaseConfig `json:"db"` DB DatabaseConfig `json:"db"`
SMTP SMTPConfig `json:"smtp"` SMTP SMTPConfig `json:"smtp"`
Security SecurityConfig `json:"security"` Security SecurityConfig `json:"security"`
Company CompanyConfig `json:"company"`
} }
var ( var (
@@ -85,7 +96,9 @@ var (
Recipients RecipientsConfig Recipients RecipientsConfig
Env string Env string
Security SecurityConfig Security SecurityConfig
Company CompanyConfig
) )
var environmentOptions map[string]bool = map[string]bool{ var environmentOptions map[string]bool = map[string]bool{
"development": true, "development": true,
"production": true, "production": true,
@@ -124,6 +137,7 @@ func LoadConfig() {
Security = CFG.Security Security = CFG.Security
Env = CFG.Env Env = CFG.Env
Site = CFG.Site Site = CFG.Site
Company = CFG.Company
logger.Info.Printf("Config loaded: %#v", CFG) logger.Info.Printf("Config loaded: %#v", CFG)
} }

View File

@@ -9,13 +9,13 @@ const (
DelayedPaymentStatus DelayedPaymentStatus
SettledPaymentStatus SettledPaymentStatus
AwaitingPaymentStatus AwaitingPaymentStatus
MailVerificationSubject = "Nur noch ein kleiner Schritt!" MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert" MailChangePasswordSubject = "Passwort Änderung angefordert"
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang" MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
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"
SupporterSubscriptionModelName = "Keins" SupporterSubscriptionName = "Keins"
) )
var Licences = struct { var Licences = struct {
@@ -77,12 +77,14 @@ var Priviliges = struct {
} }
var Roles = struct { var Roles = struct {
Opponent int8
Supporter int8 Supporter int8
Member int8 Member int8
Viewer int8 Viewer int8
Editor int8 Editor int8
Admin int8 Admin int8
}{ }{
Opponent: -5,
Supporter: 0, Supporter: 0,
Member: 1, Member: 1,
Viewer: 2, Viewer: 2,

View File

@@ -0,0 +1,116 @@
package controllers
import (
"GoMembership/internal/constants"
"GoMembership/internal/models"
"GoMembership/internal/services"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type CarController struct {
S services.CarServiceInterface
UserService services.UserServiceInterface
}
func (cr *CarController) Create(c *gin.Context) {
requestUser, err := cr.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in Create car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !requestUser.HasPrivilege(constants.Priviliges.Create) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to create a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Create), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
var newCar models.Car
if err := c.ShouldBindJSON(&newCar); err != nil {
utils.HandleValidationError(c, err)
return
}
car, err := cr.S.Create(&newCar)
if err != nil {
utils.RespondWithError(c, err, "Error creating car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusCreated, car)
}
func (cr *CarController) Update(c *gin.Context) {
requestUser, err := cr.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in Update car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !requestUser.HasPrivilege(constants.Priviliges.Update) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to update a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Update), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
var car models.Car
if err := c.ShouldBindJSON(&car); err != nil {
utils.HandleValidationError(c, err)
return
}
logger.Error.Printf("updating car: %v", car)
updatedCar, err := cr.S.Update(&car)
if err != nil {
utils.RespondWithError(c, err, "Error updating car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusOK, updatedCar)
}
func (cr *CarController) GetAll(c *gin.Context) {
requestUser, err := cr.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in GetAll car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !requestUser.HasPrivilege(constants.Priviliges.View) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to access car data. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Delete), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
cars, err := cr.S.GetAll()
if err != nil {
utils.RespondWithError(c, err, "Error getting cars", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"cars": cars,
})
}
func (cr *CarController) Delete(c *gin.Context) {
type input struct {
ID uint `json:"id" binding:"required,numeric"`
}
var deleteData input
requestUser, err := cr.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in Delete car handler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !requestUser.HasPrivilege(constants.Priviliges.Delete) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to delete a car. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.Delete), http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
if err := c.ShouldBindJSON(&deleteData); err != nil {
utils.HandleValidationError(c, err)
return
}
err = cr.S.Delete(&deleteData.ID)
if err != nil {
utils.RespondWithError(c, err, "Error deleting car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusOK, "Car deleted")
}

View File

@@ -87,7 +87,7 @@ func TestMain(t *testing.T) {
log.Fatalf("Error setting environment variable: %v", err) log.Fatalf("Error setting environment variable: %v", err)
} }
config.LoadConfig() config.LoadConfig()
db, err := database.Open("test.db", config.Recipients.AdminEmail) db, err := database.Open("test.db", config.Recipients.AdminEmail, true)
if err != nil { if err != nil {
log.Fatalf("Failed to create DB: %#v", err) log.Fatalf("Failed to create DB: %#v", err)
} }
@@ -100,7 +100,7 @@ func TestMain(t *testing.T) {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo} bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{} var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{}
var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{} var subscriptionRepo repositories.SubscriptionsRepositoryInterface = &repositories.SubscriptionsRepository{}
membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo} membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{} var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
@@ -119,21 +119,29 @@ func TestMain(t *testing.T) {
if err := initLicenceCategories(); err != nil { if err := initLicenceCategories(); err != nil {
log.Fatalf("Failed to init Categories: %v", err) log.Fatalf("Failed to init Categories: %v", err)
} }
password := "securepassword"
admin := models.User{ admin := models.User{
FirstName: "Ad", FirstName: "Ad",
LastName: "min", LastName: "min",
Email: "admin@example.com", Email: "admin@example.com",
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
Company: "SampleCorp", Company: "SampleCorp",
Phone: "+123456789", Phone: "+123456789",
Address: "123 Main Street", Address: "123 Main Street",
ZipCode: "12345", ZipCode: "12345",
City: "SampleCity", City: "SampleCity",
Status: constants.ActiveStatus, Status: constants.ActiveStatus,
RoleID: 8, Password: password,
} Notes: "",
admin.SetPassword("securepassword") RoleID: constants.Roles.Admin,
database.DB.Create(&admin) Consents: nil,
Verifications: nil,
Membership: nil,
BankAccount: nil,
Licence: &models.Licence{
Status: constants.UnverifiedStatus,
}}
admin.Create(db)
validation.SetupValidators(db) validation.SetupValidators(db)
t.Run("userController", func(t *testing.T) { t.Run("userController", func(t *testing.T) {
testUserController(t) testUserController(t)
@@ -195,7 +203,7 @@ func initLicenceCategories() error {
} }
func initSubscriptionPlans() error { func initSubscriptionPlans() error {
subscriptions := []models.SubscriptionModel{ subscriptions := []models.Subscription{
{ {
Name: "Basic", Name: "Basic",
Details: "Test Plan", Details: "Test Plan",
@@ -267,41 +275,39 @@ func GetMockedFormContext(formData url.Values, url string) (*gin.Context, *httpt
func getBaseUser() models.User { func getBaseUser() models.User {
return models.User{ return models.User{
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
Email: "john.doe@example.com", Email: "john.doe@example.com",
Address: "Pablo Escobar Str. 4", Address: "Pablo Escobar Str. 4",
ZipCode: "25474", ZipCode: "25474",
City: "Hasloh", City: "Hasloh",
Phone: "01738484993", Phone: "01738484993",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"}, BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}}, Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil, Licence: nil,
ProfilePicture: "", Password: "passw@#$#%$!-ord123",
Password: "passw@#$#%$!-ord123", Company: "",
Company: "", RoleID: 1,
RoleID: 1,
} }
} }
func getBaseSupporter() models.User { func getBaseSupporter() models.User {
return models.User{ return models.User{
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John", FirstName: "John",
LastName: "Rich", LastName: "Rich",
Email: "john.supporter@example.com", Email: "john.supporter@example.com",
Address: "Pablo Escobar Str. 4", Address: "Pablo Escobar Str. 4",
ZipCode: "25474", ZipCode: "25474",
City: "Hasloh", City: "Hasloh",
Phone: "01738484993", Phone: "01738484993",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"}, BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}}, Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil, Licence: nil,
ProfilePicture: "", Password: "passw@#$#%$!-ord123",
Password: "passw@#$#%$!-ord123", Company: "",
Company: "", RoleID: 0,
RoleID: 0,
} }
} }
func deleteTestDB(dbPath string) error { func deleteTestDB(dbPath string) error {

View File

@@ -20,13 +20,7 @@ type MembershipController struct {
UserService services.UserServiceInterface UserService services.UserServiceInterface
} }
type MembershipData struct {
// APIKey string `json:"api_key"`
Subscription models.SubscriptionModel `json:"subscription"`
}
func (mc *MembershipController) RegisterSubscription(c *gin.Context) { func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
var regData MembershipData
requestUser, err := mc.UserService.FromContext(c) requestUser, err := mc.UserService.FromContext(c)
if err != nil { if err != nil {
@@ -39,22 +33,23 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
return return
} }
if err := c.ShouldBindJSON(&regData); err != nil { var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
// Register Subscription // Register Subscription
id, err := mc.Service.RegisterSubscription(&regData.Subscription) id, err := mc.Service.RegisterSubscription(&subscription)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "UNIQUE constraint failed") {
utils.RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Duplicate) utils.RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.Subscription, errors.Responses.Keys.Duplicate)
} else { } else {
utils.RespondWithError(c, err, "Couldn't register Membershipmodel", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError) utils.RespondWithError(c, err, "Couldn't register Membershipmodel", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
} }
return return
} }
logger.Info.Printf("registering subscription: %+v", regData) logger.Info.Printf("registering subscription: %+v", subscription)
c.JSON(http.StatusCreated, gin.H{ c.JSON(http.StatusCreated, gin.H{
"status": "success", "status": "success",
"id": id, "id": id,
@@ -62,7 +57,6 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
} }
func (mc *MembershipController) UpdateHandler(c *gin.Context) { func (mc *MembershipController) UpdateHandler(c *gin.Context) {
var regData MembershipData
requestUser, err := mc.UserService.FromContext(c) requestUser, err := mc.UserService.FromContext(c)
if err != nil { if err != nil {
@@ -75,19 +69,19 @@ func (mc *MembershipController) UpdateHandler(c *gin.Context) {
return return
} }
if err := c.ShouldBindJSON(&regData); err != nil { var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
// update Subscription // update Subscription
logger.Info.Printf("Updating subscription %v", regData.Subscription.Name) logger.Info.Printf("Updating subscription %v", subscription.Name)
id, err := mc.Service.UpdateSubscription(&regData.Subscription) id, err := mc.Service.UpdateSubscription(&subscription)
if err != nil { if err != nil {
utils.HandleSubscriptionUpdateError(c, err) utils.HandleSubscriptionUpdateError(c, err)
return return
} }
logger.Info.Printf("updating subscription: %+v", regData)
c.JSON(http.StatusAccepted, gin.H{ c.JSON(http.StatusAccepted, gin.H{
"status": "success", "status": "success",
"id": id, "id": id,
@@ -96,13 +90,11 @@ func (mc *MembershipController) UpdateHandler(c *gin.Context) {
func (mc *MembershipController) DeleteSubscription(c *gin.Context) { func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
type deleteData struct { type deleteData struct {
Subscription struct { ID uint `json:"id" binding:"required,numeric,safe_content"`
ID uint `json:"id"` Name string `json:"name" binding:"required,safe_content"`
Name string `json:"name"`
} `json:"subscription"`
} }
var data deleteData var subscription deleteData
requestUser, err := mc.UserService.FromContext(c) requestUser, err := mc.UserService.FromContext(c)
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in subscription deleteSubscription", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken) utils.RespondWithError(c, err, "Error extracting user from context in subscription deleteSubscription", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
@@ -114,12 +106,12 @@ func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
return return
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
if err := mc.Service.DeleteSubscription(&data.Subscription.ID, &data.Subscription.Name); err != nil { if err := mc.Service.DeleteSubscription(&subscription.ID, &subscription.Name); err != nil {
utils.HandleSubscriptionDeleteError(c, err) utils.HandleSubscriptionDeleteError(c, err)
return return
} }
@@ -130,7 +122,7 @@ func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
func (mc *MembershipController) GetSubscriptions(c *gin.Context) { func (mc *MembershipController) GetSubscriptions(c *gin.Context) {
subscriptions, err := mc.Service.GetSubscriptions(nil) subscriptions, err := mc.Service.GetSubscriptions(nil)
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Error retrieving subscriptions", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError) utils.RespondWithError(c, err, "Error retrieving subscriptions", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
return return
} }

View File

@@ -148,18 +148,16 @@ func (dt *DeleteSubscriptionTest) ValidateResult() error {
return validateSubscription(dt.Assert, dt.WantDBData) return validateSubscription(dt.Assert, dt.WantDBData)
} }
func getBaseSubscription() MembershipData { func getBaseSubscription() models.Subscription {
return MembershipData{ return models.Subscription{
// APIKey: config.Auth.APIKEY, Name: "Premium",
Subscription: models.SubscriptionModel{ Details: "A subscription detail",
Name: "Premium", MonthlyFee: 12.0,
Details: "A subscription detail", HourlyRate: 14.0,
MonthlyFee: 12.0,
HourlyRate: 14.0,
},
} }
} }
func customizeSubscription(customize func(MembershipData) MembershipData) MembershipData {
func customizeSubscription(customize func(models.Subscription) models.Subscription) models.Subscription {
subscription := getBaseSubscription() subscription := getBaseSubscription()
return customize(subscription) return customize(subscription)
} }
@@ -175,8 +173,8 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Just a Subscription"}, WantDBData: map[string]interface{}{"name": "Just a Subscription"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Details = "" subscription.Details = ""
return subscription return subscription
})), })),
}, },
@@ -189,8 +187,8 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": ""}, WantDBData: map[string]interface{}{"name": ""},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "" subscription.Name = ""
return subscription return subscription
})), })),
}, },
@@ -202,8 +200,8 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantResponse: http.StatusBadRequest, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData { Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.Subscription.MonthlyFee = -10.0 sub.MonthlyFee = -10.0
return sub return sub
})), })),
}, },
@@ -215,8 +213,8 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantResponse: http.StatusBadRequest, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData { Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.Subscription.HourlyRate = -1.0 sub.HourlyRate = -1.0
return sub return sub
})), })),
}, },
@@ -229,10 +227,10 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
return subscription return subscription
})), })),
}, },
@@ -245,10 +243,10 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
return subscription return subscription
})), })),
}, },
@@ -276,8 +274,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "monthly_fee": "12"}, WantDBData: map[string]interface{}{"name": "Premium", "monthly_fee": "12"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.MonthlyFee = 123.0 subscription.MonthlyFee = 123.0
return subscription return subscription
})), })),
}, },
@@ -290,8 +288,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.ID = 0 subscription.ID = 0
return subscription return subscription
})), })),
}, },
@@ -304,8 +302,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "hourly_rate": "14"}, WantDBData: map[string]interface{}{"name": "Premium", "hourly_rate": "14"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.HourlyRate = 3254.0 subscription.HourlyRate = 3254.0
return subscription return subscription
})), })),
}, },
@@ -318,8 +316,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "included_per_year": "0"}, WantDBData: map[string]interface{}{"name": "Premium", "included_per_year": "0"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.IncludedPerYear = 9873.0 subscription.IncludedPerYear = 9873.0
return subscription return subscription
})), })),
}, },
@@ -332,8 +330,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "included_per_month": "1"}, WantDBData: map[string]interface{}{"name": "Premium", "included_per_month": "1"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.IncludedPerMonth = 23415.0 subscription.IncludedPerMonth = 23415.0
return subscription return subscription
})), })),
}, },
@@ -346,8 +344,8 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"}, WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "NonExistentSubscription" subscription.Name = "NonExistentSubscription"
return subscription return subscription
})), })),
}, },
@@ -360,11 +358,11 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"}, WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Details = "Altered Details" subscription.Details = "Altered Details"
subscription.Subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
return subscription return subscription
})), })),
}, },
@@ -377,11 +375,11 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"}, WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Details = "Altered Details" subscription.Details = "Altered Details"
subscription.Subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
return subscription return subscription
})), })),
}, },
@@ -390,7 +388,7 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
func getSubscriptionDeleteData() []DeleteSubscriptionTest { func getSubscriptionDeleteData() []DeleteSubscriptionTest {
var premiumSub, basicSub models.SubscriptionModel var premiumSub, basicSub models.Subscription
database.DB.Where("name = ?", "Premium").First(&premiumSub) database.DB.Where("name = ?", "Premium").First(&premiumSub)
database.DB.Where("name = ?", "Basic").First(&basicSub) database.DB.Where("name = ?", "Basic").First(&basicSub)
@@ -404,9 +402,10 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"}, WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "NonExistentSubscription" subscription.Name = "NonExistentSubscription"
subscription.Subscription.ID = basicSub.ID subscription.ID = basicSub.ID
logger.Error.Printf("subscription to delete: %#v", subscription)
return subscription return subscription
})), })),
}, },
@@ -415,13 +414,13 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
req.AddCookie(AdminCookie) req.AddCookie(AdminCookie)
}, },
Name: "Delete subscription without name should fail", Name: "Delete subscription without name should fail",
WantResponse: http.StatusExpectationFailed, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": ""}, WantDBData: map[string]interface{}{"name": ""},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "" subscription.Name = ""
subscription.Subscription.ID = basicSub.ID subscription.ID = basicSub.ID
return subscription return subscription
})), })),
}, },
@@ -434,9 +433,9 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Basic"}, WantDBData: map[string]interface{}{"name": "Basic"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "Basic" subscription.Name = "Basic"
subscription.Subscription.ID = basicSub.ID subscription.ID = basicSub.ID
return subscription return subscription
})), })),
}, },
@@ -449,9 +448,9 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true, Assert: true,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "Premium" subscription.Name = "Premium"
subscription.Subscription.ID = premiumSub.ID subscription.ID = premiumSub.ID
return subscription return subscription
})), })),
}, },
@@ -464,9 +463,9 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Subscription.Name = "Premium" subscription.Name = "Premium"
subscription.Subscription.ID = premiumSub.ID subscription.ID = premiumSub.ID
return subscription return subscription
})), })),
}, },

View File

@@ -132,9 +132,9 @@ func (uc *UserController) ChangePassword(c *gin.Context) {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
err = user.Verify(input.Token, constants.VerificationTypes.Password)
if !user.Verify(input.Token, constants.VerificationTypes.Password) { if err != nil {
utils.RespondWithError(c, errors.ErrAlreadyVerified, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError) utils.RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return return
} }

View File

@@ -31,6 +31,7 @@ func setupTestContext() (*TestContext, error) {
testEmail := "john.doe@example.com" testEmail := "john.doe@example.com"
user, err := Uc.Service.FromEmail(&testEmail) user, err := Uc.Service.FromEmail(&testEmail)
if err != nil { if err != nil {
logger.Error.Printf("error fetching user: %#v", err)
return nil, err return nil, err
} }
return &TestContext{ return &TestContext{
@@ -60,7 +61,6 @@ func testCreatePasswordHandler(t *testing.T) {
req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body)) req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body))
req.AddCookie(AdminCookie) req.AddCookie(AdminCookie)
tc.router.ServeHTTP(tc.response, req) tc.router.ServeHTTP(tc.response, req)
logger.Error.Printf("Test results for %#v", t.Name())
assert.Equal(t, http.StatusAccepted, tc.response.Code) assert.Equal(t, http.StatusAccepted, tc.response.Code)
assert.JSONEq(t, `{"message":"password_change_requested"}`, tc.response.Body.String()) assert.JSONEq(t, `{"message":"password_change_requested"}`, tc.response.Body.String())
err = checkEmailDelivery(tc.user, true) err = checkEmailDelivery(tc.user, true)
@@ -103,7 +103,6 @@ func testChangePassword(t *testing.T, tc *TestContext) {
var verification models.Verification var verification models.Verification
result := database.DB.Where("user_id = ? AND type = ?", tc.user.ID, constants.VerificationTypes.Password).First(&verification) result := database.DB.Where("user_id = ? AND type = ?", tc.user.ID, constants.VerificationTypes.Password).First(&verification)
assert.NoError(t, result.Error) assert.NoError(t, result.Error)
logger.Error.Printf("token from db: %#v", verification.VerificationToken)
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"password": "new-pas9247A@!sword", "password": "new-pas9247A@!sword",
"token": verification.VerificationToken, "token": verification.VerificationToken,

View File

@@ -75,7 +75,7 @@ func (uc *UserController) GetAllUsers(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"users": users, "users": safeUsers,
}) })
} }
@@ -89,6 +89,10 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
var updateData RegistrationData var updateData RegistrationData
if err := c.ShouldBindJSON(&updateData); err != nil { if err := c.ShouldBindJSON(&updateData); err != nil {
if updateData.User.Password != "" {
logger.Error.Printf("u.password: %#v", updateData.User.Password)
}
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
@@ -137,10 +141,7 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
} }
type deleteData struct { type deleteData struct {
User struct { ID uint `json:"id" binding:"required,numeric"`
ID uint `json:"id" binding:"required,numeric"`
LastName string `json:"last_name"`
} `json:"user"`
} }
var data deleteData var data deleteData
@@ -149,13 +150,13 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
return return
} }
if !requestUser.HasPrivilege(constants.Priviliges.Delete) && data.User.ID != requestUser.ID { if !requestUser.HasPrivilege(constants.Priviliges.Delete) && data.ID != requestUser.ID {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to delete user", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized) utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to delete user", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return return
} }
logger.Error.Printf("Deleting user: %v", data.User) logger.Error.Printf("Deleting user: %v", data)
if err := uc.Service.Delete(&data.User.ID); err != nil { if err := uc.Service.Delete(&data.ID); err != nil {
utils.HandleDeleteUserError(c, err) utils.HandleDeleteUserError(c, err)
return return
} }
@@ -238,17 +239,19 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
var regData RegistrationData var regData RegistrationData
if err := c.ShouldBindJSON(&regData); err != nil { if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership)
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership.Subscription)
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
logger.Info.Printf("Registering user %v", regData.User.Email) logger.Info.Printf("Registering user %v", regData.User.Email)
selectedModel, err := uc.MembershipService.GetSubscriptionByName(&regData.User.Membership.SubscriptionModel.Name) selectedModel, err := uc.MembershipService.GetSubscriptionByName(&regData.User.Membership.Subscription.Name)
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Error in Registeruser, couldn't get selected model", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InvalidSubscriptionModel) utils.RespondWithError(c, err, "Error in Registeruser, couldn't get selected model", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.InvalidSubscription)
return return
} }
regData.User.Membership.SubscriptionModel = *selectedModel regData.User.Membership.Subscription = *selectedModel
// Get Gin's binding validator engine with all registered validators // Get Gin's binding validator engine with all registered validators
validate := binding.Validator.Engine().(*validator.Validate) validate := binding.Validator.Engine().(*validator.Validate)
@@ -257,7 +260,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
if regData.User.Membership.SubscriptionModel.Name == constants.SupporterSubscriptionModelName { if regData.User.Membership.Subscription.Name == constants.SupporterSubscriptionName {
regData.User.RoleID = constants.Roles.Supporter regData.User.RoleID = constants.Roles.Supporter
} else { } else {
regData.User.RoleID = constants.Roles.Member regData.User.RoleID = constants.Roles.Member
@@ -292,12 +295,14 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
LastName: regData.User.LastName, LastName: regData.User.LastName,
Email: regData.User.Email, Email: regData.User.Email,
ConsentType: "TermsOfService", ConsentType: "TermsOfService",
UserID: &regData.User.ID,
}, },
{ {
FirstName: regData.User.FirstName, FirstName: regData.User.FirstName,
LastName: regData.User.LastName, LastName: regData.User.LastName,
Email: regData.User.Email, Email: regData.User.Email,
ConsentType: "Privacy", ConsentType: "Privacy",
UserID: &regData.User.ID,
}, },
} }
@@ -348,7 +353,8 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) {
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user"}) c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user"})
return return
} }
if !user.Verify(token, constants.VerificationTypes.Email) { err = user.Verify(token, constants.VerificationTypes.Email)
if err != nil {
logger.Error.Printf("Couldn't find user verification in verifyMailHandler: %v", err) logger.Error.Printf("Couldn't find user verification in verifyMailHandler: %v", err)
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user verification request"}) c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user verification request"})
return return

View File

@@ -78,7 +78,6 @@ func testUserController(t *testing.T) {
database.DB.Model(&models.User{}).Where("email = ?", "john.doe@example.com").Update("status", constants.ActiveStatus) database.DB.Model(&models.User{}).Where("email = ?", "john.doe@example.com").Update("status", constants.ActiveStatus)
loginEmail := testLoginHandler(t) loginEmail := testLoginHandler(t)
testCurrentUserHandler(t, loginEmail) testCurrentUserHandler(t, loginEmail)
// creating a admin cookie // creating a admin cookie
c, w, _ := GetMockedJSONContext([]byte(`{ c, w, _ := GetMockedJSONContext([]byte(`{
"email": "admin@example.com", "email": "admin@example.com",
@@ -245,12 +244,12 @@ func testLoginHandler(t *testing.T) string {
if cookie.Name == "jwt" { if cookie.Name == "jwt" {
MemberCookie = cookie MemberCookie = cookie
// tokenString := loginCookie.Value tokenString := cookie.Value
// _, claims, err := middlewares.ExtractContentFrom(tokenString) _, claims, err := middlewares.ExtractContentFrom(tokenString)
// assert.NoError(t, err, "FAiled getting cookie string") assert.NoError(t, err, "FAiled getting cookie string")
// jwtUserID := uint((*claims)["user_id"].(float64)) jwtUserID := uint((*claims)["user_id"].(float64))
// user, err := Uc.Service.GetUserByID(jwtUserID) _, err = Uc.Service.FromID(&jwtUserID)
// assert.NoError(t, err, "FAiled getting cookie string") assert.NoError(t, err, "FAiled getting cookie string")
// logger.Error.Printf("cookie user: %#v", user) // logger.Error.Printf("cookie user: %#v", user)
err = json.Unmarshal([]byte(tt.input), &loginInput) err = json.Unmarshal([]byte(tt.input), &loginInput)
@@ -352,8 +351,8 @@ func testCurrentUserHandler(t *testing.T, loginEmail string) http.Cookie {
if tt.expectedStatus == http.StatusOK { if tt.expectedStatus == http.StatusOK {
var response struct { var response struct {
User models.User `json:"user"` User models.User `json:"user"`
Subscriptions []models.SubscriptionModel `json:"subscriptions"` Subscriptions []models.Subscription `json:"subscriptions"`
} }
err := json.Unmarshal(w.Body.Bytes(), &response) err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err) assert.NoError(t, err)
@@ -402,19 +401,21 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error {
if err != nil { if err != nil {
return fmt.Errorf("Error in database ops: %#v", err) return fmt.Errorf("Error in database ops: %#v", err)
} }
if assert != (len(*users) != 0) { if assert != (len(*users) != 0) {
return fmt.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users) return fmt.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users)
} }
if assert { if assert {
user := (*users)[0] user := (*users)[0]
// Check for mandate reference // Check for mandate reference
if user.BankAccount.MandateReference == "" {
if user.BankAccount.IBAN != "" && user.BankAccount.MandateReference == "" {
return fmt.Errorf("Mandate reference not generated for user: %s", user.Email) return fmt.Errorf("Mandate reference not generated for user: %s", user.Email)
} else if user.BankAccount.IBAN == "" && user.BankAccount.MandateReference != "" {
return fmt.Errorf("Mandate reference generated without IBAN for user: %s", user.Email)
} }
// Validate mandate reference format // Validate mandate reference format
expected := user.GenerateMandateReference() expected := user.BankAccount.GenerateMandateReference(user.ID)
if !strings.HasPrefix(user.BankAccount.MandateReference, expected) { if !strings.HasPrefix(user.BankAccount.MandateReference, expected) {
return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference) return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference)
} }
@@ -575,7 +576,7 @@ func testUpdateUser(t *testing.T) {
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{} var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("B") category, err := licenceRepo.FindCategoryByName("B")
assert.NoError(t, err) assert.NoError(t, err)
u.Licence.Categories = []models.Category{category} u.Licence.Categories = []*models.Category{&category}
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
@@ -594,7 +595,7 @@ func testUpdateUser(t *testing.T) {
category, err := licenceRepo.FindCategoryByName("A") category, err := licenceRepo.FindCategoryByName("A")
category2, err := licenceRepo.FindCategoryByName("BE") category2, err := licenceRepo.FindCategoryByName("BE")
assert.NoError(t, err) assert.NoError(t, err)
u.Licence.Categories = []models.Category{category, category2} u.Licence.Categories = []*models.Category{&category, &category2}
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
@@ -612,7 +613,7 @@ func testUpdateUser(t *testing.T) {
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{} var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("A") category, err := licenceRepo.FindCategoryByName("A")
assert.NoError(t, err) assert.NoError(t, err)
u.Licence.Categories = []models.Category{category} u.Licence.Categories = []*models.Category{&category}
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
@@ -627,7 +628,7 @@ func testUpdateUser(t *testing.T) {
u.LastName = "Doe Updated" u.LastName = "Doe Updated"
u.Phone = "01738484994" u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50" u.Licence.Number = "B072RRE2I50"
u.Licence.Categories = []models.Category{} u.Licence.Categories = []*models.Category{}
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
@@ -688,6 +689,20 @@ func testUpdateUser(t *testing.T) {
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{
name: "Admin Password Update low entropy should fail",
setupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = "newpassword"
},
expectedErrors: []map[string]string{
{"field": "server.validation.special server.validation.uppercase server.validation.numbers server.validation.longer", "key": "server.validation.insecure"},
},
expectedStatus: http.StatusBadRequest,
},
{ {
name: "Admin Password Update", name: "Admin Password Update",
setupCookie: func(req *http.Request) { setupCookie: func(req *http.Request) {
@@ -794,7 +809,11 @@ func testUpdateUser(t *testing.T) {
if updatedUser.Password == "" { if updatedUser.Password == "" {
assert.Equal(t, user.Password, (*updatedUserFromDB).Password) assert.Equal(t, user.Password, (*updatedUserFromDB).Password)
} else { } else {
assert.NotEqual(t, user.Password, (*updatedUserFromDB).Password) matches, err := updatedUserFromDB.PasswordMatches(updatedUser.Password)
if err != nil {
t.Fatalf("Error matching password: %v", err)
}
assert.True(t, matches, "Password mismatch")
} }
updatedUserFromDB.Password = "" updatedUserFromDB.Password = ""
@@ -806,11 +825,9 @@ func testUpdateUser(t *testing.T) {
assert.Equal(t, updatedUser.Company, updatedUserFromDB.Company, "Company mismatch") assert.Equal(t, updatedUser.Company, updatedUserFromDB.Company, "Company mismatch")
assert.Equal(t, updatedUser.Phone, updatedUserFromDB.Phone, "Phone mismatch") assert.Equal(t, updatedUser.Phone, updatedUserFromDB.Phone, "Phone mismatch")
assert.Equal(t, updatedUser.Notes, updatedUserFromDB.Notes, "Notes mismatch") assert.Equal(t, updatedUser.Notes, updatedUserFromDB.Notes, "Notes mismatch")
assert.Equal(t, updatedUser.ProfilePicture, updatedUserFromDB.ProfilePicture, "ProfilePicture mismatch")
assert.Equal(t, updatedUser.Address, updatedUserFromDB.Address, "Address mismatch") assert.Equal(t, updatedUser.Address, updatedUserFromDB.Address, "Address mismatch")
assert.Equal(t, updatedUser.ZipCode, updatedUserFromDB.ZipCode, "ZipCode mismatch") assert.Equal(t, updatedUser.ZipCode, updatedUserFromDB.ZipCode, "ZipCode mismatch")
assert.Equal(t, updatedUser.City, updatedUserFromDB.City, "City mismatch") assert.Equal(t, updatedUser.City, updatedUserFromDB.City, "City mismatch")
assert.Equal(t, updatedUser.PaymentStatus, updatedUserFromDB.PaymentStatus, "PaymentStatus mismatch")
assert.Equal(t, updatedUser.Status, updatedUserFromDB.Status, "Status mismatch") assert.Equal(t, updatedUser.Status, updatedUserFromDB.Status, "Status mismatch")
assert.Equal(t, updatedUser.RoleID, updatedUserFromDB.RoleID, "RoleID mismatch") assert.Equal(t, updatedUser.RoleID, updatedUserFromDB.RoleID, "RoleID mismatch")
@@ -824,7 +841,7 @@ func testUpdateUser(t *testing.T) {
assert.Equal(t, updatedUser.Membership.StartDate, updatedUserFromDB.Membership.StartDate, "Membership.StartDate mismatch") assert.Equal(t, updatedUser.Membership.StartDate, updatedUserFromDB.Membership.StartDate, "Membership.StartDate mismatch")
assert.Equal(t, updatedUser.Membership.EndDate, updatedUserFromDB.Membership.EndDate, "Membership.EndDate mismatch") assert.Equal(t, updatedUser.Membership.EndDate, updatedUserFromDB.Membership.EndDate, "Membership.EndDate mismatch")
assert.Equal(t, updatedUser.Membership.Status, updatedUserFromDB.Membership.Status, "Membership.Status mismatch") assert.Equal(t, updatedUser.Membership.Status, updatedUserFromDB.Membership.Status, "Membership.Status mismatch")
assert.Equal(t, updatedUser.Membership.SubscriptionModelID, updatedUserFromDB.Membership.SubscriptionModelID, "Membership.SubscriptionModelID mismatch") assert.Equal(t, updatedUser.Membership.SubscriptionID, updatedUserFromDB.Membership.SubscriptionID, "Membership.SubscriptionID mismatch")
assert.Equal(t, updatedUser.Membership.ParentMembershipID, updatedUserFromDB.Membership.ParentMembershipID, "Membership.ParentMembershipID mismatch") assert.Equal(t, updatedUser.Membership.ParentMembershipID, updatedUserFromDB.Membership.ParentMembershipID, "Membership.ParentMembershipID mismatch")
if updatedUser.Licence == nil { if updatedUser.Licence == nil {
@@ -839,8 +856,17 @@ func testUpdateUser(t *testing.T) {
assert.Equal(t, updatedUser.Licence.IssuingCountry, updatedUserFromDB.Licence.IssuingCountry, "Licence.IssuingCountry mismatch") assert.Equal(t, updatedUser.Licence.IssuingCountry, updatedUserFromDB.Licence.IssuingCountry, "Licence.IssuingCountry mismatch")
} }
// For slices or more complex nested structures, you might want to use deep equality checks if len(updatedUser.Consents) > 0 {
assert.ElementsMatch(t, updatedUser.Consents, updatedUserFromDB.Consents, "Consents mismatch") for i := range updatedUser.Consents {
assert.Equal(t, updatedUser.Consents[i].ConsentType, updatedUserFromDB.Consents[i].ConsentType, "ConsentType mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].Email, updatedUserFromDB.Consents[i].Email, "ConsentEmail mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].FirstName, updatedUserFromDB.Consents[i].FirstName, "ConsentFirstName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].LastName, updatedUserFromDB.Consents[i].LastName, "ConsentLastName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].UserID, updatedUserFromDB.Consents[i].UserID, "Consent UserId mismatch at index %d", i)
}
} else {
assert.Emptyf(t, updatedUserFromDB.Licence.Categories, "Categories aren't empty when they should")
}
if len(updatedUser.Licence.Categories) > 0 { if len(updatedUser.Licence.Categories) > 0 {
for i := range updatedUser.Licence.Categories { for i := range updatedUser.Licence.Categories {
assert.Equal(t, updatedUser.Licence.Categories[i].Name, updatedUserFromDB.Licence.Categories[i].Name, "Category Category mismatch at index %d", i) assert.Equal(t, updatedUser.Licence.Categories[i].Name, updatedUserFromDB.Licence.Categories[i].Name, "Category Category mismatch at index %d", i)
@@ -866,11 +892,11 @@ func checkWelcomeMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, user.FirstName) { if !strings.Contains(message.Body, user.FirstName) {
return fmt.Errorf("User first name(%v) has not been rendered in registration mail.", user.FirstName) return fmt.Errorf("User first name(%v) has not been rendered in registration mail.", user.FirstName)
} }
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee) return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
} }
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.HourlyRate) return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
} }
if user.Company != "" && !strings.Contains(message.Body, user.Company) { if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company) return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -902,11 +928,11 @@ func checkRegistrationMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, user.FirstName+" "+user.LastName) { if !strings.Contains(message.Body, user.FirstName+" "+user.LastName) {
return fmt.Errorf("User first and last name(%v) has not been rendered in registration mail.", user.FirstName+" "+user.LastName) return fmt.Errorf("User first and last name(%v) has not been rendered in registration mail.", user.FirstName+" "+user.LastName)
} }
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.Subscription.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.MonthlyFee) return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.MonthlyFee)
} }
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) { if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.Subscription.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.SubscriptionModel.HourlyRate) return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", user.Membership.Subscription.HourlyRate)
} }
if user.Company != "" && !strings.Contains(message.Body, user.Company) { if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company) return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -946,7 +972,7 @@ func checkVerificationMail(message *utils.Email, user *models.User) error {
if err != nil { if err != nil {
return fmt.Errorf("Error parsing verification URL: %#v", err.Error()) return fmt.Errorf("Error parsing verification URL: %#v", err.Error())
} }
v, err := user.GetVerification(constants.VerificationTypes.Email) v, err := user.FindVerification(constants.VerificationTypes.Email)
if err != nil { if err != nil {
return fmt.Errorf("Error getting verification token: %v", err.Error()) return fmt.Errorf("Error getting verification token: %v", err.Error())
} }
@@ -1127,7 +1153,7 @@ func getTestUsers() []RegisterUserTest {
user.BankAccount.IBAN = "DE1234234123134" user.BankAccount.IBAN = "DE1234234123134"
user.RoleID = constants.Roles.Supporter user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com" user.Email = "john.supporter@example.com"
user.Membership.SubscriptionModel.Name = constants.SupporterSubscriptionModelName user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user return user
})), })),
}, },
@@ -1140,7 +1166,7 @@ func getTestUsers() []RegisterUserTest {
user.BankAccount.IBAN = "" user.BankAccount.IBAN = ""
user.RoleID = constants.Roles.Supporter user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com" user.Email = "john.supporter@example.com"
user.Membership.SubscriptionModel.Name = constants.SupporterSubscriptionModelName user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user return user
})), })),
}, },
@@ -1150,7 +1176,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "" user.Membership.Subscription.Name = ""
return user return user
})), })),
}, },
@@ -1160,7 +1186,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN" user.Membership.Subscription.Name = "NOTEXISTENTPLAN"
return user return user
})), })),
}, },
@@ -1199,7 +1225,7 @@ func getTestUsers() []RegisterUserTest {
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com" user.Email = "john.junior.doe@example.com"
user.Membership.SubscriptionModel.Name = "additional" user.Membership.Subscription.Name = "additional"
return user return user
})), })),
}, },
@@ -1211,7 +1237,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com" user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 200 user.Membership.ParentMembershipID = 200
user.Membership.SubscriptionModel.Name = "additional" user.Membership.Subscription.Name = "additional"
return user return user
})), })),
}, },
@@ -1223,7 +1249,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com" user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 1 user.Membership.ParentMembershipID = 1
user.Membership.SubscriptionModel.Name = "additional" user.Membership.Subscription.Name = "additional"
return user return user
})), })),
}, },
@@ -1254,35 +1280,35 @@ func getTestUsers() []RegisterUserTest {
// return user // return user
// })), // })),
// }, // },
// { {
// Name: "empty driverslicence number, should fail", Name: "empty driverslicence number, should fail",
// WantResponse: http.StatusBadRequest, WantResponse: http.StatusBadRequest,
// WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
// Assert: false, Assert: false,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.wronglicence.doe@example.com" user.Email = "john.wronglicence.doe@example.com"
// user.Licence = &models.Licence{ user.Licence = &models.Licence{
// Number: "", Number: "",
// ExpirationDate: time.Now().AddDate(1, 0, 0), ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0), IssuedDate: time.Now().AddDate(-1, 0, 0),
// } }
// return user return user
// })), })),
// }, },
// { {
// Name: "Correct Licence number, should pass", Name: "Correct Licence number, should pass",
// WantResponse: http.StatusCreated, WantResponse: http.StatusCreated,
// WantDBData: map[string]interface{}{"email": "john.correctLicenceNumber@example.com"}, WantDBData: map[string]interface{}{"email": "john.correctlicencenumber@example.com"},
// Assert: true, Assert: true,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.correctLicenceNumber@example.com" user.Email = "john.correctLicenceNumber@example.com"
// user.Licence = &models.Licence{ user.Licence = &models.Licence{
// Number: "B072RRE2I55", Number: "B072RRE2I55",
// ExpirationDate: time.Now().AddDate(1, 0, 0), ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0), IssuedDate: time.Now().AddDate(-1, 0, 0),
// } }
// return user return user
// })), })),
// }, },
} }
} }

View File

@@ -6,34 +6,68 @@ import (
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"fmt"
"time" "time"
"github.com/alexedwards/argon2id"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
var DB *gorm.DB var DB *gorm.DB
func Open(dbPath string, adminMail string) (*gorm.DB, error) { func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
// Add foreign key support and WAL journal mode to DSN
dsn := fmt.Sprintf("%s?_foreign_keys=1&_journal_mode=WAL", dbPath)
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
// Enable PrepareStmt for better performance
PrepareStmt: true,
})
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to connect database: %w", err)
} }
// Verify foreign key support is enabled
var foreignKeyEnabled int
if err := db.Raw("PRAGMA foreign_keys").Scan(&foreignKeyEnabled).Error; err != nil {
return nil, fmt.Errorf("foreign key check failed: %w", err)
}
if foreignKeyEnabled != 1 {
return nil, errors.New("SQLite foreign key constraints not enabled")
}
if debug {
db = db.Debug()
}
// Configure connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get DB instance: %w", err)
}
sqlDB.SetMaxOpenConns(1) // Required for SQLite in production
sqlDB.SetMaxIdleConns(1)
sqlDB.SetConnMaxLifetime(time.Hour)
db.Exec("PRAGMA foreign_keys = OFF;")
if err := db.AutoMigrate( if err := db.AutoMigrate(
&models.User{}, &models.Subscription{},
&models.SubscriptionModel{},
&models.Membership{}, &models.Membership{},
&models.Consent{}, &models.Consent{},
&models.Verification{}, &models.Verification{},
&models.BankAccount{},
&models.Licence{}, &models.Licence{},
&models.Category{}, &models.Category{},
&models.BankAccount{}); err != nil { &models.Car{},
logger.Error.Fatalf("Couldn't create database: %v", err) &models.Location{},
return nil, err &models.Damage{},
// &models.Insurance{},
// &models.User{},
); err != nil {
return nil, fmt.Errorf("failed to migrate database: %w", err)
} }
db.Exec("PRAGMA foreign_keys = ON;")
logger.Info.Print("Opened DB") logger.Info.Print("Opened DB")
DB = db DB = db
var categoriesCount int64 var categoriesCount int64
@@ -49,12 +83,12 @@ func Open(dbPath string, adminMail string) (*gorm.DB, error) {
} }
var subscriptionsCount int64 var subscriptionsCount int64
db.Model(&models.SubscriptionModel{}).Count(&subscriptionsCount) db.Model(&models.Subscription{}).Count(&subscriptionsCount)
subscriptionModels := createSubscriptionModels() subscriptions := createSubscriptions()
for _, model := range subscriptionModels { for _, model := range subscriptions {
var exists int64 var exists int64
db. db.
Model(&models.SubscriptionModel{}). Model(&models.Subscription{}).
Where("name = ?", model.Name). Where("name = ?", model.Name).
Count(&exists) Count(&exists)
logger.Error.Printf("looked for model.name %v and found %v", model.Name, exists) logger.Error.Printf("looked for model.name %v and found %v", model.Name, exists)
@@ -69,28 +103,25 @@ func Open(dbPath string, adminMail string) (*gorm.DB, error) {
var userCount int64 var userCount int64
db.Model(&models.User{}).Count(&userCount) db.Model(&models.User{}).Count(&userCount)
if userCount == 0 { if userCount == 0 {
var createdModel models.SubscriptionModel var createdModel models.Subscription
if err := db.First(&createdModel).Error; err != nil { if err := db.First(&createdModel).Error; err != nil {
return nil, err return nil, err
} }
admin, err := createAdmin(adminMail, createdModel.ID) admin, err := createAdmin(adminMail)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := db.Session(&gorm.Session{FullSaveAssociations: true}).Create(&admin) admin.Create(db)
if result.Error != nil {
return nil, result.Error
}
} }
return db, nil return db, nil
} }
func createSubscriptionModels() []models.SubscriptionModel { func createSubscriptions() []models.Subscription {
return []models.SubscriptionModel{ return []models.Subscription{
{ {
Name: constants.SupporterSubscriptionModelName, Name: constants.SupporterSubscriptionName,
Details: "Dieses Modell ist für Sponsoren und Nichtmitglieder, die keinen Vereinsmitglied sind.", Details: "Dieses Modell ist für Sponsoren und Nichtmitglieder, die keinen Vereinsmitglied sind.",
HourlyRate: 999, HourlyRate: 999,
MonthlyFee: 0, MonthlyFee: 0,
@@ -121,7 +152,7 @@ func createLicenceCategories() []models.Category {
// TODO: Landing page to create an admin // TODO: Landing page to create an admin
func createAdmin(userMail string, subscriptionModelID uint) (*models.User, error) { func createAdmin(userMail string) (*models.User, error) {
passwordBytes := make([]byte, 12) passwordBytes := make([]byte, 12)
_, err := rand.Read(passwordBytes) _, err := rand.Read(passwordBytes)
if err != nil { if err != nil {
@@ -131,37 +162,30 @@ func createAdmin(userMail string, subscriptionModelID uint) (*models.User, error
// Encode into a URL-safe base64 string // Encode into a URL-safe base64 string
password := base64.URLEncoding.EncodeToString(passwordBytes)[:12] password := base64.URLEncoding.EncodeToString(passwordBytes)[:12]
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return nil, err
}
logger.Error.Print("==============================================================") logger.Error.Print("==============================================================")
logger.Error.Printf("Admin Email: %v", userMail) logger.Error.Printf("Admin Email: %v", userMail)
logger.Error.Printf("Admin Password: %v", password) logger.Error.Printf("Admin Password: %v", password)
logger.Error.Print("==============================================================") logger.Error.Print("==============================================================")
return &models.User{ return &models.User{
FirstName: "ad", FirstName: "Ad",
LastName: "min", LastName: "Min",
DateOfBirth: time.Now().AddDate(-20, 0, 0), DateOfBirth: time.Now().AddDate(-20, 0, 0),
Password: hash, Password: password,
Address: "Downhill 4", Company: "",
ZipCode: "99999", Address: "",
City: "TechTown", ZipCode: "",
Phone: "0123455678", City: "",
Email: userMail, Phone: "",
Status: constants.ActiveStatus, Notes: "",
RoleID: constants.Roles.Admin, Email: userMail,
Membership: models.Membership{ Status: constants.ActiveStatus,
Status: constants.DisabledStatus, RoleID: constants.Roles.Admin,
StartDate: time.Now(), Consents: nil,
SubscriptionModelID: subscriptionModelID, Verifications: nil,
}, Membership: nil,
BankAccount: models.BankAccount{}, BankAccount: nil,
Licence: &models.Licence{ Licence: nil,
Status: constants.UnverifiedStatus,
},
}, nil }, nil
//"DE49700500000008447644", //fake //"DE49700500000008447644", //fake
} }

View File

@@ -0,0 +1,54 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Insurance struct {
ID uint `gorm:"primary_key" json:"id"`
Cars []Car `gorm:"many2many:car_insurances;" json:"-"`
Company string `json:"company" binding:"safe_content"`
Reference string `json:"reference" binding:"safe_content"`
Notes string `json:"notes" binding:"safe_content"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
}
func (i *Insurance) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(i).Error; err != nil {
return err
}
logger.Info.Printf("Insurance created: %#v", i)
// Preload all associations to return the fully populated User
return tx.
First(i, i.ID).Error // Refresh the user object with all associations
})
}
func (i *Insurance) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingInsurance Insurance
logger.Info.Printf("updating Insurance: %#v", i)
if err := tx.First(&existingInsurance, i.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingInsurance).Updates(i).Error; err != nil {
return err
}
return tx.First(i, i.ID).Error
})
}
func (i *Insurance) Delete(db *gorm.DB) error {
return db.Delete(&i).Error
}

View File

@@ -1,15 +1,60 @@
package models package models
import "time" import (
"GoMembership/internal/config"
"GoMembership/pkg/logger"
"fmt"
"time"
"gorm.io/gorm"
)
type BankAccount struct { type BankAccount struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
MandateDateSigned time.Time `gorm:"not null" json:"mandate_date_signed"` User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-" binding:"-"`
UserID uint `gorm:"index" json:"user_id"`
MandateDateSigned time.Time `json:"mandate_date_signed"`
Bank string `json:"bank_name" binding:"safe_content"` Bank string `json:"bank_name" binding:"safe_content"`
AccountHolderName string `json:"account_holder_name" binding:"safe_content"` AccountHolderName string `json:"account_holder_name" binding:"safe_content"`
IBAN string `json:"iban"` IBAN string `json:"iban" binding:"safe_content"`
BIC string `json:"bic"` BIC string `json:"bic" binding:"safe_content"`
MandateReference string `gorm:"not null" json:"mandate_reference"` MandateReference string `json:"mandate_reference" binding:"safe_content"`
ID uint `gorm:"primaryKey"` }
func (b *BankAccount) Create(db *gorm.DB) error {
// b.ID = 0
// only the children the belongs to association gets a reference id
if err := db.Create(b).Error; err != nil {
return err
}
logger.Info.Printf("BankAccount created: %#v", b)
return db.First(b, b.ID).Error // Refresh the object with all associations
}
func (b *BankAccount) Update(db *gorm.DB) error {
var existingBankAccount BankAccount
logger.Info.Printf("updating BankAccount: %#v", b)
if err := db.First(&existingBankAccount, b.ID).Error; err != nil {
return err
}
if err := db.Model(&existingBankAccount).Updates(b).Error; err != nil {
return err
}
return db.First(b, b.ID).Error
}
func (b *BankAccount) Delete(db *gorm.DB) error {
return db.Delete(&b).Error
}
func (b *BankAccount) GenerateMandateReference(id uint) string {
if b.IBAN == "" {
return ""
}
return fmt.Sprintf("%s-%s%d-%s", config.Company.SepaPrefix, time.Now().Format("20060102"), id, b.IBAN[len(b.IBAN)-4:])
} }

View File

@@ -0,0 +1,165 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Car struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Status uint `json:"status"`
Name string `json:"name"`
Brand string `gorm:"not null" json:"brand"`
Model string `gorm:"not null" json:"model"`
Color string `gorm:"not null" json:"color"`
LicencePlate string `gorm:"not null,unique" json:"licence_plate"`
Price float32 `gorm:"type:decimal(10,2)" json:"price"`
Rate float32 `gorm:"type:decimal(10,2)" json:"rate"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Location *Location `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"location"`
Damages []Damage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"damages"`
Insurances []Insurance `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;many2many:car_insurances" json:"insurances"`
Notes string `json:"notes"`
}
func (c *Car) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Preload(clause.Associations).Create(c).Error; err != nil {
return err
}
logger.Info.Printf("car created: %#v", c)
return tx.
Preload(clause.Associations).
First(c, c.ID).Error
})
}
func (c *Car) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingCar Car
logger.Info.Printf("updating car: %#v", c)
if err := tx.Preload("Damages.Insurance").
Preload("Damages.Opponent").
First(&existingCar, c.ID).Error; err != nil {
return err
}
if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(c).Error; err != nil {
return err
}
// if err := tx.Model(c).Association("Damages").Replace(c.Damages); err != nil {
// return err
// }
// if err := tx.Model(c).Association("Insurances").Replace(c.Insurances); err != nil {
// return err
// }
// Calculate damage IDs to delete
// existingDamageIDs := make(map[uint]bool)
// for _, d := range existingCar.Damages {
// existingDamageIDs[d.ID] = true
// }
// newDamageIDs := make(map[uint]bool)
// for _, d := range c.Damages {
// if d.ID != 0 {
// newDamageIDs[d.ID] = true
// }
// }
// // Find IDs to delete
// var toDelete []uint
// for id := range existingDamageIDs {
// if !newDamageIDs[id] {
// toDelete = append(toDelete, id)
// }
// }
// // Batch delete orphaned damages
// if len(toDelete) > 0 {
// if err := tx.Where("id IN ?", toDelete).Delete(&Damage{}).Error; err != nil {
// return err
// }
// }
// if len(c.Insurances) > 0 {
// logger.Info.Printf("updating insurances: %#v", c.Insurances)
// if err := tx.Model(&existingCar).Association("Insurances").Replace(c.Insurances); err != nil {
// return err
// }
// }
// // Upsert new damages
// for _, damage := range c.Damages {
// // Process relationships
// if damage.Opponent != nil {
// if err := tx.Save(damage.Opponent).Error; err != nil {
// return err
// }
// damage.OpponentID = damage.Opponent.ID
// }
// if damage.Insurance != nil {
// if err := tx.Save(damage.Insurance).Error; err != nil {
// return err
// }
// damage.InsuranceID = damage.Insurance.ID
// }
// // Create or update damage
// if err := tx.Save(damage).Error; err != nil {
// return err
// }
// }
// // Update associations
// if err := tx.Model(&existingCar).Association("Damages").Replace(c.Damages); err != nil {
// return err
// }
return tx.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
First(c, c.ID).Error
})
}
func (c *Car) Delete(db *gorm.DB) error {
return db.Select(clause.Associations).Delete(&c).Error
}
func GetAllCars(db *gorm.DB) ([]Car, error) {
var cars []Car
if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
Find(&cars).Error; err != nil {
return nil, err
}
return cars, nil
}
func (c *Car) FromID(db *gorm.DB, id uint) error {
var car Car
if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
First(&car, id).Error; err != nil {
return err
}
*c = car
return nil
}

View File

@@ -0,0 +1,48 @@
package models
import (
"GoMembership/pkg/logger"
"gorm.io/gorm"
)
type Category struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"category" binding:"safe_content"`
}
func (c *Category) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(c).Error; err != nil {
return err
}
logger.Info.Printf("Category created: %#v", c)
// Preload all associations to return the fully populated User
return tx.
First(c, c.ID).Error // Refresh the user object with all associations
})
}
func (c *Category) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingCategory Category
logger.Info.Printf("updating Category: %#v", c)
if err := tx.First(&existingCategory, c.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingCategory).Updates(c).Error; err != nil {
return err
}
return tx.First(c, c.ID).Error
})
}
func (c *Category) Delete(db *gorm.DB) error {
return db.Delete(&c).Error
}

View File

@@ -1,17 +1,56 @@
package models package models
import ( import (
"GoMembership/pkg/logger"
"strings"
"time" "time"
"gorm.io/gorm"
) )
type Consent struct { type Consent struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
FirstName string `gorm:"not null" json:"first_name" binding:"safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"safe_content"`
Email string `json:"email" binding:"email,safe_content"` Email string `json:"email" binding:"email,safe_content"`
ConsentType string `gorm:"not null" json:"consent_type" binding:"safe_content"` ConsentType string `gorm:"not null" json:"consent_type" binding:"safe_content"`
ID uint `gorm:"primaryKey"` UserID *uint `json:"user_id"`
User User User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"-" binding:"-"`
UserID uint }
func (c *Consent) BeforeSave(tx *gorm.DB) (err error) {
c.Email = strings.ToLower(c.Email)
return nil
}
func (c *Consent) Create(db *gorm.DB) error {
if err := db.Create(c).Error; err != nil {
return err
}
logger.Info.Printf("Consent created: %#v", c)
return db.First(c, c.ID).Error // Refresh the user object with all associations
}
func (c *Consent) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingConsent Consent
logger.Info.Printf("updating Consent: %#v", c)
if err := tx.First(&existingConsent, c.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingConsent).Updates(c).Error; err != nil {
return err
}
return tx.First(c, c.ID).Error
})
}
func (c *Consent) Delete(db *gorm.DB) error {
return db.Delete(&c).Error
} }

View File

@@ -0,0 +1,58 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Damage struct {
ID uint `gorm:"primaryKey" json:"id"`
CarID uint `json:"car_id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Name string `json:"name"`
Date time.Time `json:"date"`
Opponent *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"opponent"`
OpponentID uint `json:"opponent_id"`
Driver *User `json:"driver"`
DriverID uint `json:"driver_id"`
Insurance *Insurance `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
InsuranceID uint `json:"insurance_id"`
Notes string `json:"notes"`
}
func (d *Damage) Create(db *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := db.Create(d).Error; err != nil {
return err
}
logger.Info.Printf("Damage created: %#v", d)
// Preload all associations to return the fully populated User
return db.First(d, d.ID).Error // Refresh the user object with all associations
}
func (d *Damage) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingDamage Damage
logger.Info.Printf("updating Damage: %#v", d)
if err := tx.First(&existingDamage, d.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingDamage).Updates(d).Error; err != nil {
return err
}
return tx.First(d, d.ID).Error
})
}
func (d *Damage) Delete(db *gorm.DB) error {
return db.Delete(&d).Error
}

View File

@@ -1,7 +1,11 @@
package models package models
import ( import (
"GoMembership/pkg/logger"
"fmt"
"time" "time"
"gorm.io/gorm"
) )
type Licence struct { type Licence struct {
@@ -9,15 +13,54 @@ type Licence struct {
UserID uint `json:"user_id"` UserID uint `json:"user_id"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Status int8 `json:"status" binding:"omitempty,number"` Status int8 `json:"status" binding:"omitempty,number"`
Number string `json:"number" binding:"omitempty,safe_content"` Number string `json:"number" binding:"omitempty,safe_content"`
IssuedDate time.Time `json:"issued_date" binding:"omitempty"` IssuedDate time.Time `json:"issued_date" binding:"omitempty"`
ExpirationDate time.Time `json:"expiration_date" binding:"omitempty"` ExpirationDate time.Time `json:"expiration_date" binding:"omitempty"`
IssuingCountry string `json:"country" binding:"safe_content"` IssuingCountry string `json:"country" binding:"safe_content"`
Categories []Category `json:"categories" gorm:"many2many:licence_2_categories"` Categories []*Category `json:"categories" gorm:"many2many:licence_2_categories"`
} }
type Category struct { func (l *Licence) BeforeSafe(tx *gorm.DB) error {
ID uint `json:"id" gorm:"primaryKey"` if err := tx.Model(l).Association("Categories").Replace(l.Categories); err != nil {
Name string `json:"category" binding:"safe_content"` return fmt.Errorf("failed to link categories: %w", err)
}
return nil
}
func (l *Licence) Create(db *gorm.DB) error {
if err := db.Omit("Categories").Create(l).Error; err != nil {
return err
}
if err := db.Model(&l).Association("Categories").Replace(l.Categories); err != nil {
return err
}
logger.Info.Printf("Licence created: %#v", l)
return db.Preload("Categories").First(l, l.ID).Error // Refresh the object with Categories
}
func (l *Licence) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingLicence Licence
logger.Info.Printf("updating Licence: %#v", l)
if err := tx.First(&existingLicence, l.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingLicence).Updates(l).Error; err != nil {
return err
}
return tx.First(l, l.ID).Error
})
}
func (l *Licence) Delete(db *gorm.DB) error {
return db.Delete(&l).Error
} }

View File

@@ -0,0 +1,54 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Location struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
CarID uint `gorm:"index" json:"car_id"`
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
func (l *Location) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(l).Error; err != nil {
return err
}
logger.Info.Printf("Location created: %#v", l)
// Preload all associations to return the fully populated User
return tx.
First(l, l.ID).Error // Refresh the user object with all associations
})
}
func (l *Location) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingLocation Location
logger.Info.Printf("updating Location: %#v", l)
if err := tx.First(&existingLocation, l.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingLocation).Updates(l).Error; err != nil {
return err
}
return tx.First(l, l.ID).Error
})
}
func (l *Location) Delete(db *gorm.DB) error {
return db.Delete(&l).Error
}

View File

@@ -1,15 +1,59 @@
package models package models
import "time" import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Membership struct { type Membership struct {
CreatedAt time.Time ID uint `gorm:"primaryKey" json:"id"`
UpdatedAt time.Time UserID uint `gorm:"index" json:"user_id"`
StartDate time.Time `json:"start_date"` User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-" binding:"-"`
EndDate time.Time `json:"end_date"` CreatedAt time.Time
Status int8 `json:"status" binding:"number,safe_content"` UpdatedAt time.Time
SubscriptionModel SubscriptionModel `gorm:"foreignKey:SubscriptionModelID" json:"subscription_model"` StartDate time.Time `json:"start_date"`
SubscriptionModelID uint `json:"subsription_model_id"` EndDate time.Time `json:"end_date"`
ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"` Status int8 `json:"status" binding:"number,safe_content"`
ID uint `json:"id"` Subscription Subscription `gorm:"foreignKey:SubscriptionID" json:"subscription"`
SubscriptionID uint `json:"subscription_id"`
ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"`
}
func (m *Membership) BeforeSave(tx *gorm.DB) error {
m.SubscriptionID = m.Subscription.ID
return nil
}
func (m *Membership) Create(db *gorm.DB) error {
if err := db.Create(m).Error; err != nil {
return err
}
logger.Info.Printf("Membership created: %#v", m)
return db.Preload("Subscription").First(m, m.ID).Error // Refresh the user object with Subscription
}
func (m *Membership) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingMembership Membership
logger.Info.Printf("updating Membership: %#v", m)
if err := tx.First(&existingMembership, m.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingMembership).Updates(m).Error; err != nil {
return err
}
return tx.First(m, m.ID).Error
})
}
func (m *Membership) Delete(db *gorm.DB) error {
return db.Delete(&m).Error
} }

View File

@@ -0,0 +1,58 @@
package models
import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Subscription struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"uniqueIndex:idx_subscriptions_name" json:"name" binding:"required,safe_content"`
Details string `json:"details" binding:"safe_content"`
Conditions string `json:"conditions" binding:"safe_content"`
RequiredMembershipField string `json:"required_membership_field" binding:"safe_content"`
MonthlyFee float32 `json:"monthly_fee"`
HourlyRate float32 `json:"hourly_rate"`
IncludedPerYear int16 `json:"included_hours_per_year"`
IncludedPerMonth int16 `json:"included_hours_per_month"`
}
func (s *Subscription) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately)
if err := tx.Create(s).Error; err != nil {
return err
}
logger.Info.Printf("Subscription created: %#v", s)
// Preload all associations to retuvn the fully populated User
return tx.
First(s, s.ID).Error // Refresh the user object with all associations
})
}
func (s *Subscription) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingSubscription Subscription
logger.Info.Printf("updating Subscription: %#v", s)
if err := tx.First(&existingSubscription, s.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingSubscription).Updates(s).Error; err != nil {
return err
}
return tx.First(s, s.ID).Error
})
}
func (s *Subscription) Delete(db *gorm.DB) error {
return db.Delete(&s).Error
}

View File

@@ -1,19 +0,0 @@
package models
import (
"time"
)
type SubscriptionModel struct {
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"unique" json:"name" binding:"required"`
Details string `json:"details"`
Conditions string `json:"conditions"`
RequiredMembershipField string `json:"required_membership_field"`
ID uint `json:"id" gorm:"primaryKey"`
MonthlyFee float32 `json:"monthly_fee"`
HourlyRate float32 `json:"hourly_rate"`
IncludedPerYear int16 `json:"included_hours_per_year"`
IncludedPerMonth int16 `json:"included_hours_per_month"`
}

View File

@@ -3,11 +3,11 @@ package models
import ( import (
"GoMembership/internal/config" "GoMembership/internal/config"
"GoMembership/internal/constants" "GoMembership/internal/constants"
"GoMembership/internal/utils"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"fmt" "fmt"
"slices" "slices"
"strings"
"time" "time"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
@@ -18,94 +18,58 @@ import (
) )
type User struct { type User struct {
ID uint `gorm:"primarykey" json:"id"` ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time
DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"` DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"`
Company string `json:"company" binding:"omitempty,omitnil,safe_content"` Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"` Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" binding:"safe_content"` Notes string `json:"notes" binding:"safe_content"`
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
Password string `json:"password" binding:"safe_content"` Password string `json:"password" binding:"safe_content"`
Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"` Email string `gorm:"uniqueIndex:idx_users_email,not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"` Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
Address string `gorm:"not null" json:"address" binding:"required,safe_content"` ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"` City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"` Consents []Consent `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"` BankAccount *BankAccount `gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"bank_account"`
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"` Verifications []Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
BankAccountID uint Membership *Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"membership"`
Verifications *[]Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"licence"`
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"` Status int8 `json:"status"`
MembershipID uint RoleID int8 `json:"role_id"`
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
LicenceID uint
PaymentStatus int8 `json:"payment_status"`
Status int8 `json:"status"`
RoleID int8 `json:"role_id"`
} }
func (u *User) AfterCreate(tx *gorm.DB) (err error) { func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.BankAccount.ID != 0 && u.BankAccount.MandateReference == "" { if u.BankAccount != nil && u.BankAccount.MandateReference == "" {
mandateReference := u.GenerateMandateReference() u.BankAccount.MandateReference = u.BankAccount.GenerateMandateReference(u.ID)
u.BankAccount.Update(tx)
return tx.Model(&u.BankAccount).Update("MandateReference", mandateReference).Error
} }
return nil return nil
} }
func (u *User) GenerateMandateReference() string { func (u *User) BeforeSave(tx *gorm.DB) (err error) {
return fmt.Sprintf("%s%d%s", time.Now().Format("20060102"), u.ID, u.BankAccount.IBAN) u.Email = strings.ToLower(u.Email)
} if u.Password != "" {
hash, err := argon2id.CreateHash(u.Password, argon2id.DefaultParams)
func (u *User) SetPassword(plaintextPassword string) error { if err != nil {
if plaintextPassword == "" { return err
return nil }
u.Password = hash
} }
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil {
return err
}
u.Password = hash
return nil return nil
} }
func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password)
}
func (u *User) PasswordExists() bool {
return u.Password != ""
}
func (u *User) Delete(db *gorm.DB) error {
return db.Delete(&User{}, "id = ?", u.ID).Error
}
func (u *User) Create(db *gorm.DB) error { func (u *User) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
// Create the base User record (omit associations to handle them separately) if err := tx.Preload(clause.Associations).Create(u).Error; err != nil {
if err := tx.Create(u).Error; err != nil {
return err return err
} }
// Replace associated Categories (assumes Categories already exist) return nil
if u.Licence != nil && len(u.Licence.Categories) > 0 {
if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
return err
}
}
logger.Info.Printf("user created: %#v", u.Safe())
// Preload all associations to return the fully populated User
return tx.
Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories").
First(u, u.ID).Error // Refresh the user object with all associations
}) })
} }
@@ -117,16 +81,11 @@ func (u *User) Update(db *gorm.DB) error {
logger.Info.Printf("updating user: %#v", u) logger.Info.Printf("updating user: %#v", u)
if err := tx. if err := tx.
Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories").
Preload("Verifications").
First(&existingUser, u.ID).Error; err != nil { First(&existingUser, u.ID).Error; err != nil {
return err return err
} }
// Update the user's main fields // Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Password", "Membership", "Licence", "Verifications").Updates(u) result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Verifications", "Licence.Categories").Updates(u)
if result.Error != nil { if result.Error != nil {
logger.Error.Printf("User update error in update user: %#v", result.Error) logger.Error.Printf("User update error in update user: %#v", result.Error)
return result.Error return result.Error
@@ -135,59 +94,14 @@ func (u *User) Update(db *gorm.DB) error {
return errors.ErrNoRowsAffected return errors.ErrNoRowsAffected
} }
if u.Password != "" {
if err := tx.Model(&existingUser).
Update("Password", u.Password).Error; err != nil {
logger.Error.Printf("Password update error in update user: %#v", err)
return err
}
}
// Update the Membership if provided
if u.Membership.ID != 0 {
if err := tx.Model(&existingUser.Membership).Updates(u.Membership).Error; err != nil {
logger.Error.Printf("Membership update error in update user: %#v", err)
return err
}
}
if u.Licence != nil {
u.Licence.UserID = existingUser.ID
if err := tx.Save(u.Licence).Error; err != nil {
return err
}
if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil {
return err
}
if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
return err
}
}
// if u.Licence != nil {
// if existingUser.Licence == nil || existingUser.LicenceID == 0 {
// u.Licence.UserID = existingUser.ID // Ensure Licence belongs to User
// if err := tx.Create(u.Licence).Error; err != nil {
// return err
// }
// existingUser.Licence = u.Licence
// existingUser.LicenceID = u.Licence.ID
// if err := tx.Model(&existingUser).Update("LicenceID", u.Licence.ID).Error; err != nil {
// return err
// }
// }
// if err := tx.Model(existingUser.Licence).Updates(u.Licence).Error; err != nil {
// return err
// }
// // Update Categories association
// if err := tx.Model(existingUser.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return err
// }
// }
if u.Verifications != nil { if u.Verifications != nil {
if err := tx.Save(*u.Verifications).Error; err != nil { if err := tx.Save(u.Verifications).Error; err != nil {
return err
}
}
if u.Licence != nil {
if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
return err return err
} }
} }
@@ -199,23 +113,23 @@ func (u *User) Update(db *gorm.DB) error {
} }
return db. return db.
Preload("Membership"). Preload(clause.Associations).
Preload("Membership.SubscriptionModel"). Preload("Membership.Subscription").
Preload("Licence").
Preload("Licence.Categories"). Preload("Licence.Categories").
Preload("Verifications").
First(&u, u.ID).Error First(&u, u.ID).Error
} }
func (u *User) Delete(db *gorm.DB) error {
return db.Unscoped().Delete(&User{}, "id = ?", u.ID).Error
// return db.Delete(&User{}, "id = ?", u.ID).Error
}
func (u *User) FromID(db *gorm.DB, userID *uint) error { func (u *User) FromID(db *gorm.DB, userID *uint) error {
var user User var user User
result := db. result := db.
Preload(clause.Associations). Preload(clause.Associations).
Preload("Membership"). Preload("Membership.Subscription").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories"). Preload("Licence.Categories").
Preload("Verifications").
First(&user, userID) First(&user, userID)
if result.Error != nil { if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound { if result.Error == gorm.ErrRecordNotFound {
@@ -231,11 +145,8 @@ func (u *User) FromEmail(db *gorm.DB, email *string) error {
var user User var user User
result := db. result := db.
Preload(clause.Associations). Preload(clause.Associations).
Preload("Membership"). Preload("Membership.Subscription").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories"). Preload("Licence.Categories").
Preload("Verifications").
Where("email = ?", email).First(&user) Where("email = ?", email).First(&user)
if result.Error != nil { if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound { if result.Error == gorm.ErrRecordNotFound {
@@ -262,6 +173,14 @@ func (u *User) FromContext(db *gorm.DB, c *gin.Context) error {
return nil return nil
} }
func (u *User) PasswordMatches(plaintextPassword string) (bool, error) {
return argon2id.ComparePasswordAndHash(plaintextPassword, u.Password)
}
func (u *User) PasswordExists() bool {
return u.Password != ""
}
func (u *User) IsVerified() bool { func (u *User) IsVerified() bool {
return u.Status > constants.DisabledStatus return u.Status > constants.DisabledStatus
} }
@@ -284,108 +203,74 @@ func (u *User) IsSupporter() bool {
func (u *User) SetVerification(verificationType string) (*Verification, error) { func (u *User) SetVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil { if u.Verifications == nil {
u.Verifications = new([]Verification) u.Verifications = []Verification{}
} }
token, err := utils.GenerateVerificationToken() v, err := CreateVerification(verificationType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
v := Verification{ v.UserID = u.ID
UserID: u.ID, if vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 {
VerificationToken: token, u.Verifications[vi] = *v
Type: verificationType,
}
if vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 {
(*u.Verifications)[vi] = v
} else { } else {
*u.Verifications = append(*u.Verifications, v) u.Verifications = append(u.Verifications, *v)
} }
return &v, nil return v, nil
} }
func (u *User) GetVerification(verificationType string) (*Verification, error) { func (u *User) FindVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil { if u.Verifications == nil {
return nil, errors.ErrNoData return nil, errors.ErrNoData
} }
vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { return vsl.Type == verificationType }) vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == verificationType })
if vi == -1 { if vi == -1 {
return nil, errors.ErrNotFound return nil, errors.ErrNotFound
} }
return &(*u.Verifications)[vi], nil return &u.Verifications[vi], nil
} }
func (u *User) Verify(token string, verificationType string) bool { func (u *User) Verify(token string, verificationType string) error {
if token == "" || verificationType == "" { if token == "" || verificationType == "" {
logger.Error.Printf("token or verification type are empty in user.Verify") logger.Error.Printf("token or verification type are empty in user.Verify")
return false return errors.ErrNoData
} }
vi := slices.IndexFunc(*u.Verifications, func(vsl Verification) bool { vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool {
return vsl.Type == verificationType && vsl.VerificationToken == token return vsl.Type == verificationType && vsl.VerificationToken == token
}) })
if vi == -1 { if vi == -1 {
logger.Error.Printf("Couldn't find verification in users verifications") logger.Error.Printf("Couldn't find verification in users verifications")
return false return errors.ErrNotFound
} }
return u.Verifications[vi].Validate()
if (*u.Verifications)[vi].VerifiedAt != nil {
logger.Error.Printf("VerifiedAt is not nil, already verified?: %#v", (*u.Verifications)[vi])
return false
}
t := time.Now()
(*u.Verifications)[vi].VerifiedAt = &t
return true
} }
func (u *User) Safe() map[string]interface{} { func (u *User) Safe() map[string]interface{} {
result := map[string]interface{}{ var membership map[string]interface{} = nil
"email": u.Email, var licence map[string]interface{} = nil
"first_name": u.FirstName, var bankAccount map[string]interface{} = nil
"last_name": u.LastName, if u.Membership != nil {
"phone": u.Phone, membership = map[string]interface{}{
"notes": u.Notes,
"address": u.Address,
"zip_code": u.ZipCode,
"city": u.City,
"status": u.Status,
"id": u.ID,
"role_id": u.RoleID,
"company": u.Company,
"dateofbirth": u.DateOfBirth,
"membership": map[string]interface{}{
"id": u.Membership.ID, "id": u.Membership.ID,
"start_date": u.Membership.StartDate, "start_date": u.Membership.StartDate,
"end_date": u.Membership.EndDate, "end_date": u.Membership.EndDate,
"status": u.Membership.Status, "status": u.Membership.Status,
"subscription_model": map[string]interface{}{ "subscription": map[string]interface{}{
"id": u.Membership.SubscriptionModel.ID, "id": u.Membership.Subscription.ID,
"name": u.Membership.SubscriptionModel.Name, "name": u.Membership.Subscription.Name,
"details": u.Membership.SubscriptionModel.Details, "details": u.Membership.Subscription.Details,
"conditions": u.Membership.SubscriptionModel.Conditions, "conditions": u.Membership.Subscription.Conditions,
"monthly_fee": u.Membership.SubscriptionModel.MonthlyFee, "monthly_fee": u.Membership.Subscription.MonthlyFee,
"hourly_rate": u.Membership.SubscriptionModel.HourlyRate, "hourly_rate": u.Membership.Subscription.HourlyRate,
"included_per_year": u.Membership.SubscriptionModel.IncludedPerYear, "included_per_year": u.Membership.Subscription.IncludedPerYear,
"included_per_month": u.Membership.SubscriptionModel.IncludedPerMonth, "included_per_month": u.Membership.Subscription.IncludedPerMonth,
}, },
}, }
"licence": map[string]interface{}{
"id": 0,
},
"bank_account": map[string]interface{}{
"id": u.BankAccount.ID,
"mandate_date_signed": u.BankAccount.MandateDateSigned,
"bank": u.BankAccount.Bank,
"account_holder_name": u.BankAccount.AccountHolderName,
"iban": u.BankAccount.IBAN,
"bic": u.BankAccount.BIC,
"mandate_reference": u.BankAccount.MandateReference,
},
} }
if u.Licence != nil { if u.Licence != nil {
result["licence"] = map[string]interface{}{ licence = map[string]interface{}{
"id": u.Licence.ID, "id": u.Licence.ID,
"number": u.Licence.Number, "number": u.Licence.Number,
"status": u.Licence.Status, "status": u.Licence.Status,
@@ -396,6 +281,36 @@ func (u *User) Safe() map[string]interface{} {
} }
} }
if u.BankAccount != nil {
bankAccount = map[string]interface{}{
"id": u.BankAccount.ID,
"mandate_date_signed": u.BankAccount.MandateDateSigned,
"bank": u.BankAccount.Bank,
"account_holder_name": u.BankAccount.AccountHolderName,
"iban": u.BankAccount.IBAN,
"bic": u.BankAccount.BIC,
"mandate_reference": u.BankAccount.MandateReference,
}
}
result := map[string]interface{}{
"email": u.Email,
"first_name": u.FirstName,
"last_name": u.LastName,
"phone": u.Phone,
"notes": u.Notes,
"address": u.Address,
"zip_code": u.ZipCode,
"city": u.City,
"status": u.Status,
"id": u.ID,
"role_id": u.RoleID,
"company": u.Company,
"dateofbirth": u.DateOfBirth,
"membership": membership,
"licence": licence,
"bank_account": bankAccount,
}
return result return result
} }
@@ -439,14 +354,12 @@ func extractUserIDFrom(tokenString string) (uint, error) {
} }
func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) { func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) {
logger.Error.Printf("where: %#v", where)
var users []User var users []User
result := db. result := db.
Preload(clause.Associations). Preload(clause.Associations).
Preload("Membership"). Preload("Membership.Subscription").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories"). Preload("Licence.Categories").
Preload("Verifications").
Where(where).Find(&users) Where(where).Find(&users)
if result.Error != nil { if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound { if result.Error == gorm.ErrRecordNotFound {

View File

@@ -1,15 +1,78 @@
package models package models
import ( import (
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"time" "time"
"gorm.io/gorm"
) )
type Verification struct { type Verification struct {
UpdatedAt time.Time gorm.Model
CreatedAt time.Time
VerifiedAt *time.Time `json:"verified_at"` VerifiedAt *time.Time `json:"verified_at"`
VerificationToken string `json:"token"` VerificationToken string `json:"token"`
ID uint `gorm:"primaryKey"`
UserID uint `json:"user_id"` UserID uint `json:"user_id"`
Type string Type string
} }
func (v *Verification) Create(db *gorm.DB) error {
if err := db.Create(v).Error; err != nil {
return err
}
logger.Info.Printf("verification created: %#v", v)
// Preload all associations to return the fully populated object
return db.First(v, v.ID).Error // Refresh the verification object with all associations
}
func (v *Verification) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingVerification Verification
logger.Info.Printf("updating verification: %#v", v)
if err := tx.First(&existingVerification, v.ID).Error; err != nil {
return err
}
if err := tx.Model(&existingVerification).Updates(v).Error; err != nil {
return err
}
return tx.First(v, v.ID).Error
})
}
func (v *Verification) Delete(db *gorm.DB) error {
return db.Delete(&v).Error
}
func (v *Verification) Validate() error {
if v.VerifiedAt != nil {
return errors.ErrAlreadyVerified
}
t := time.Now()
v.VerifiedAt = &t
return nil
}
func CreateVerification(verificationType string) (*Verification, error) {
token, err := GenerateVerificationToken()
if err != nil {
return nil, err
}
v := Verification{
UserID: 0,
VerificationToken: token,
Type: verificationType,
}
return &v, nil
}
func GenerateVerificationToken() (string, error) {
return utils.GenerateRandomString(32)
}

View File

@@ -0,0 +1,69 @@
package repositories
import (
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/pkg/errors"
"gorm.io/gorm"
)
// CarRepository interface defines the CRUD operations
type CarRepositoryInterface interface {
Create(car *models.Car) (*models.Car, error)
GetByID(id uint) (*models.Car, error)
GetAll() ([]models.Car, error)
Update(car *models.Car) (*models.Car, error)
Delete(id uint) error
}
type CarRepository struct{}
// Create a new car
func (r *CarRepository) Create(car *models.Car) (*models.Car, error) {
if err := database.DB.Create(car).Error; err != nil {
return nil, err
}
return car, nil
}
// GetByID fetches a car by its ID
func (r *CarRepository) GetByID(id uint) (*models.Car, error) {
var car models.Car
if err := database.DB.Where("id = ?", id).First(&car).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.ErrNotFound
}
return nil, err
}
return &car, nil
}
// GetAll retrieves all cars
func (r *CarRepository) GetAll() ([]models.Car, error) {
var cars []models.Car
if err := database.DB.Find(&cars).Error; err != nil {
return nil, err
}
return cars, nil
}
// Update an existing car
func (r *CarRepository) Update(car *models.Car) (*models.Car, error) {
if err := database.DB.Save(car).Error; err != nil {
return nil, err
}
return car, nil
}
// Delete a car (soft delete)
func (r *CarRepository) Delete(id uint) error {
result := database.DB.Delete(&models.Car{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.ErrNotFound
}
return nil
}

View File

@@ -1,97 +0,0 @@
package repositories
import (
"GoMembership/internal/database"
"gorm.io/gorm"
"GoMembership/internal/models"
)
type SubscriptionModelsRepositoryInterface interface {
CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error)
UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error)
GetSubscriptionModelNames() ([]string, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
// GetUsersBySubscription(id uint) (*[]models.SubscriptionModel, error)
DeleteSubscription(id *uint) error
}
type SubscriptionModelsRepository struct{}
func (sr *SubscriptionModelsRepository) CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error) {
result := database.DB.Create(subscriptionModel)
if result.Error != nil {
return 0, result.Error
}
return subscriptionModel.ID, nil
}
func (sr *SubscriptionModelsRepository) UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) {
result := database.DB.Model(&models.SubscriptionModel{ID: subscription.ID}).Updates(subscription)
if result.Error != nil {
return nil, result.Error
}
return subscription, nil
}
func (sr *SubscriptionModelsRepository) DeleteSubscription(id *uint) error {
result := database.DB.Delete(&models.SubscriptionModel{}, id)
if result.Error != nil {
return result.Error
}
return nil
}
func GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) {
var model models.SubscriptionModel
result := database.DB.Where("name = ?", modelname).First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}
func (sr *SubscriptionModelsRepository) GetSubscriptionModelNames() ([]string, error) {
var names []string
if err := database.DB.Model(&models.SubscriptionModel{}).Pluck("name", &names).Error; err != nil {
return []string{}, err
}
return names, nil
}
func (sr *SubscriptionModelsRepository) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) {
var subscriptions []models.SubscriptionModel
result := database.DB.Where(where).Find(&subscriptions)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &subscriptions, nil
}
func GetUsersBySubscription(subscriptionID uint) (*[]models.User, error) {
var users []models.User
err := database.DB.Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("BankAccount").
Preload("Licence").
Preload("Licence.Categories").
Joins("JOIN memberships ON users.membership_id = memberships.id").
Joins("JOIN subscription_models ON memberships.subscription_model_id = subscription_models.id").
Where("subscription_models.id = ?", subscriptionID).
Find(&users).Error
if err != nil {
return nil, err
}
return &users, nil
}

View File

@@ -0,0 +1,97 @@
package repositories
import (
"GoMembership/internal/database"
"gorm.io/gorm"
"GoMembership/internal/models"
)
type SubscriptionsRepositoryInterface interface {
CreateSubscription(subscription *models.Subscription) (uint, error)
UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error)
GetSubscriptionNames() ([]string, error)
GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error)
// GetUsersBySubscription(id uint) (*[]models.Subscription, error)
DeleteSubscription(id *uint) error
}
type SubscriptionsRepository struct{}
func (sr *SubscriptionsRepository) CreateSubscription(subscription *models.Subscription) (uint, error) {
result := database.DB.Create(subscription)
if result.Error != nil {
return 0, result.Error
}
return subscription.ID, nil
}
func (sr *SubscriptionsRepository) UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error) {
result := database.DB.Model(&models.Subscription{ID: subscription.ID}).Updates(subscription)
if result.Error != nil {
return nil, result.Error
}
return subscription, nil
}
func (sr *SubscriptionsRepository) DeleteSubscription(id *uint) error {
result := database.DB.Delete(&models.Subscription{}, id)
if result.Error != nil {
return result.Error
}
return nil
}
func GetSubscriptionByName(modelname *string) (*models.Subscription, error) {
var model models.Subscription
result := database.DB.Where("name = ?", modelname).First(&model)
if result.Error != nil {
return nil, result.Error
}
return &model, nil
}
func (sr *SubscriptionsRepository) GetSubscriptionNames() ([]string, error) {
var names []string
if err := database.DB.Model(&models.Subscription{}).Pluck("name", &names).Error; err != nil {
return []string{}, err
}
return names, nil
}
func (sr *SubscriptionsRepository) GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error) {
var subscriptions []models.Subscription
result := database.DB.Where(where).Find(&subscriptions)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &subscriptions, nil
}
func GetUsersBySubscription(subscriptionID uint) (*[]models.User, error) {
var users []models.User
err := database.DB.Preload("Membership").
Preload("Membership.Subscription").
Preload("BankAccount").
Preload("Licence").
Preload("Licence.Categories").
Joins("JOIN memberships ON users.id = memberships.user_id").
Joins("JOIN subscriptions ON memberships.subscription_id = subscriptions.id").
Where("subscriptions.id = ?", subscriptionID).
Find(&users).Error
if err != nil {
return nil, err
}
return &users, nil
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController, licenceController *controllers.LicenceController) { func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController, licenceController *controllers.LicenceController, carController *controllers.CarController) {
router.GET("/api/users/verify/:id", userController.VerifyMailHandler) router.GET("/api/users/verify/:id", userController.VerifyMailHandler)
router.POST("/api/users/register", userController.RegisterUser) router.POST("/api/users/register", userController.RegisterUser)
router.POST("/api/users/contact", contactController.RelayContactRequest) router.POST("/api/users/contact", contactController.RelayContactRequest)
@@ -19,6 +19,10 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
userRouter := router.Group("/api/auth") userRouter := router.Group("/api/auth")
userRouter.Use(middlewares.AuthMiddleware()) userRouter.Use(middlewares.AuthMiddleware())
{ {
userRouter.GET("/cars", carController.GetAll)
userRouter.PUT("/cars", carController.Update)
userRouter.POST("/cars", carController.Create)
userRouter.DELETE("/cars", carController.Delete)
userRouter.GET("/users/current", userController.CurrentUserHandler) userRouter.GET("/users/current", userController.CurrentUserHandler)
userRouter.POST("/logout", userController.LogoutHandler) userRouter.POST("/logout", userController.LogoutHandler)
userRouter.PUT("/users", userController.UpdateHandler) userRouter.PUT("/users", userController.UpdateHandler)

View File

@@ -37,7 +37,7 @@ func Run(db *gorm.DB) {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo} bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{} var membershipRepo repositories.MembershipRepositoryInterface = &repositories.MembershipRepository{}
var subscriptionRepo repositories.SubscriptionModelsRepositoryInterface = &repositories.SubscriptionModelsRepository{} var subscriptionRepo repositories.SubscriptionsRepositoryInterface = &repositories.SubscriptionsRepository{}
membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo} membershipService := &services.MembershipService{Repo: membershipRepo, SubscriptionRepo: subscriptionRepo}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{} var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
@@ -48,6 +48,8 @@ func Run(db *gorm.DB) {
membershipController := &controllers.MembershipController{Service: membershipService, UserService: userService} membershipController := &controllers.MembershipController{Service: membershipService, UserService: userService}
licenceController := &controllers.LicenceController{Service: licenceService} licenceController := &controllers.LicenceController{Service: licenceService}
contactController := &controllers.ContactController{EmailService: emailService} contactController := &controllers.ContactController{EmailService: emailService}
carService := &services.CarService{DB: db}
carController := &controllers.CarController{S: carService, UserService: userService}
router := gin.Default() router := gin.Default()
// gin.SetMode(gin.ReleaseMode) // gin.SetMode(gin.ReleaseMode)
@@ -63,7 +65,7 @@ func Run(db *gorm.DB) {
limiter := middlewares.NewIPRateLimiter(config.Security.Ratelimits.Limit, config.Security.Ratelimits.Burst) limiter := middlewares.NewIPRateLimiter(config.Security.Ratelimits.Limit, config.Security.Ratelimits.Burst)
router.Use(middlewares.RateLimitMiddleware(limiter)) router.Use(middlewares.RateLimitMiddleware(limiter))
routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController) routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController, carController)
validation.SetupValidators(db) validation.SetupValidators(db)
logger.Info.Println("Starting server on :8080") logger.Info.Println("Starting server on :8080")

View File

@@ -0,0 +1,67 @@
package services
import (
"GoMembership/internal/models"
"gorm.io/gorm"
)
type CarServiceInterface interface {
Create(car *models.Car) (*models.Car, error)
Update(car *models.Car) (*models.Car, error)
Delete(carID *uint) error
FromID(id uint) (*models.Car, error)
GetAll() (*[]models.Car, error)
}
type CarService struct {
DB *gorm.DB
}
// Create a new car
func (s *CarService) Create(car *models.Car) (*models.Car, error) {
err := car.Create(s.DB)
if err != nil {
return nil, err
}
return car, nil
}
// Update an existing car
func (s *CarService) Update(car *models.Car) (*models.Car, error) {
err := car.Update(s.DB)
if err != nil {
return nil, err
}
return car, nil
}
// Delete a car (soft delete)
func (s *CarService) Delete(carID *uint) error {
var car models.Car
err := car.FromID(s.DB, *carID)
if err != nil {
return err
}
return car.Delete(s.DB)
}
// GetByID fetches a car by its ID
func (s *CarService) FromID(id uint) (*models.Car, error) {
car := &models.Car{}
err := car.FromID(s.DB, id)
if err != nil {
return nil, err
}
return car, nil
}
// GetAll retrieves all cars
func (s *CarService) GetAll() (*[]models.Car, error) {
cars, err := models.GetAllCars(s.DB)
if err != nil {
return nil, err
}
return &cars, nil
}

View File

@@ -46,7 +46,7 @@ func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT
func ParseTemplate(filename string, data interface{}) (string, error) { func ParseTemplate(filename string, data interface{}) (string, error) {
// Read the email template file // Read the email template file
logger.Error.Printf("Data: %#v", data)
templateDir := config.Templates.MailPath templateDir := config.Templates.MailPath
tpl, err := template.ParseFiles(templateDir + "/" + filename) tpl, err := template.ParseFiles(templateDir + "/" + filename)
if err != nil { if err != nil {
@@ -71,15 +71,18 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
LastName string LastName string
Token string Token string
BASEURL string BASEURL string
ID uint UserID uint
Logo string
}{ }{
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
Token: *token, Token: *token,
BASEURL: config.Site.BaseURL, BASEURL: config.Site.BaseURL,
ID: user.ID, UserID: user.ID,
Logo: config.Templates.LogoURI,
} }
logger.Error.Printf("USERIID: %#v", user.ID)
subject := constants.MailVerificationSubject subject := constants.MailVerificationSubject
body, err := ParseTemplate("mail_verification.tmpl", data) body, err := ParseTemplate("mail_verification.tmpl", data)
if err != nil { if err != nil {
@@ -98,6 +101,7 @@ func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *str
BASEURL string BASEURL string
FRONTEND_PATH string FRONTEND_PATH string
UserID uint UserID uint
Logo string
}{ }{
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
@@ -105,6 +109,7 @@ func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *str
FRONTEND_PATH: config.Site.FrontendPath, FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL, BASEURL: config.Site.BaseURL,
UserID: user.ID, UserID: user.ID,
Logo: config.Templates.LogoURI,
} }
subject := constants.MailGrantBackendAccessSubject subject := constants.MailGrantBackendAccessSubject
@@ -130,6 +135,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
BASEURL string BASEURL string
FRONTEND_PATH string FRONTEND_PATH string
UserID uint UserID uint
Logo string
}{ }{
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
@@ -137,6 +143,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
FRONTEND_PATH: config.Site.FrontendPath, FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL, BASEURL: config.Site.BaseURL,
UserID: user.ID, UserID: user.ID,
Logo: config.Templates.LogoURI,
} }
subject := constants.MailChangePasswordSubject subject := constants.MailChangePasswordSubject
@@ -169,10 +176,10 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
}{ }{
Company: user.Company, Company: user.Company,
FirstName: user.FirstName, FirstName: user.FirstName,
MembershipModel: user.Membership.SubscriptionModel.Name, MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID, MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee), MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate), RentalFee: float32(user.Membership.Subscription.HourlyRate),
BASEURL: config.Site.BaseURL, BASEURL: config.Site.BaseURL,
WebsiteTitle: config.Site.WebsiteTitle, WebsiteTitle: config.Site.WebsiteTitle,
Logo: config.Templates.LogoURI, Logo: config.Templates.LogoURI,
@@ -216,10 +223,10 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
Company: user.Company, Company: user.Company,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
MembershipModel: user.Membership.SubscriptionModel.Name, MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID, MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee), MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate), RentalFee: float32(user.Membership.Subscription.HourlyRate),
Address: user.Address, Address: user.Address,
ZipCode: user.ZipCode, ZipCode: user.ZipCode,
City: user.City, City: user.City,

View File

@@ -11,17 +11,17 @@ import (
type MembershipServiceInterface interface { type MembershipServiceInterface interface {
RegisterMembership(membership *models.Membership) (uint, error) RegisterMembership(membership *models.Membership) (uint, error)
FindMembershipByUserID(userID uint) (*models.Membership, error) FindMembershipByUserID(userID uint) (*models.Membership, error)
RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) RegisterSubscription(subscription *models.Subscription) (uint, error)
UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error)
DeleteSubscription(id *uint, name *string) error DeleteSubscription(id *uint, name *string) error
GetSubscriptionModelNames() ([]string, error) GetSubscriptionNames() ([]string, error)
GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) GetSubscriptionByName(modelname *string) (*models.Subscription, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error)
} }
type MembershipService struct { type MembershipService struct {
Repo repositories.MembershipRepositoryInterface Repo repositories.MembershipRepositoryInterface
SubscriptionRepo repositories.SubscriptionModelsRepositoryInterface SubscriptionRepo repositories.SubscriptionsRepositoryInterface
} }
func (service *MembershipService) RegisterMembership(membership *models.Membership) (uint, error) { func (service *MembershipService) RegisterMembership(membership *models.Membership) (uint, error) {
@@ -29,7 +29,7 @@ func (service *MembershipService) RegisterMembership(membership *models.Membersh
return service.Repo.CreateMembership(membership) return service.Repo.CreateMembership(membership)
} }
func (service *MembershipService) UpdateSubscription(subscription *models.SubscriptionModel) (*models.SubscriptionModel, error) { func (service *MembershipService) UpdateSubscription(subscription *models.Subscription) (*models.Subscription, error) {
existingSubscription, err := repositories.GetSubscriptionByName(&subscription.Name) existingSubscription, err := repositories.GetSubscriptionByName(&subscription.Name)
if err != nil { if err != nil {
@@ -82,19 +82,19 @@ func (service *MembershipService) FindMembershipByUserID(userID uint) (*models.M
} }
// Membership_Subscriptions // Membership_Subscriptions
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) { func (service *MembershipService) RegisterSubscription(subscription *models.Subscription) (uint, error) {
return service.SubscriptionRepo.CreateSubscriptionModel(subscription) return service.SubscriptionRepo.CreateSubscription(subscription)
} }
func (service *MembershipService) GetSubscriptionModelNames() ([]string, error) { func (service *MembershipService) GetSubscriptionNames() ([]string, error) {
return service.SubscriptionRepo.GetSubscriptionModelNames() return service.SubscriptionRepo.GetSubscriptionNames()
} }
func (service *MembershipService) GetSubscriptionByName(modelname *string) (*models.SubscriptionModel, error) { func (service *MembershipService) GetSubscriptionByName(modelname *string) (*models.Subscription, error) {
return repositories.GetSubscriptionByName(modelname) return repositories.GetSubscriptionByName(modelname)
} }
func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) { func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.Subscription, error) {
if where == nil { if where == nil {
where = map[string]interface{}{} where = map[string]interface{}{}
} }

View File

@@ -68,24 +68,19 @@ func (s *UserService) Update(user *models.User) (*models.User, error) {
if err := existingUser.FromID(s.DB, &user.ID); err != nil { if err := existingUser.FromID(s.DB, &user.ID); err != nil {
return nil, err return nil, err
} }
user.MembershipID = existingUser.MembershipID
user.Membership.ID = existingUser.Membership.ID user.Membership.ID = existingUser.Membership.ID
if existingUser.Licence != nil { if existingUser.Licence != nil {
user.Licence.ID = existingUser.Licence.ID user.Licence.ID = existingUser.Licence.ID
user.LicenceID = existingUser.LicenceID
} }
user.BankAccount.ID = existingUser.BankAccount.ID user.BankAccount.ID = existingUser.BankAccount.ID
user.BankAccountID = existingUser.BankAccountID
user.SetPassword(user.Password)
// Validate subscription model // Validate subscription model
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name) selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil { if err != nil {
return nil, errors.ErrSubscriptionNotFound return nil, errors.ErrSubscriptionNotFound
} }
user.Membership.SubscriptionModel = *selectedModel user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionModelID = selectedModel.ID user.Membership.SubscriptionID = selectedModel.ID
if err := user.Update(s.DB); err != nil { if err := user.Update(s.DB); err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
@@ -100,16 +95,14 @@ func (s *UserService) Update(user *models.User) (*models.User, error) {
} }
func (s *UserService) Register(user *models.User) (id uint, token string, err error) { func (s *UserService) Register(user *models.User) (id uint, token string, err error) {
user.SetPassword(user.Password)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name) selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil { if err != nil {
return 0, "", errors.ErrSubscriptionNotFound return 0, "", errors.ErrSubscriptionNotFound
} }
user.Membership.SubscriptionModel = *selectedModel user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionModelID = selectedModel.ID user.Membership.SubscriptionID = selectedModel.ID
user.Status = constants.UnverifiedStatus user.Status = constants.UnverifiedStatus
user.PaymentStatus = constants.AwaitingPaymentStatus
user.BankAccount.MandateDateSigned = time.Now() user.BankAccount.MandateDateSigned = time.Now()
v, err := user.SetVerification(constants.VerificationTypes.Email) v, err := user.SetVerification(constants.VerificationTypes.Email)
if err != nil { if err != nil {

View File

@@ -30,10 +30,6 @@ func GenerateRandomString(length int) (string, error) {
return base64.URLEncoding.EncodeToString(bytes), nil return base64.URLEncoding.EncodeToString(bytes), nil
} }
func GenerateVerificationToken() (string, error) {
return GenerateRandomString(32)
}
func DecodeMail(message string) (*Email, error) { func DecodeMail(message string) (*Email, error) {
msg, err := mail.ReadMessage(strings.NewReader(message)) msg, err := mail.ReadMessage(strings.NewReader(message))
if err != nil { if err != nil {

View File

@@ -44,7 +44,7 @@ func HandleUserUpdateError(c *gin.Context, err error) {
case errors.ErrDuplicateEntry: case errors.ErrDuplicateEntry:
RespondWithError(c, err, "User Unique constraint failed", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.Duplicate) RespondWithError(c, err, "User Unique constraint failed", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.Duplicate)
case errors.ErrSubscriptionNotFound: case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Couldn't find subscription", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound) RespondWithError(c, err, "Couldn't find subscription", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
default: default:
RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError) RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
} }
@@ -53,30 +53,30 @@ func HandleUserUpdateError(c *gin.Context, err error) {
func HandleSubscriptionDeleteError(c *gin.Context, err error) { func HandleSubscriptionDeleteError(c *gin.Context, err error) {
switch err { switch err {
case errors.ErrNoData: case errors.ErrNoData:
RespondWithError(c, err, "Missing subscription name during deletion", http.StatusExpectationFailed, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid) RespondWithError(c, err, "Missing subscription name during deletion", http.StatusExpectationFailed, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
case errors.ErrSubscriptionNotFound: case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound) RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
case errors.ErrInvalidSubscriptionData: case errors.ErrInvalidSubscriptionData:
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid) RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
case errors.ErrSubscriptionInUse: case errors.ErrSubscriptionInUse:
RespondWithError(c, err, "Subscription is in use by at least one user", http.StatusExpectationFailed, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InUse) RespondWithError(c, err, "Subscription is in use by at least one user", http.StatusExpectationFailed, errors.Responses.Fields.Subscription, errors.Responses.Keys.InUse)
default: default:
RespondWithError(c, err, "Error during subscription Deletion", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError) RespondWithError(c, err, "Error during subscription Deletion", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
} }
} }
func HandleSubscriptionUpdateError(c *gin.Context, err error) { func HandleSubscriptionUpdateError(c *gin.Context, err error) {
if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "UNIQUE constraint failed") {
RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Duplicate) RespondWithError(c, err, "Subscription already exists", http.StatusConflict, errors.Responses.Fields.Subscription, errors.Responses.Keys.Duplicate)
} else { } else {
switch err { switch err {
case errors.ErrSubscriptionNotFound: case errors.ErrSubscriptionNotFound:
RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.NotFound) RespondWithError(c, err, "Subscription not found", http.StatusNotFound, errors.Responses.Fields.Subscription, errors.Responses.Keys.NotFound)
case errors.ErrInvalidSubscriptionData: case errors.ErrInvalidSubscriptionData:
RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.Invalid) RespondWithError(c, err, "Invalid subscription data", http.StatusBadRequest, errors.Responses.Fields.Subscription, errors.Responses.Keys.Invalid)
default: default:
RespondWithError(c, err, "Couldn't update subscription", http.StatusInternalServerError, errors.Responses.Fields.SubscriptionModel, errors.Responses.Keys.InternalServerError) RespondWithError(c, err, "Couldn't update subscription", http.StatusInternalServerError, errors.Responses.Fields.Subscription, errors.Responses.Keys.InternalServerError)
} }
} }
} }

View File

@@ -10,7 +10,7 @@ type User struct {
Age int Age int
Address *Address Address *Address
Tags []string Tags []string
License License Licence Licence
} }
type Address struct { type Address struct {
@@ -18,7 +18,7 @@ type Address struct {
Country string Country string
} }
type License struct { type Licence struct {
ID string ID string
Categories []string Categories []string
} }
@@ -98,22 +98,22 @@ func TestFilterAllowedStructFields(t *testing.T) {
{ {
name: "Filter slice of structs", name: "Filter slice of structs",
input: &User{ input: &User{
License: License{ Licence: Licence{
ID: "123", ID: "123",
Categories: []string{"A", "B"}, Categories: []string{"A", "B"},
}, },
}, },
existing: &User{ existing: &User{
License: License{ Licence: Licence{
ID: "456", ID: "456",
Categories: []string{"C"}, Categories: []string{"C"},
}, },
}, },
allowedFields: map[string]bool{ allowedFields: map[string]bool{
"License.ID": true, "Licence.ID": true,
}, },
expectedResult: &User{ expectedResult: &User{
License: License{ Licence: Licence{
ID: "123", // Allowed field ID: "123", // Allowed field
Categories: []string{"C"}, // Kept from existing Categories: []string{"C"}, // Kept from existing
}, },

View File

@@ -10,17 +10,17 @@ import (
) )
func validateMembership(db *gorm.DB, user *models.User, sl validator.StructLevel) { func validateMembership(db *gorm.DB, user *models.User, sl validator.StructLevel) {
if user.Membership.SubscriptionModel.RequiredMembershipField != "" { if user.Membership.Subscription.RequiredMembershipField != "" {
switch user.Membership.SubscriptionModel.RequiredMembershipField { switch user.Membership.Subscription.RequiredMembershipField {
case "ParentMembershipID": case "ParentMembershipID":
if err := CheckParentMembershipID(db, user); err != nil { if err := CheckParentMembershipID(db, user); err != nil {
logger.Error.Printf("Error ParentMembershipValidation: %v", err.Error()) logger.Error.Printf("Error ParentMembershipValidation: %v", err.Error())
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.SubscriptionModel.RequiredMembershipField, sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "invalid", "") "RequiredMembershipField", "invalid", "")
} }
default: default:
logger.Error.Printf("Error no matching RequiredMembershipField: %v", errors.ErrInvalidValue.Error()) logger.Error.Printf("Error no matching RequiredMembershipField: %v", errors.ErrInvalidValue.Error())
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.SubscriptionModel.RequiredMembershipField, sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "not_implemented", "") "RequiredMembershipField", "not_implemented", "")
} }
} }

View File

@@ -1,8 +1,6 @@
package validation package validation
import ( import (
"GoMembership/internal/models"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gorm.io/gorm" "gorm.io/gorm"
@@ -15,7 +13,7 @@ func SetupValidators(db *gorm.DB) {
v.RegisterValidation("safe_content", ValidateSafeContent) v.RegisterValidation("safe_content", ValidateSafeContent)
// Register struct-level validations // Register struct-level validations
v.RegisterStructValidation(ValidateUserFactory(db), models.User{}) // v.RegisterStructValidation(ValidateUserFactory(db), models.User{})
v.RegisterStructValidation(ValidateSubscription, models.SubscriptionModel{}) // v.RegisterStructValidation(ValidateSubscription, models.Subscription{})
} }
} }

View File

@@ -9,13 +9,12 @@ import (
// ValidateNewSubscription validates a new subscription model being created // ValidateNewSubscription validates a new subscription model being created
func ValidateSubscription(sl validator.StructLevel) { func ValidateSubscription(sl validator.StructLevel) {
subscription := sl.Current().Interface().(models.SubscriptionModel) subscription := sl.Current().Interface().(models.Subscription)
if subscription.Name == "" { if subscription.Name == "" {
sl.ReportError(subscription.Name, "Name", "name", "required", "") sl.ReportError(subscription.Name, "Name", "name", "required", "")
} }
if sl.Parent().Type().Name() == "" {
if sl.Parent().Type().Name() == "MembershipData" {
// This is modifying a subscription directly // This is modifying a subscription directly
if subscription.Details == "" { if subscription.Details == "" {
sl.ReportError(subscription.Details, "Details", "details", "required", "") sl.ReportError(subscription.Details, "Details", "details", "required", "")

View File

@@ -30,16 +30,16 @@ func ValidateUserFactory(db *gorm.DB) validator.StructLevelFunc {
func validateUser(db *gorm.DB, sl validator.StructLevel) { func validateUser(db *gorm.DB, sl validator.StructLevel) {
user := sl.Current().Interface().(models.User) user := sl.Current().Interface().(models.User)
// validate subscriptionModel // validate subscription
if user.Membership.SubscriptionModel.Name == "" { if user.Membership.Subscription.Name == "" {
sl.ReportError(user.Membership.SubscriptionModel.Name, "subscription.name", "name", "required", "") sl.ReportError(user.Membership.Subscription.Name, "subscription.name", "name", "required", "")
} else { } else {
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name) selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil { if err != nil {
logger.Error.Printf("Error finding subscription model for user %v: %v", user.Email, err) logger.Error.Printf("Error finding subscription model for user %v: %v", user.Email, err)
sl.ReportError(user.Membership.SubscriptionModel.Name, "subscription.name", "name", "invalid", "") sl.ReportError(user.Membership.Subscription.Name, "subscription.name", "name", "invalid", "")
} else { } else {
user.Membership.SubscriptionModel = *selectedModel user.Membership.Subscription = *selectedModel
} }
} }
if user.IsSupporter() { if user.IsSupporter() {

View File

@@ -2,36 +2,6 @@ package errors
import "errors" import "errors"
type ValidationKeys struct {
Invalid string
InternalServerError string
InvalidJson string
Unauthorized string
InvalidSubscriptionModel string
UserNotFoundWrongPassword string
JwtGenerationFailed string
Duplicate string
InvalidUserID string
PasswordAlreadyChanged string
UserDisabled string
NoAuthToken string
NotFound string
InUse string
UndeliveredVerificationMail string
UserAlreadyVerified string
}
type ValidationFields struct {
General string
ParentMemberShipID string
SubscriptionModel string
Login string
Email string
User string
Licences string
Verification string
}
var ( var (
ErrNotFound = errors.New("not found") ErrNotFound = errors.New("not found")
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
@@ -56,6 +26,37 @@ var (
ErrInvalidSubscriptionData = errors.New("Provided subscription data is invalid. Immutable fields where changed.") ErrInvalidSubscriptionData = errors.New("Provided subscription data is invalid. Immutable fields where changed.")
) )
type ValidationKeys struct {
Invalid string
InternalServerError string
InvalidJSON string
InvalidUserID string
InvalidSubscription string
Unauthorized string
UserNotFoundWrongPassword string
JwtGenerationFailed string
Duplicate string
UserDisabled string
PasswordAlreadyChanged string
NoAuthToken string
NotFound string
InUse string
UndeliveredVerificationMail string
UserAlreadyVerified string
}
type ValidationFields struct {
General string
ParentMembershipID string
Subscription string
Login string
Email string
User string
Licences string
Verification string
Car string
}
var Responses = struct { var Responses = struct {
Keys ValidationKeys Keys ValidationKeys
Fields ValidationFields Fields ValidationFields
@@ -63,7 +64,9 @@ var Responses = struct {
Keys: ValidationKeys{ Keys: ValidationKeys{
Invalid: "server.validation.invalid", Invalid: "server.validation.invalid",
InternalServerError: "server.error.internal_server_error", InternalServerError: "server.error.internal_server_error",
InvalidJson: "server.error.invalid_json", InvalidJSON: "server.error.invalid_json",
InvalidUserID: "server.validation.invalid_user_id",
InvalidSubscription: "server.validation.invalid_subscription",
Unauthorized: "server.error.unauthorized", Unauthorized: "server.error.unauthorized",
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",
@@ -78,13 +81,14 @@ var Responses = struct {
}, },
Fields: ValidationFields{ Fields: ValidationFields{
General: "server.general", General: "server.general",
ParentMemberShipID: "parent_membership_id", ParentMembershipID: "parent_membership_id",
SubscriptionModel: "subscription_model", Subscription: "subscription",
Login: "user.login", Login: "user.login",
Email: "user.email", Email: "user.email",
User: "user.user", User: "user.user",
Licences: "licence", Licences: "licence",
Verification: "verification", Verification: "verification",
Car: "car",
}, },
} }

View File

@@ -36,7 +36,7 @@
target="_blank" target="_blank"
><img ><img
alt="Dörpsmobil Hasloh" alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;

View File

@@ -40,7 +40,7 @@
target="_blank" target="_blank"
><img ><img
alt="{{.WebsiteTitle}}" alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;

View File

@@ -36,7 +36,7 @@
target="_blank" target="_blank"
><img ><img
alt="Dörpsmobil Hasloh" alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;

View File

@@ -36,7 +36,7 @@
target="_blank" target="_blank"
><img ><img
alt="{{.WebsiteTitle}}" alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;

View File

@@ -36,7 +36,7 @@
target="_blank" target="_blank"
><img ><img
alt="Dörpsmobil Hasloh" alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/images/CarsharingSH-Hasloh-LOGO.jpeg" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;
@@ -70,7 +70,7 @@
</div> </div>
<div style="text-align: center; padding: 16px 24px 16px 24px"> <div style="text-align: center; padding: 16px 24px 16px 24px">
<a <a
href="{{.BASEURL}}/api/users/verify/{{.ID}}?token={{.Token}}" href="{{.BASEURL}}/api/users/verify/{{.UserID}}?token={{.Token}}"
style=" style="
color: #ffffff; color: #ffffff;
font-size: 26px; font-size: 26px;

View File

@@ -8,7 +8,7 @@ noch Ihre Emailadresse indem Sie hier klicken:
E-Mail Adresse bestätigen E-Mail Adresse bestätigen
{{.BASEURL}}/api/users/verify/{{.ID}}?token={{.Token}} {{.BASEURL}}/api/users/verify/{{.UserID}}?token={{.Token}}
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die Ihnen alle weiteren Informationen zu. Wir freuen uns auf die

View File

@@ -36,7 +36,7 @@
target="_blank" target="_blank"
><img ><img
alt="{{.WebsiteTitle}}" alt="{{.WebsiteTitle}}"
src="{{.BASEURL}}{{.Logo}}" src="{{.Logo}}"
style=" style="
outline: none; outline: none;
border: none; border: none;
@@ -80,22 +80,61 @@
</ul> </ul>
</li> </li>
<li> <li>
<strong>Mitgliedsbeitrag</strong>: Solange wir noch kein <strong>Führerscheinverifikation</strong>:
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch Dein Führerschein wird bei der Anmeldung bei unserem Dienstleister Moqo verifiziert.
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
</li> </li>
<li> <li>
<strong>Führerscheinverifikation</strong>: Weitere Bitte melde Dich nun noch auf der Webseite von Moqo für Deinen gewählten
Informationen zur Verifikation deines Führerscheins folgen Tarif an:
in Kürze. Du musst nichts weiter tun, wir werden uns bei dir
melden, sobald es notwendig ist.
</li> </li>
<li> <li>
<strong>Moqo App</strong>: Wir werden die Moqo App nutzen, </li>
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen <strong>Moqo Anmeldung(Bitte UNBEDINGT AUSFÜLLEN, sonst ist kein Ausleihen möglich)</strong>:
ersten Eindruck von dem Buchungsvorgang haben möchtest, <div style="text-align: center; padding: 0px 24px 16px 24px">
schaue Dir gerne dieses kurze Video an: <a
href="https://portal.moqo.de/js_sign_up/18013114#/subscription-selection"
style="
color: #ffffff;
font-size: 16px;
font-weight: bold;
background-color: #f45050;
border-radius: 4px;
display: inline-block;
padding: 12px 20px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 20px;
mso-font-width: -100%;
mso-text-raise: 30;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>MOQO Anmeldung</span
><span
><!--[if mso
]><i
style="letter-spacing: 20px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<li>
<strong>Moqo App</strong>:
Wir nutzen die Moqo App, damit Du das Fahrzeug mit Deinem Handy
entsperren und Deine Buchungen verwalten kannst.
Wenn Du schon mal einen ersten Eindruck von dem Buchungsvorgang
haben möchtest, schaue Dir gerne dieses kurze Video an:
</li> </li>
</ul> </ul>
</div> </div>
@@ -272,8 +311,8 @@
</ul> </ul>
</div> </div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px"> <div style="font-weight: normal; padding: 16px 24px 16px 24px">
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns Wir danken Dir herzlich für Dein Vertrauen und freuen uns drauf, Dich bald
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen. Dörpsmobil fahrend in Hasloh zu sehen!
</div> </div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px"> <div style="font-weight: normal; padding: 16px 24px 16px 24px">
<p>Mit freundlichen Grüßen,</p> <p>Mit freundlichen Grüßen,</p>

View File

@@ -12,23 +12,24 @@ Hier einige wichtige Informationen für dich:
Preis/Monat: {{.MembershipFee}} Preis/Monat: {{.MembershipFee}}
Preis/h: {{.RentalFee}} Preis/h: {{.RentalFee}}
Mitgliedsbeitrag: Solange wir noch kein Führerscheinverifikation: Dein Führerschein wird bei der Anmeldung
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch bei unserem Dienstleister Moqo verifiziert.
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
Führerscheinverifikation: Weitere Informationen zur Verifikation Bitte melde Dich nun noch auf der Webseite von Moqo für Deinen gewählten
deines Führerscheins folgen in Kürze. Du musst nichts weiter tun, Tarif an:
wir werden uns bei dir melden, sobald es notwendig ist.
Moqo App: Moqo Anmeldung(Bitte UNBEDINGT AUSFÜLLEN, sonst ist kein Ausleihen möglich):
Wir werden die Moqo App nutzen, https://portal.moqo.de/js_sign_up/18013114#/subscription-selection
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest, Das Ausleihen:
schaue Dir gerne dieses kurze Video an: Wir nutzen die Moqo App, damit Du das Fahrzeug mit Deinem Handy
entsperren und Deine Buchungen verwalten kannst.
Wenn Du schon mal einen ersten Eindruck von dem Buchungsvorgang
haben möchtest, schaue Dir gerne dieses kurze Video an:
Moqo App Nutzung Moqo App Nutzung
https://www.youtube.com/shorts/ZMKUX0uyOps https://www.youtube.com/shorts/ZMKUX0uyOps
Dörpsmobil: Dörpsmobil:
Wir sind nicht alleine sondern Mitglied in einem Schleswig-Holstein Wir sind nicht alleine sondern Mitglied in einem Schleswig-Holstein
weiten Netz an gemeinnützigen Carsharing Anbietern. Für mehr weiten Netz an gemeinnützigen Carsharing Anbietern. Für mehr
@@ -49,8 +50,8 @@ du dich jederzeit an unsere Vorsitzende wenden:
E-Mail: vorstand@carsharing-hasloh.de E-Mail: vorstand@carsharing-hasloh.de
Telefon: +49 176 5013 4256 Telefon: +49 176 5013 4256
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns Wir danken Dir herzlich für Dein Vertrauen und freuen uns drauf, Dich bald
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen. Dörpsmobil fahrend in Hasloh zu sehen!
Mit freundlichen Grüßen, Mit freundlichen Grüßen,
Dein Carsharing Hasloh Team Dein Carsharing Hasloh Team