This commit is contained in:
Alex
2025-04-10 15:40:22 +02:00
parent 87f08dd3be
commit 18f5dadb06
48 changed files with 1650 additions and 981 deletions

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

@@ -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,9 @@ 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;
licence: Licence;
notes: string | ''; notes: string | '';
} }
@@ -80,9 +77,9 @@ interface Car {
end_date: string | ''; end_date: string | '';
color: string | ''; color: string | '';
licence_plate: string | ''; licence_plate: string | '';
location: Location; location: Location | null;
damages: Damage[] | []; damages: Damage[] | null;
insurances: Insurance[] | []; insurances: Insurance[] | null;
notes: string | ''; notes: string | '';
} }
@@ -93,8 +90,11 @@ interface Location {
interface Damage { interface Damage {
id: number | -1; id: number | -1;
opponent: User; name: string | '';
opponent: User | null;
driver_id: number | -1;
insurance: Insurance | null; insurance: Insurance | null;
date: string | '';
notes: string | ''; notes: string | '';
} }

View File

@@ -5,8 +5,10 @@
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 { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { defaultCar } from '$lib/utils/defaults'; import { defaultDamage, defaultInsurance, defaultOpponent } from '$lib/utils/defaults';
import { PERMISSIONS } from '$lib/utils/constants'; import { PERMISSIONS } from '$lib/utils/constants';
import Modal from './Modal.svelte';
import UserEditForm from './UserEditForm.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -16,19 +18,54 @@
/** @type {App.Locals['user'] } */ /** @type {App.Locals['user'] } */
export let editor; export let editor;
/** @type {App.Types['car'] | null} */ /** @type {App.Locals['users'] } */
export let users;
/** @type {App.Types['car']} */
export let car; export let car;
console.log('Opening car modal with:', car); $: console.log(
$: car = car || { ...defaultCar() }; '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; $: isLoading = car === undefined || editor === undefined;
let isUpdating = false; let isUpdating = false;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update); let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
/** @type {number | null} */
let editingUserIndex = null;
const TABS = ['car.car', 'insurance', 'car.damages']; const TABS = ['car.car', 'insurance', 'car.damages'];
let activeTab = TABS[0]; let activeTab = TABS[0];
/** @type {import('../../routes/auth/about/[id]/$types').SubmitFunction} */ /** @type {import('@sveltejs/kit').SubmitFunction} */
const handleUpdate = async () => { const handleUpdate = async () => {
isUpdating = true; isUpdating = true;
return async ({ result }) => { return async ({ result }) => {
@@ -47,7 +84,7 @@
<SmallLoader width={30} message={$t('loading.car_data')} /> <SmallLoader width={30} message={$t('loading.car_data')} />
{:else if editor && car} {:else if editor && car}
<form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}> <form class="content" action="?/updateCar" method="POST" use:enhance={handleUpdate}>
<input name="susbscription[id]" type="hidden" bind:value={car.id} /> <input name="car[id]" type="hidden" bind:value={car.id} />
<h1 class="step-title" style="text-align: center;"> <h1 class="step-title" style="text-align: center;">
{car.id ? $t('car.edit') : $t('car.create')} {car.id ? $t('car.edit') : $t('car.create')}
</h1> </h1>
@@ -155,9 +192,17 @@
/> />
</div> </div>
<div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}"> <div class="tab-content" style="display: {activeTab === 'insurance' ? 'block' : 'none'}">
{#each car.insurances as insurance} <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 <InputField
name="car[insurance][company]" name="car[insurances][{index}][company]"
label={$t('company')} label={$t('company')}
bind:value={insurance.company} bind:value={insurance.company}
placeholder={$t('placeholder.company')} placeholder={$t('placeholder.company')}
@@ -165,29 +210,29 @@
readonly={readonlyUser} readonly={readonlyUser}
/> />
<InputField <InputField
name="car[insurance][reference]" name="car[insurances][{index}][reference]"
label={$t('insurance.reference')} label={$t('insurance_reference')}
bind:value={insurance.reference} bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')} placeholder={$t('placeholder.insurance_reference')}
required={true} required={true}
readonly={readonlyUser} readonly={readonlyUser}
/> />
<InputField <InputField
name="car[insurance][start_date]" name="car[insurances][{index}][start_date]"
type="date" type="date"
label={$t('start')} label={$t('start')}
bind:value={insurance.start_date} bind:value={insurance.start_date}
readonly={readonlyUser} readonly={readonlyUser}
/> />
<InputField <InputField
name="car[insurance][end_date]" name="car[insurances][{index}][end_date]"
type="date" type="date"
label={$t('end')} label={$t('end')}
bind:value={insurance.end_date} bind:value={insurance.end_date}
readonly={readonlyUser} readonly={readonlyUser}
/> />
<InputField <InputField
name="car[insurance][notes]" name="car[insurances][{index}][notes]"
type="textarea" type="textarea"
label={$t('notes')} label={$t('notes')}
bind:value={insurance.notes} bind:value={insurance.notes}
@@ -196,8 +241,309 @@
})} })}
rows={10} 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} {/each}
</div> </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"> <div class="button-container">
{#if isUpdating} {#if isUpdating}
<SmallLoader width={30} message={$t('loading.updating')} /> <SmallLoader width={30} message={$t('loading.updating')} />
@@ -211,7 +557,81 @@
</form> </form>
{/if} {/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> <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 { .tab-content {
padding: 1rem; padding: 1rem;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
@@ -219,6 +639,16 @@
border: 1px solid var(--surface1); border: 1px solid var(--surface1);
margin-top: 1rem; 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 { .button-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

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

@@ -44,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)}
@@ -60,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}
@@ -77,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}
@@ -85,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}
@@ -94,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}
@@ -102,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,7 +6,7 @@
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;
@@ -20,10 +20,15 @@
export let submit_form = true; export let submit_form = true;
// Ensure licence is initialized before passing to child // Ensure licence is initialized before passing to child
$: if (user && !user.licence) { // $: if (user && !user.licence) {
user.licence = defaultLicence(); // 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;
@@ -31,7 +36,9 @@
// $: isNewUser = user === null; // $: isNewUser = user === null;
$: isLoading = user === undefined; $: isLoading = user === undefined;
$: if (user != null) {
console.log(user);
}
/** @type {App.Locals['licence_categories'] | null} */ /** @type {App.Locals['licence_categories'] | null} */
export let licence_categories; export let licence_categories;
@@ -43,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"
@@ -62,14 +70,10 @@
]; ];
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const TABS = [ /** @type { (keyof user)[] } */
'profile', const TABS = ['membership', 'licence', 'bank_account'];
'bankaccount',
...(hasPrivilige(user, PERMISSIONS.Member) ? 'membership' : []),
...(user.licence ? 'licence' : [])
];
let activeTab = TABS[0]; let activeTab = 'profile';
let isUpdating = false, let isUpdating = false,
password = '', password = '',
@@ -77,13 +81,13 @@
/** @type {Object.<string, App.Locals['licence_categories']>} */ /** @type {Object.<string, App.Locals['licence_categories']>} */
$: groupedCategories = licence_categories ? groupCategories(licence_categories) : {}; $: groupedCategories = licence_categories ? groupCategories(licence_categories) : {};
$: subscriptionModelOptions = subscriptions $: subscriptionOptions = subscriptions
? subscriptions.map((sub) => ({ ? subscriptions.map((sub) => ({
value: sub?.name ?? '', value: sub?.name ?? '',
label: sub?.name ?? '' label: sub?.name ?? ''
})) }))
: []; : [];
$: selectedSubscriptionModel = subscriptions $: selectedSubscription = subscriptions
? subscriptions.find((sub) => sub?.name === user.membership?.subscription.name) || null ? subscriptions.find((sub) => sub?.name === user.membership?.subscription.name) || null
: null; : null;
/** /**
@@ -172,18 +176,29 @@
{/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}
{#if user[tab] != null}
<button <button
type="button" type="button"
class="button-dark" class="button-dark"
class:active={activeTab === tab} class:active={activeTab === tab}
on:click={() => (activeTab = tab)} on:click={() => (activeTab = tab)}
> >
{$t(tab)} {$t('user.' + tab)}
</button> </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'}">
{#if hasPrivilige(user, PERMISSIONS.Member)}
<InputField <InputField
name="user[status]" name="user[status]"
type="select" type="select"
@@ -192,6 +207,7 @@
options={userStatusOptions} options={userStatusOptions}
readonly={readonlyUser} readonly={readonlyUser}
/> />
{/if}
{#if hasPrivilige(editor, PERMISSIONS.Super)} {#if hasPrivilige(editor, PERMISSIONS.Super)}
<InputField <InputField
name="user[role_id]" name="user[role_id]"
@@ -367,6 +383,7 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if user.membership}
<div <div
class="tab-content" class="tab-content"
style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}" style="display: {activeTab === 'membership' && subscriptions ? 'block' : 'none'}"
@@ -384,7 +401,7 @@
type="select" type="select"
label={$t('subscriptions.subscription')} label={$t('subscriptions.subscription')}
bind:value={user.membership.subscription.name} bind:value={user.membership.subscription.name}
options={subscriptionModelOptions} options={subscriptionOptions}
readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)} readonly={readonlyUser || !hasPrivilige(user, PERMISSIONS.Member)}
/> />
<div class="subscription-info"> <div class="subscription-info">
@@ -392,22 +409,22 @@
<div class="subscription-column"> <div class="subscription-column">
<p> <p>
<strong>{$t('subscriptions.monthly_fee')}:</strong> <strong>{$t('subscriptions.monthly_fee')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'} {selectedSubscription?.monthly_fee || '-'}
</p> </p>
<p> <p>
<strong>{$t('subscriptions.hourly_rate')}:</strong> <strong>{$t('subscriptions.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'} {selectedSubscription?.hourly_rate || '-'}
</p> </p>
{#if selectedSubscriptionModel?.included_hours_per_year} {#if selectedSubscription?.included_hours_per_year}
<p> <p>
<strong>{$t('subscriptions.included_hours_per_year')}:</strong> <strong>{$t('subscriptions.included_hours_per_year')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year} {selectedSubscription?.included_hours_per_year}
</p> </p>
{/if} {/if}
{#if selectedSubscriptionModel?.included_hours_per_month} {#if selectedSubscription?.included_hours_per_month}
<p> <p>
<strong>{$t('subscriptions.included_hours_per_month')}:</strong> <strong>{$t('subscriptions.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month} {selectedSubscription?.included_hours_per_month}
</p> </p>
{/if} {/if}
</div> </div>
@@ -415,12 +432,12 @@
<div class="subscription-column"> <div class="subscription-column">
<p> <p>
<strong>{$t('details')}:</strong> <strong>{$t('details')}:</strong>
{selectedSubscriptionModel?.details || '-'} {selectedSubscription?.details || '-'}
</p> </p>
{#if selectedSubscriptionModel?.conditions} {#if selectedSubscription?.conditions}
<p> <p>
<strong>{$t('subscriptions.conditions')}:</strong> <strong>{$t('subscriptions.conditions')}:</strong>
{selectedSubscriptionModel?.conditions} {selectedSubscription?.conditions}
</p> </p>
{/if} {/if}
</div> </div>
@@ -452,7 +469,9 @@
/> />
{/if} {/if}
</div> </div>
<div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}"> {/if}
{#if user.bank_account}
<div class="tab-content" style="display: {activeTab === 'bank_account' ? 'block' : 'none'}">
<InputField <InputField
name="user[bank_account][account_holder_name]" name="user[bank_account][account_holder_name]"
label={$t('bank_account_holder')} label={$t('bank_account_holder')}
@@ -494,6 +513,7 @@
readonly={true} readonly={true}
/> />
</div> </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,7 +7,7 @@ export default {
5: 'Passiv' 5: 'Passiv'
}, },
userRole: { userRole: {
'-5': 'Unfallgegner', '-1': 'Unfallgegner',
0: 'Sponsor', 0: 'Sponsor',
1: 'Mitglied', 1: 'Mitglied',
2: 'Betrachter', 2: 'Betrachter',
@@ -131,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',
@@ -138,9 +139,12 @@ 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'
}, },
subscriptions: { subscriptions: {
name: 'Modellname', name: 'Modellname',
@@ -186,8 +190,11 @@ export default {
actions: 'Aktionen', actions: 'Aktionen',
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', price: 'Preis',
color: 'Farbe', color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren', grant_backend_access: 'Backend Zugriff gewähren',
@@ -221,8 +228,6 @@ export default {
login: 'Anmeldung', login: 'Anmeldung',
profile: 'Profil', profile: 'Profil',
cars: 'Fahrzeuge', cars: 'Fahrzeuge',
membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten',
status: 'Status', status: 'Status',
start: 'Beginn', start: 'Beginn',
end: 'Ende', end: 'Ende',

View File

@@ -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,6 +10,3 @@ export const PERMISSIONS = {
Delete: 4, Delete: 4,
Super: 8 Super: 8
}; };
export const SUPPORTER_SUBSCRIPTION_NAME = 'Keins';
export const OPPONENT_SUBSCRIPTION_NAME = 'Keins';

View File

@@ -1,7 +1,5 @@
// src/lib/utils/defaults.js // src/lib/utils/defaults.js
import { OPPONENT_SUBSCRIPTION_NAME, SUPPORTER_SUBSCRIPTION_NAME } from './constants';
/** /**
* @returns {App.Types['subscription']} * @returns {App.Types['subscription']}
*/ */
@@ -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(),
@@ -97,7 +93,7 @@ export function defaultSupporter() {
supporter.status = 5; supporter.status = 5;
supporter.role_id = 0; supporter.role_id = 0;
supporter.licence = null; supporter.licence = null;
supporter.membership.subscription.name = SUPPORTER_SUBSCRIPTION_NAME; supporter.membership = null;
return supporter; return supporter;
} }
@@ -109,7 +105,7 @@ export function defaultOpponent() {
opponent.status = 5; opponent.status = 5;
opponent.role_id = -1; opponent.role_id = -1;
opponent.licence = null; opponent.licence = null;
opponent.membership.subscription.name = OPPONENT_SUBSCRIPTION_NAME; opponent.membership = null;
return opponent; return opponent;
} }
/** /**
@@ -128,8 +124,11 @@ export function defaultLocation() {
export function defaultDamage() { export function defaultDamage() {
return { return {
id: 0, id: 0,
opponent: defaultUser(), name: '',
insurance: null, opponent: defaultOpponent(),
driver_id: -1,
insurance: defaultInsurance(),
date: '',
notes: '' notes: ''
}; };
} }
@@ -155,7 +154,7 @@ export function defaultCar() {
return { return {
id: 0, id: 0,
name: '', name: '',
status: '', status: 0,
brand: '', brand: '',
model: '', model: '',
price: 0, price: 0,

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

View File

@@ -1,3 +1,4 @@
import { defaultBankAccount, defaultMembership } from './defaults';
import { toRFC3339 } from './helpers'; import { toRFC3339 } from './helpers';
/** /**
@@ -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];
@@ -49,18 +48,31 @@ export function formDataToObject(formData) {
} catch { } catch {
current[lastKey].push(value); current[lastKey].push(value);
} }
} else {
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 { } else {
current[lastKey] = value; current[lastKey] = value;
} }
} }
}
return { object: object, confirm_password: confirm_password }; return { object: object, confirm_password: confirm_password };
} }
/** /**
* Processes the raw form data into the expected user data structure * Processes the raw form data into the expected 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 processMembershipFormData(membership) { export function processMembershipFormData(membership) {
return { return {
@@ -73,39 +85,71 @@ export function processMembershipFormData(membership) {
}; };
} }
licence: { /**
id: Number(rawData.object.licence?.id) || 0, * Processes the raw form data into the expected licence data structure
status: Number(rawData.object.licence?.status), * @param { App.Types['licence'] } licence - The raw form data object
number: String(rawData.object.licence?.number || ''), * @returns {App.Types['licence']} Processed licence data
issued_date: toRFC3339(String(rawData.object.licence?.issued_date || '')), */
expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date || '')), export function processLicenceFormData(licence) {
country: String(rawData.object.licence?.country || ''), return {
categories: rawData.object.licence?.categories || [] 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 || []
};
}
bank_account: { /**
id: Number(rawData.object.bank_account?.id) || 0, * Processes the raw form data into the expected bank_account data structure
account_holder_name: String(rawData.object.bank_account?.account_holder_name || ''), * @param { App.Types['bankAccount'] } bank_account - The raw form data object
bank: String(rawData.object.bank_account?.bank || ''), * @returns {App.Types['bankAccount']} Processed bank_account data
iban: String(rawData.object.bank_account?.iban || ''), */
bic: String(rawData.object.bank_account?.bic || ''), export function processBankAccountFormData(bank_account) {
mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''), {
mandate_date_signed: toRFC3339( return {
String(rawData.object.bank_account?.mandate_date_signed || '') 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 = {
id: Number(user.id) || 0,
status: Number(user.status),
role_id: Number(user.role_id),
first_name: String(user.first_name),
last_name: String(user.last_name),
password: String(user.password) || '',
email: String(user.email),
phone: String(user.phone || ''),
company: String(user.company || ''),
dateofbirth: toRFC3339(String(user.dateofbirth || '')),
address: String(user.address || ''),
zip_code: String(user.zip_code || ''),
city: String(user.city || ''),
notes: String(user.notes || ''),
membership: processMembershipFormData(user.membership ? user.membership : defaultMembership()),
licence: user.licence ? processLicenceFormData(user.licence) : null,
bank_account: processBankAccountFormData(
user.bank_account ? user.bank_account : defaultBankAccount()
) )
}
}
}; };
// 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
); );
@@ -136,39 +180,85 @@ export function processSubscriptionFormData(subscription) {
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 * Processes the raw form data into the expected car data structure
* @param {{ object: Partial<App.Types['car']>, confirm_password: string }} rawData - The raw form data object * @param {Partial<App.Types['car']>} car - The raw form data object
* @returns {{ car: Partial<App.Types['car']> }} Processed user data * @returns {App.Types['car']} Processed user data
*/ */
export function processCarFormData(rawData) { export function processCarFormData(car) {
/** @type {{ car: Partial<App.Types['car']> }} */ console.dir(car);
/** @type {App.Types['car']} */
let processedData = { let processedData = {
car: { id: Number(car.id) || 0,
id: Number(rawData.object.id) || 0, name: String(car.name) || '',
name: String(rawData.object.name) || '', status: Number(car.status) || 0,
status: Number(rawData.object.status) || 0, brand: String(car.brand) || '',
brand: String(rawData.object.brand) || '', model: String(car.model) || '',
model: String(rawData.object.model) || '', price: Number(car.price) || 0,
price: Number(rawData.object.price) || 0, rate: Number(car.rate) || 0,
rate: Number(rawData.object.rate) || 0, licence_plate: String(car.licence_plate),
licence_plate: String(rawData.object.licence_plate) || '', start_date: 'start_date' in car ? toRFC3339(String(car.start_date) || '') : '',
start_date: toRFC3339(String(rawData.object.start_date)) || '', end_date: 'end_date' in car ? toRFC3339(String(car.end_date) || '') : '',
end_date: toRFC3339(String(rawData.object.end_date)) || '', color: String(car.color) || '',
color: String(rawData.object.color) || '', notes: String(car.notes) || '',
notes: String(rawData.object.notes) || '', location:
location: { 'location' in car
latitude: Number(rawData.object.location?.latitude) || 0, ? {
longitude: Number(rawData.object.location?.longitude) || 0 latitude: Number(car.location?.latitude) || 0,
}, longitude: Number(car.location?.longitude) || 0
damages: rawData.object.damages || [],
insurances: rawData.object.insurances || []
} }
: {
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) => const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined value !== null && value !== '' ? value : undefined
); );
console.dir(clean); console.dir(clean);
return 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

@@ -38,15 +38,23 @@ export const actions = {
let formData = await request.formData(); let formData = await request.formData();
const rawFormData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */ /** @type {{object: App.Locals['user'], confirm_password: string}} */
const rawData = { const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object), object: /** @type {App.Locals['user']} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password confirm_password: rawFormData.confirm_password
}; };
const processedData = processUserFormData(rawData); // 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`;
@@ -58,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);
@@ -128,18 +136,13 @@ export const actions = {
*/ */
updateCar: async ({ request, fetch, cookies }) => { updateCar: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
console.dir(formData);
const rawCar = /**@type {Partial<App.Types['car']>} */ (formDataToObject(formData).object);
const car = processCarFormData(rawCar);
const rawFormData = formDataToObject(formData); const isCreating = !car.id || car.id === 0;
/** @type {{object: Partial<App.Types['car']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['car']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processCarFormData(rawData);
const isCreating = !processedData.car.id || processedData.car.id === 0;
console.log('Is creating: ', isCreating); console.log('Is creating: ', isCreating);
console.log('sending: ', JSON.stringify(processedData.car)); console.log('sending: ', JSON.stringify(car.damages));
const apiURL = `${BASE_API_URI}/auth/cars`; const apiURL = `${BASE_API_URI}/auth/cars`;
@@ -151,7 +154,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.car) body: JSON.stringify(car)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -164,6 +167,8 @@ export const actions = {
const response = await res.json(); const response = await res.json();
console.log('Server success response:', response); 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`); throw redirect(303, `${base}/auth/admin/users`);
}, },
@@ -179,12 +184,8 @@ export const actions = {
let formData = await request.formData(); let formData = await request.formData();
const rawFormData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */ /** @type {Partial<App.Locals['user']>} */
const rawData = { const rawUser = /** @type {Partial<App.Locals['user']>} */ (rawFormData.object);
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData);
const apiURL = `${BASE_API_URI}/auth/users`; const apiURL = `${BASE_API_URI}/auth/users`;
@@ -196,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);
@@ -217,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`;
@@ -236,7 +238,45 @@ 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.subscription) body: JSON.stringify({ id: Number(subscription.id), name: subscription.name })
};
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
*/
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); const res = await fetch(apiURL, requestOptions);
@@ -263,12 +303,9 @@ export const actions = {
let formData = await request.formData(); let formData = await request.formData();
const rawFormData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['subscription']>, confirm_password: string}} */ /** @type {App.Locals['user']} */
const rawData = { const rawUser = /** @type {App.Locals['user']} */ (rawFormData.object);
object: /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object), const processedData = processUserFormData(rawUser);
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData);
console.dir(processedData); console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`; const apiURL = `${BASE_API_URI}/auth/users/activate`;

View File

@@ -145,7 +145,7 @@
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>
@@ -391,6 +391,7 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
{#if hasPrivilige(user, PERMISSIONS.Update)}
<button <button
class="btn primary" class="btn primary"
on:click={() => { on:click={() => {
@@ -400,6 +401,8 @@
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
{/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form <form
method="POST" method="POST"
action="?/userDelete" action="?/userDelete"
@@ -432,6 +435,7 @@
{$t('delete')} {$t('delete')}
</button> </button>
</form> </form>
{/if}
</div> </div>
</div> </div>
</details> </details>
@@ -439,7 +443,7 @@
</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 <button
class="btn primary" class="btn primary"
@@ -468,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 + '€'
@@ -476,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 + '€'
@@ -484,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>
@@ -496,13 +500,13 @@
<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={() => { on:click={() => {
@@ -527,6 +531,20 @@
?.scrollTo({ top: 0, behavior: 'smooth' }); ?.scrollTo({ top: 0, behavior: 'smooth' });
await applyAction(result); 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
}
}} }}
> >
<input type="hidden" name="subscription[id]" value={subscription.id} /> <input type="hidden" name="subscription[id]" value={subscription.id} />
@@ -538,7 +556,6 @@
</form> </form>
{/if} {/if}
</div> </div>
{/if}
</div> </div>
</details> </details>
{/each} {/each}
@@ -597,8 +614,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{#if hasPrivilige(user, PERMISSIONS.Update)}
<div class="button-group"> <div class="button-group">
{#if hasPrivilige(user, PERMISSIONS.Update)}
<button <button
class="btn primary" class="btn primary"
on:click={() => { on:click={() => {
@@ -608,6 +625,8 @@
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
{/if}
{#if hasPrivilige(user, PERMISSIONS.Delete)}
<form <form
method="POST" method="POST"
action="?/carDelete" action="?/carDelete"
@@ -637,15 +656,15 @@
} }
}} }}
> >
<input type="hidden" name="subscription[id]" value={car.id} /> <input type="hidden" name="car[id]" value={car.id} />
<button class="btn danger" type="submit"> <button class="btn danger" type="submit">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
{$t('delete')} {$t('delete')}
</button> </button>
</form> </form>
</div>
{/if} {/if}
</div> </div>
</div>
</details> </details>
{/each} {/each}
</div> </div>
@@ -705,7 +724,7 @@
</Modal> </Modal>
{:else if selected && 'brand' in selected} {:else if selected && 'brand' in selected}
<Modal on:close={close}> <Modal on:close={close}>
<CarEditForm {form} editor={user} car={selected} on:cancel={close} on:close={close} /> <CarEditForm {form} editor={user} {users} car={selected} on:cancel={close} on:close={close} />
</Modal> </Modal>
{/if} {/if}

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

@@ -15,7 +15,7 @@ const (
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 {

View File

@@ -89,9 +89,7 @@ func (cr *CarController) GetAll(c *gin.Context) {
func (cr *CarController) Delete(c *gin.Context) { func (cr *CarController) Delete(c *gin.Context) {
type input struct { type input struct {
Car struct {
ID uint `json:"id" binding:"required,numeric"` ID uint `json:"id" binding:"required,numeric"`
} `json:"car"`
} }
var deleteData input var deleteData input
requestUser, err := cr.UserService.FromContext(c) requestUser, err := cr.UserService.FromContext(c)
@@ -109,7 +107,7 @@ func (cr *CarController) Delete(c *gin.Context) {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
} }
err = cr.S.Delete(&deleteData.Car.ID) err = cr.S.Delete(&deleteData.ID)
if err != nil { if err != nil {
utils.RespondWithError(c, err, "Error deleting car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError) utils.RespondWithError(c, err, "Error deleting car", http.StatusInternalServerError, errors.Responses.Fields.Car, errors.Responses.Keys.InternalServerError)
return return

View File

@@ -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,6 +119,7 @@ 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",
@@ -130,7 +131,7 @@ func TestMain(t *testing.T) {
ZipCode: "12345", ZipCode: "12345",
City: "SampleCity", City: "SampleCity",
Status: constants.ActiveStatus, Status: constants.ActiveStatus,
Password: "", Password: password,
Notes: "", Notes: "",
RoleID: constants.Roles.Admin, RoleID: constants.Roles.Admin,
Consents: nil, Consents: nil,
@@ -140,7 +141,6 @@ func TestMain(t *testing.T) {
Licence: &models.Licence{ Licence: &models.Licence{
Status: constants.UnverifiedStatus, Status: constants.UnverifiedStatus,
}} }}
admin.SetPassword("securepassword")
admin.Create(db) admin.Create(db)
validation.SetupValidators(db) validation.SetupValidators(db)
t.Run("userController", func(t *testing.T) { t.Run("userController", func(t *testing.T) {
@@ -203,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",
@@ -284,7 +284,7 @@ func getBaseUser() models.User {
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,
Password: "passw@#$#%$!-ord123", Password: "passw@#$#%$!-ord123",
Company: "", Company: "",
@@ -303,7 +303,7 @@ func getBaseSupporter() models.User {
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,
Password: "passw@#$#%$!-ord123", Password: "passw@#$#%$!-ord123",
Company: "", Company: "",

View File

@@ -33,7 +33,7 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
return return
} }
var subscription models.SubscriptionModel var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil { if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
@@ -43,9 +43,9 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
id, err := mc.Service.RegisterSubscription(&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
} }
@@ -69,7 +69,7 @@ func (mc *MembershipController) UpdateHandler(c *gin.Context) {
return return
} }
var subscription models.SubscriptionModel var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil { if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err) utils.HandleValidationError(c, err)
return return
@@ -122,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,8 +148,8 @@ func (dt *DeleteSubscriptionTest) ValidateResult() error {
return validateSubscription(dt.Assert, dt.WantDBData) return validateSubscription(dt.Assert, dt.WantDBData)
} }
func getBaseSubscription() models.SubscriptionModel { func getBaseSubscription() models.Subscription {
return models.SubscriptionModel{ return models.Subscription{
Name: "Premium", Name: "Premium",
Details: "A subscription detail", Details: "A subscription detail",
MonthlyFee: 12.0, MonthlyFee: 12.0,
@@ -157,7 +157,7 @@ func getBaseSubscription() models.SubscriptionModel {
} }
} }
func customizeSubscription(customize func(models.SubscriptionModel) models.SubscriptionModel) models.SubscriptionModel { func customizeSubscription(customize func(models.Subscription) models.Subscription) models.Subscription {
subscription := getBaseSubscription() subscription := getBaseSubscription()
return customize(subscription) return customize(subscription)
} }
@@ -173,7 +173,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "" subscription.Details = ""
return subscription return subscription
})), })),
@@ -187,7 +187,7 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
WantDBData: map[string]interface{}{"name": ""}, WantDBData: map[string]interface{}{"name": ""},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "" subscription.Name = ""
return subscription return subscription
})), })),
@@ -200,7 +200,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.MonthlyFee = -10.0 sub.MonthlyFee = -10.0
return sub return sub
})), })),
@@ -213,7 +213,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.HourlyRate = -1.0 sub.HourlyRate = -1.0
return sub return sub
})), })),
@@ -227,7 +227,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
@@ -243,7 +243,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1 subscription.IncludedPerMonth = 1
@@ -274,7 +274,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.MonthlyFee = 123.0 subscription.MonthlyFee = 123.0
return subscription return subscription
})), })),
@@ -288,7 +288,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.ID = 0 subscription.ID = 0
return subscription return subscription
})), })),
@@ -302,7 +302,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.HourlyRate = 3254.0 subscription.HourlyRate = 3254.0
return subscription return subscription
})), })),
@@ -316,7 +316,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerYear = 9873.0 subscription.IncludedPerYear = 9873.0
return subscription return subscription
})), })),
@@ -330,7 +330,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerMonth = 23415.0 subscription.IncludedPerMonth = 23415.0
return subscription return subscription
})), })),
@@ -344,7 +344,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription" subscription.Name = "NonExistentSubscription"
return subscription return subscription
})), })),
@@ -358,7 +358,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details" subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
@@ -375,7 +375,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details" subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition" subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0 subscription.IncludedPerYear = 0
@@ -388,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)
@@ -402,7 +402,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription" subscription.Name = "NonExistentSubscription"
subscription.ID = basicSub.ID subscription.ID = basicSub.ID
logger.Error.Printf("subscription to delete: %#v", subscription) logger.Error.Printf("subscription to delete: %#v", subscription)
@@ -418,7 +418,7 @@ func getSubscriptionDeleteData() []DeleteSubscriptionTest {
WantDBData: map[string]interface{}{"name": ""}, WantDBData: map[string]interface{}{"name": ""},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
customizeSubscription(func(subscription models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "" subscription.Name = ""
subscription.ID = basicSub.ID subscription.ID = basicSub.ID
return subscription return subscription
@@ -433,7 +433,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Basic" subscription.Name = "Basic"
subscription.ID = basicSub.ID subscription.ID = basicSub.ID
return subscription return subscription
@@ -448,7 +448,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium" subscription.Name = "Premium"
subscription.ID = premiumSub.ID subscription.ID = premiumSub.ID
return subscription return subscription
@@ -463,7 +463,7 @@ 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 models.SubscriptionModel) models.SubscriptionModel { customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium" subscription.Name = "Premium"
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

@@ -34,7 +34,6 @@ func setupTestContext() (*TestContext, error) {
logger.Error.Printf("error fetching user: %#v", err) logger.Error.Printf("error fetching user: %#v", err)
return nil, err return nil, err
} }
logger.Error.Printf("found user: %#v", user)
return &TestContext{ return &TestContext{
router: gin.Default(), router: gin.Default(),
response: httptest.NewRecorder(), response: httptest.NewRecorder(),
@@ -104,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

@@ -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
} }
@@ -240,12 +244,12 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
} }
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)
@@ -254,7 +258,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
@@ -347,7 +351,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

@@ -352,7 +352,7 @@ 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)
@@ -407,12 +407,15 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error {
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)
} }
@@ -686,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) {
@@ -792,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 = ""
@@ -820,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 {
@@ -871,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)
@@ -907,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)
@@ -951,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())
} }
@@ -1132,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
})), })),
}, },
@@ -1145,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
})), })),
}, },
@@ -1155,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
})), })),
}, },
@@ -1165,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
})), })),
}, },
@@ -1204,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
})), })),
}, },
@@ -1216,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
})), })),
}, },
@@ -1228,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
})), })),
}, },

View File

@@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/alexedwards/argon2id"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -53,7 +52,7 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, error) {
if err := db.AutoMigrate( if err := db.AutoMigrate(
&models.User{}, &models.User{},
&models.SubscriptionModel{}, &models.Subscription{},
&models.Membership{}, &models.Membership{},
&models.Consent{}, &models.Consent{},
&models.Verification{}, &models.Verification{},
@@ -83,12 +82,12 @@ func Open(dbPath string, adminMail string, debug bool) (*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)
@@ -103,7 +102,7 @@ func Open(dbPath string, adminMail string, debug bool) (*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
} }
@@ -118,10 +117,10 @@ func Open(dbPath string, adminMail string, debug bool) (*gorm.DB, 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,
@@ -162,11 +161,6 @@ func createAdmin(userMail string) (*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)
@@ -176,7 +170,7 @@ func createAdmin(userMail string) (*models.User, error) {
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,
Company: "", Company: "",
Address: "", Address: "",
ZipCode: "", ZipCode: "",

View File

@@ -1,13 +1,54 @@
package models package models
import "time" import (
"GoMembership/pkg/logger"
"time"
"gorm.io/gorm"
)
type Insurance struct { type Insurance struct {
ID uint `gorm:"primary_key" json:"id"` ID uint `gorm:"primary_key" json:"id"`
OwnerID uint `gorm:"not null" json:"owner_id" binding:"numeric"` Cars []Car `gorm:"many2many:car_insurances;" json:"-"`
Company string `json:"company" binding:"safe_content"` Company string `json:"company" binding:"safe_content"`
Reference string `json:"reference" binding:"safe_content"` Reference string `json:"reference" binding:"safe_content"`
Notes string `json:"notes" binding:"safe_content"` Notes string `json:"notes" binding:"safe_content"`
StartDate time.Time `json:"start_date"` StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_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,7 +1,9 @@
package models package models
import ( import (
"GoMembership/internal/config"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"fmt"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -48,3 +50,10 @@ func (b *BankAccount) Update(db *gorm.DB) error {
func (b *BankAccount) Delete(db *gorm.DB) error { func (b *BankAccount) Delete(db *gorm.DB) error {
return db.Delete(&b).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

@@ -1,15 +1,15 @@
package models package models
import ( import (
"GoMembership/pkg/errors"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type Car struct { type Car 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 DeletedAt *time.Time
@@ -19,105 +19,133 @@ type Car struct {
Model string `gorm:"not null" json:"model"` Model string `gorm:"not null" json:"model"`
Color string `gorm:"not null" json:"color"` Color string `gorm:"not null" json:"color"`
LicencePlate string `gorm:"not null,unique" json:"licence_plate"` LicencePlate string `gorm:"not null,unique" json:"licence_plate"`
Price float32 `json:"price"` Price float32 `gorm:"type:decimal(10,2)" json:"price"`
Rate float32 `json:"rate"` Rate float32 `gorm:"type:decimal(10,2)" json:"rate"`
StartDate time.Time `json:"start_date"` StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"` EndDate time.Time `json:"end_date"`
Location Location `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"location"` Location *Location `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"location"`
LocationID uint Damages []Damage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"damages"`
Damages *[]Damage `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"damages"` Insurances []Insurance `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;many2many:car_insurances" json:"insurances"`
Insurances *[]Insurance `gorm:"foreignkey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
Notes string `json:"notes"`
}
type Location struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Latitude float32 `json:"latitude"`
Longitude float32 `json:"longitude"`
}
type Damage struct {
ID uint `gorm:"primarykey" json:"id"`
CarID uint `json:"car_id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
Opponent *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"opponent"`
OpponentID uint
Insurance *Insurance `gorm:"foreignkey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"insurance"`
InsuranceID uint
Notes string `json:"notes"` Notes string `json:"notes"`
} }
func (c *Car) Create(db *gorm.DB) error { func (c *Car) 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(c).Error; err != nil {
if err := tx.Create(c).Error; err != nil {
return err return err
} }
// Replace associated Categories (assumes Categories already exist)
if c.Insurances != nil {
if err := tx.Model(c).Association("Insurances").Replace(c.Insurances); err != nil {
return err
}
}
logger.Info.Printf("car created: %#v", c) logger.Info.Printf("car created: %#v", c)
// Preload all associations to return the fully populated User
return tx. return tx.
Preload("Insurances"). Preload(clause.Associations).
First(c, c.ID).Error // Refresh the user object with all associations First(c, c.ID).Error
}) })
} }
func (c *Car) Update(db *gorm.DB) error { func (c *Car) Update(db *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database // Check if the user exists in the database
var existingCar Car var existingCar Car
logger.Info.Printf("updating car: %#v", c) logger.Info.Printf("updating car: %#v", c)
if err := tx. if err := tx.Preload("Damages.Insurance").
Preload("Insurances"). Preload("Damages.Opponent").
First(&existingCar, c.ID).Error; err != nil { First(&existingCar, c.ID).Error; err != nil {
return err return err
} }
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(c)
if result.Error != nil {
logger.Error.Printf("car update error: %#v", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.ErrNoRowsAffected
}
if c.Insurances != nil { if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(c).Error; err != nil {
if err := tx.Save(*c.Insurances).Error; err != nil {
return err return err
} }
} // if err := tx.Model(c).Association("Damages").Replace(c.Damages); err != nil {
return 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
}) })
if err != nil {
return err
}
return db.
Preload("Insurances").
First(&c, c.ID).Error
} }
func (c *Car) Delete(db *gorm.DB) error { func (c *Car) Delete(db *gorm.DB) error {
return db.Delete(&c).Error return db.Select(clause.Associations).Delete(&c).Error
} }
func GetAllCars(db *gorm.DB) ([]Car, error) { func GetAllCars(db *gorm.DB) ([]Car, error) {
var cars []Car var cars []Car
if err := db.Find(&cars).Error; err != nil { if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
Find(&cars).Error; err != nil {
return nil, err return nil, err
} }
return cars, nil return cars, nil
@@ -125,7 +153,11 @@ func GetAllCars(db *gorm.DB) ([]Car, error) {
func (c *Car) FromID(db *gorm.DB, id uint) error { func (c *Car) FromID(db *gorm.DB, id uint) error {
var car Car var car Car
if err := db.Preload("Insurances").First(&car, id).Error; err != nil { if err := db.
Preload(clause.Associations).
Preload("Damages").
Preload("Insurances").
First(&car, id).Error; err != nil {
return err return err
} }
*c = car *c = car

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

@@ -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

@@ -15,13 +15,13 @@ type Membership struct {
StartDate time.Time `json:"start_date"` StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"` EndDate time.Time `json:"end_date"`
Status int8 `json:"status" binding:"number,safe_content"` Status int8 `json:"status" binding:"number,safe_content"`
SubscriptionModel SubscriptionModel `gorm:"foreignKey:SubscriptionModelID" json:"subscription"` Subscription Subscription `gorm:"foreignKey:SubscriptionID" json:"subscription"`
SubscriptionModelID uint `json:"subsription_model_id"` SubscriptionID uint `json:"subscription_id"`
ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"` ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"`
} }
func (m *Membership) BeforeSave(tx *gorm.DB) error { func (m *Membership) BeforeSave(tx *gorm.DB) error {
m.SubscriptionModelID = m.SubscriptionModel.ID m.SubscriptionID = m.Subscription.ID
return nil return nil
} }
@@ -31,7 +31,7 @@ func (m *Membership) Create(db *gorm.DB) error {
} }
logger.Info.Printf("Membership created: %#v", m) logger.Info.Printf("Membership created: %#v", m)
return db.Preload("SubscriptionModel").First(m, m.ID).Error // Refresh the user object with SubscriptionModel return db.Preload("Subscription").First(m, m.ID).Error // Refresh the user object with Subscription
} }
func (m *Membership) Update(db *gorm.DB) error { func (m *Membership) Update(db *gorm.DB) error {

View File

@@ -7,7 +7,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type SubscriptionModel struct { type Subscription struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
@@ -21,31 +21,31 @@ type SubscriptionModel struct {
IncludedPerMonth int16 `json:"included_hours_per_month"` IncludedPerMonth int16 `json:"included_hours_per_month"`
} }
func (s *SubscriptionModel) Create(db *gorm.DB) error { func (s *Subscription) 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) // Create the base User record (omit associations to handle them separately)
if err := tx.Create(s).Error; err != nil { if err := tx.Create(s).Error; err != nil {
return err return err
} }
logger.Info.Printf("SubscriptionModel created: %#v", s) logger.Info.Printf("Subscription created: %#v", s)
// Preload all associations to retuvn the fully populated User // Preload all associations to retuvn the fully populated User
return tx. return tx.
First(s, s.ID).Error // Refresh the user object with all associations First(s, s.ID).Error // Refresh the user object with all associations
}) })
} }
func (s *SubscriptionModel) Update(db *gorm.DB) error { func (s *Subscription) Update(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database // Check if the user exists in the database
var existingSubscriptionModel SubscriptionModel var existingSubscription Subscription
logger.Info.Printf("updating SubscriptionModel: %#v", s) logger.Info.Printf("updating Subscription: %#v", s)
if err := tx.First(&existingSubscriptionModel, s.ID).Error; err != nil { if err := tx.First(&existingSubscription, s.ID).Error; err != nil {
return err return err
} }
if err := tx.Model(&existingSubscriptionModel).Updates(s).Error; err != nil { if err := tx.Model(&existingSubscription).Updates(s).Error; err != nil {
return err return err
} }
return tx.First(s, s.ID).Error return tx.First(s, s.ID).Error
@@ -53,6 +53,6 @@ func (s *SubscriptionModel) Update(db *gorm.DB) error {
} }
func (s *SubscriptionModel) Delete(db *gorm.DB) error { func (s *Subscription) Delete(db *gorm.DB) error {
return db.Delete(&s).Error return db.Delete(&s).Error
} }

View File

@@ -3,7 +3,6 @@ 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"
@@ -45,7 +44,7 @@ type User struct {
func (u *User) AfterCreate(tx *gorm.DB) (err error) { func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.BankAccount != nil && u.BankAccount.MandateReference == "" { if u.BankAccount != nil && u.BankAccount.MandateReference == "" {
u.BankAccount.MandateReference = u.GenerateMandateReference() u.BankAccount.MandateReference = u.BankAccount.GenerateMandateReference(u.ID)
u.BankAccount.Update(tx) u.BankAccount.Update(tx)
} }
return nil return nil
@@ -53,38 +52,16 @@ func (u *User) AfterCreate(tx *gorm.DB) (err error) {
func (u *User) BeforeSave(tx *gorm.DB) (err error) { func (u *User) BeforeSave(tx *gorm.DB) (err error) {
u.Email = strings.ToLower(u.Email) u.Email = strings.ToLower(u.Email)
return nil if u.Password != "" {
} hash, err := argon2id.CreateHash(u.Password, argon2id.DefaultParams)
func (u *User) GenerateMandateReference() string {
return fmt.Sprintf("%s%d%s", time.Now().Format("20060102"), u.ID, u.BankAccount.IBAN)
}
func (u *User) SetPassword(plaintextPassword string) error {
if plaintextPassword == "" {
return nil
}
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil { if err != nil {
return err return err
} }
u.Password = hash 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 {
@@ -96,118 +73,6 @@ func (u *User) Create(db *gorm.DB) error {
}) })
} }
// return db.Transaction(func(tx *gorm.DB) error {
// // Initialize slices/pointers if nil
// if u.Verifications == nil {
// u.Verifications = &[]Verification{}
// }
// // Create base user first
// if err := tx.Omit(clause.Associations).Create(u).Error; err != nil {
// return fmt.Errorf("failed to create user: %w", err)
// }
// // Handle BankAccount
// if u.BankAccount != (BankAccount{}) {
// u.BankAccount.MandateReference = u.GenerateMandateReference()
// if err := tx.Create(&u.BankAccount).Error; err != nil {
// return fmt.Errorf("failed to create bank account: %w", err)
// }
// if err := tx.Model(u).Update("bank_account_id", u.BankAccount.ID).Error; err != nil {
// return fmt.Errorf("failed to link bank account: %w", err)
// }
// }
// // Handle Membership and SubscriptionModel
// if u.Membership != (Membership{}) {
// if err := tx.Create(&u.Membership).Error; err != nil {
// return fmt.Errorf("failed to create membership: %w", err)
// }
// if err := tx.Model(u).Update("membership_id", u.Membership.ID).Error; err != nil {
// return fmt.Errorf("failed to link membership: %w", err)
// }
// }
// // Handle Licence and Categories
// if u.Licence != nil {
// u.Licence.UserID = u.ID
// if err := tx.Create(u.Licence).Error; err != nil {
// return fmt.Errorf("failed to create licence: %w", err)
// }
// if len(u.Licence.Categories) > 0 {
// if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
// return fmt.Errorf("failed to link categories: %w", err)
// }
// }
// if err := tx.Model(u).Update("licence_id", u.Licence.ID).Error; err != nil {
// return fmt.Errorf("failed to link licence: %w", err)
// }
// }
// // Handle Consents
// for i := range u.Consents {
// u.Consents[i].UserID = u.ID
// }
// if len(u.Consents) > 0 {
// if err := tx.Create(&u.Consents).Error; err != nil {
// return fmt.Errorf("failed to create consents: %w", err)
// }
// }
// // Handle Verifications
// for i := range *u.Verifications {
// (*u.Verifications)[i].UserID = u.ID
// }
// if len(*u.Verifications) > 0 {
// if err := tx.Create(u.Verifications).Error; err != nil {
// return fmt.Errorf("failed to create verifications: %w", err)
// }
// }
// // Reload the complete user with all associations
// return tx.Preload(clause.Associations).
// Preload("Membership.SubscriptionModel").
// Preload("Licence.Categories").
// First(u, u.ID).Error
// })
// }
// func (u *User) 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(u).Error; err != nil {
// return err
// }
// for i := range u.Consents {
// u.Consents[i].UserID = u.ID
// }
// for i := range *u.Verifications {
// (*u.Verifications)[i].UserID = u.ID
// }
// if err := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(u).Error; err != nil {
// return err
// }
// // Replace associated Categories (assumes Categories already exist)
// 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(clause.Associations).
// Preload("Membership.SubscriptionModel").
// Preload("Licence.Categories").
// First(u, u.ID).Error // Refresh the user object with all associations
// })
// }
func (u *User) Update(db *gorm.DB) error { func (u *User) Update(db *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error { err := db.Transaction(func(tx *gorm.DB) error {
@@ -220,7 +85,7 @@ func (u *User) Update(db *gorm.DB) error {
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", "Verifications", "Licence.Categories").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
@@ -229,57 +94,6 @@ 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).Where("id = ?", existingUser.Membership.ID).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 return err
@@ -300,16 +114,20 @@ func (u *User) Update(db *gorm.DB) error {
return db. return db.
Preload(clause.Associations). Preload(clause.Associations).
Preload("Membership.SubscriptionModel"). Preload("Membership.Subscription").
Preload("Licence.Categories"). Preload("Licence.Categories").
First(&u, u.ID).Error First(&u, u.ID).Error
} }
func (u *User) Delete(db *gorm.DB) 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.SubscriptionModel"). Preload("Membership.Subscription").
Preload("Licence.Categories"). Preload("Licence.Categories").
First(&user, userID) First(&user, userID)
if result.Error != nil { if result.Error != nil {
@@ -326,7 +144,7 @@ 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.SubscriptionModel"). Preload("Membership.Subscription").
Preload("Licence.Categories"). Preload("Licence.Categories").
Where("email = ?", email).First(&user) Where("email = ?", email).First(&user)
if result.Error != nil { if result.Error != nil {
@@ -354,6 +172,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
} }
@@ -378,24 +204,20 @@ func (u *User) SetVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil { if u.Verifications == nil {
u.Verifications = []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,
VerificationToken: token,
Type: verificationType,
}
if vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 { if vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 {
u.Verifications[vi] = v 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
} }
@@ -406,10 +228,10 @@ func (u *User) GetVerification(verificationType string) (*Verification, error) {
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 {
@@ -418,17 +240,9 @@ func (u *User) Verify(token string, verificationType string) bool {
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{} {
@@ -442,14 +256,14 @@ func (u *User) Safe() map[string]interface{} {
"end_date": u.Membership.EndDate, "end_date": u.Membership.EndDate,
"status": u.Membership.Status, "status": u.Membership.Status,
"subscription": 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,
}, },
} }
} }
@@ -543,7 +357,7 @@ func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) {
var users []User var users []User
result := db. result := db.
Preload(clause.Associations). Preload(clause.Associations).
Preload("Membership.SubscriptionModel"). Preload("Membership.Subscription").
Preload("Licence.Categories"). Preload("Licence.Categories").
Where(where).Find(&users) Where(where).Find(&users)
if result.Error != nil { if result.Error != nil {

View File

@@ -1,6 +1,8 @@
package models package models
import ( import (
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"time" "time"
@@ -46,3 +48,31 @@ func (v *Verification) Update(db *gorm.DB) error {
func (v *Verification) Delete(db *gorm.DB) error { func (v *Verification) Delete(db *gorm.DB) error {
return db.Delete(&v).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

@@ -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.id = memberships.user_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

@@ -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{}

View File

@@ -59,8 +59,8 @@ func (s *CarService) FromID(id uint) (*models.Car, error) {
// GetAll retrieves all cars // GetAll retrieves all cars
func (s *CarService) GetAll() (*[]models.Car, error) { func (s *CarService) GetAll() (*[]models.Car, error) {
var cars []models.Car cars, err := models.GetAllCars(s.DB)
if err := s.DB.Find(&cars).Error; err != nil { if err != nil {
return nil, err return nil, err
} }
return &cars, nil return &cars, nil

View File

@@ -169,10 +169,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 +216,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

@@ -74,15 +74,13 @@ func (s *UserService) Update(user *models.User) (*models.User, error) {
} }
user.BankAccount.ID = existingUser.BankAccount.ID user.BankAccount.ID = existingUser.BankAccount.ID
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 {
@@ -97,14 +95,13 @@ 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.BankAccount.MandateDateSigned = time.Now() user.BankAccount.MandateDateSigned = time.Now()
v, err := user.SetVerification(constants.VerificationTypes.Email) v, err := user.SetVerification(constants.VerificationTypes.Email)

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,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

@@ -16,6 +16,6 @@ func SetupValidators(db *gorm.DB) {
// 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

@@ -3,19 +3,17 @@ package validation
import ( import (
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
// 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", "")
} }
logger.Error.Printf("parent.type.name: %#v", sl.Parent().Type().Name())
if sl.Parent().Type().Name() == "" { if sl.Parent().Type().Name() == "" {
// This is modifying a subscription directly // This is modifying a subscription directly
if subscription.Details == "" { if subscription.Details == "" {

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

@@ -31,7 +31,7 @@ type ValidationKeys struct {
InternalServerError string InternalServerError string
InvalidJSON string InvalidJSON string
InvalidUserID string InvalidUserID string
InvalidSubscriptionModel string InvalidSubscription string
Unauthorized string Unauthorized string
UserNotFoundWrongPassword string UserNotFoundWrongPassword string
JwtGenerationFailed string JwtGenerationFailed string
@@ -48,7 +48,7 @@ type ValidationKeys struct {
type ValidationFields struct { type ValidationFields struct {
General string General string
ParentMembershipID string ParentMembershipID string
SubscriptionModel string Subscription string
Login string Login string
Email string Email string
User string User string
@@ -66,7 +66,7 @@ var Responses = struct {
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", InvalidUserID: "server.validation.invalid_user_id",
InvalidSubscriptionModel: "server.validation.invalid_subscription", 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",
@@ -82,7 +82,7 @@ var Responses = struct {
Fields: ValidationFields{ Fields: ValidationFields{
General: "server.general", General: "server.general",
ParentMembershipID: "parent_membership_id", ParentMembershipID: "parent_membership_id",
SubscriptionModel: "subscription", Subscription: "subscription",
Login: "user.login", Login: "user.login",
Email: "user.email", Email: "user.email",
User: "user.user", User: "user.user",