Compare commits

..

60 Commits

Author SHA1 Message Date
Alex
9d25fa005c gorm model fixes 2025-05-20 12:41:21 +02:00
Alex
20a693a80c Setting Consents userid null upon deletion 2025-05-14 18:25:53 +02:00
Alex
242d37713d unscoped delete 2025-05-14 15:36:20 +02:00
Alex
4b83be6ed8 templates and constraints 2025-05-14 15:27:10 +02:00
Alex
8fcb73f24d logging 2025-05-14 12:36:46 +02:00
Alex
11740cb503 fixes in prod 2025-05-14 12:29:47 +02:00
Alex
06f8078b17 chg: mail templates 2025-05-14 12:13:12 +02:00
Alex
18f5dadb06 wip 2025-04-10 15:40:22 +02:00
Alex
87f08dd3be subscription_model -> subscription 2025-03-24 18:00:57 +01:00
Alex
2af4575ff2 new db management 2025-03-24 17:46:25 +01:00
Alex
560623788a refactor 2025-03-24 17:46:11 +01:00
Alex
5d55f5a8d9 add new db constraints and foreignKey mode 2025-03-24 17:45:55 +01:00
Alex
28dfe7ecde refactoring 2025-03-24 17:45:33 +01:00
Alex
741145b960 added Opponent 2025-03-24 17:44:45 +01:00
Alex
490b29295f subscription_validation 2025-03-15 00:14:51 +01:00
Alex
ded5e6ceb1 tests 2025-03-15 00:14:31 +01:00
Alex
b81804439e moved to flat json handling 2025-03-15 00:14:21 +01:00
Alex
9a9af9f002 returning safeUsers now 2025-03-15 00:13:53 +01:00
Alex
cd495584b0 model work 2025-03-15 00:13:23 +01:00
Alex
bbead3c43b backend errors typo 2025-03-15 00:12:59 +01:00
Alex
ce18324391 backend: add car 2025-03-15 00:12:46 +01:00
Alex
c9d5a88dbf frontend: add car handling 2025-03-15 00:12:00 +01:00
Alex
380fee09c1 add: carEditForm 2025-03-15 00:11:41 +01:00
Alex
e35524132e add: car processing 2025-03-15 00:11:27 +01:00
Alex
af000aa4bc add: car date handline 2025-03-15 00:11:10 +01:00
Alex
90ed0925ca new defaults (car) 2025-03-15 00:10:47 +01:00
Alex
7c0a6fedb5 locale 2025-03-15 00:10:25 +01:00
Alex
0e6edc8e65 moved to default subscription in editform 2025-03-15 00:10:16 +01:00
Alex
45a219625a mod: supporter summary 2025-03-12 17:14:20 +01:00
Alex
ee10389f1d changed supporter default to passiv 2025-03-12 17:13:44 +01:00
Alex
073d353764 tests 2025-03-11 20:52:54 +01:00
Alex
9d2b33f832 adapted new user model. 2025-03-11 20:52:39 +01:00
Alex
e60aaa1d69 refactored auth.go & tests 2025-03-11 20:52:11 +01:00
Alex
ca99e28433 removed logging in csp 2025-03-11 20:51:54 +01:00
Alex
9427492cb1 removed logging in headers 2025-03-11 20:51:44 +01:00
Alex
60d3f075bf naming 2025-03-11 20:51:28 +01:00
Alex
d473aef3a9 refactored user service for new model repo style 2025-03-11 20:51:05 +01:00
Alex
ef4d3c9576 add userid to drivers licence model 2025-03-11 20:50:34 +01:00
Alex
9a8b386931 cleaned verification model 2025-03-11 20:50:19 +01:00
Alex
9c429185dc add route for new mail verification 2025-03-11 20:47:57 +01:00
Alex
c7865d0582 add user id to mail verification url 2025-03-11 20:47:31 +01:00
Alex
feb8abcc42 refactored validation 2025-03-11 20:46:45 +01:00
Alex
c8d0904fd7 del obsolete repos and services 2025-03-11 20:46:24 +01:00
Alex
294ad76e4b first step to remove global database.db 2025-03-11 20:45:49 +01:00
Alex
ca441d51e7 moved repo to user model 2025-03-11 20:44:29 +01:00
Alex
39c060794a del obsolete handleVerifyUserError 2025-03-11 20:43:42 +01:00
Alex
c6ea179eca moved field validation to validation package 2025-03-11 20:42:45 +01:00
Alex
0d6013d566 add new errors 2025-03-11 20:42:05 +01:00
Alex
0c3204df15 fix: moved to licenceServiceInterface 2025-03-11 20:39:45 +01:00
Alex
cfc10ab087 moved generateRandomString to local config class 2025-03-11 20:39:12 +01:00
Alex
df6125b7cb locale 2025-03-11 20:30:58 +01:00
Alex
7af66ee9de added user_password tests 2025-03-05 14:54:19 +01:00
Alex
b2b702c21d readonly fields changed 2025-03-05 10:54:03 +01:00
Alex
fa996692fe inputfield: readonly is more prominent 2025-03-04 10:04:06 +01:00
Alex
8258a7c2a3 missing base var 2025-03-03 18:17:52 +01:00
Alex
37ccbaaba4 added redirect logging 2025-03-03 18:14:46 +01:00
Alex
c810e48451 Add: CreateBackendAccess function 2025-03-03 17:52:19 +01:00
Alex
8f737282f2 disabled logging 2025-03-03 17:51:38 +01:00
Alex
3756205ad4 locale 2025-03-03 17:51:24 +01:00
Alex
68851c6257 frontend: DO IT THE SVELTE WAY 2025-03-03 17:51:12 +01:00
84 changed files with 4709 additions and 1792 deletions

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

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

View File

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

View File

@@ -50,9 +50,9 @@
let inputValue = target.value;
if (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();
}
}
@@ -132,8 +132,11 @@
{/if}
{#if readonly}
<input {name} type="hidden" bind:value />
{/if}
{#if type === 'select'}
<span class="label"
>{type == 'select' && typeof value === 'number' ? options[value].label : value}</span
>
{:else if type === 'select'}
<select
{name}
bind:value
@@ -176,7 +179,7 @@
<style>
:root {
--form-control-color: var(--green); /* Changed from #6bff55 */
--form-control-disabled: var(--overlay0); /* Changed from #959495 */
--form-control-disabled: var(--subtext1); /* Changed from #959495 */
}
.form-control {

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ export default {
5: 'Passiv'
},
userRole: {
'-1': 'Unfallgegner',
0: 'Sponsor',
1: 'Mitglied',
2: 'Betrachter',
@@ -14,6 +15,12 @@ export default {
8: 'Administrator'
},
placeholder: {
car_name: 'Hat das Fahrzeug einen Namen?',
car_brand: 'Fahrzeughersteller eingeben...',
car_model: 'Fahrzeugmodell eingeben...',
car_color: 'Fahrzeugfarbe eingeben...',
car_licence_plate: 'Fahrzeugkennzeichen eingeben...',
insurance_reference: 'Versicherungsnummer eingeben...',
password: 'Passwort eingeben...',
email: 'Emailadresse eingeben...',
company: 'Firmennamen eingeben...',
@@ -69,12 +76,13 @@ export default {
validation: {
invalid: 'ungültig',
invalid_user_id: 'Nutzer ID ungültig',
invalid_subscription_model: 'Model nicht gefunden',
invalid_subscription: 'Model nicht gefunden',
user_not_found: '{field} konnte nicht gefunden werden',
invalid_user_data: 'Nutzerdaten ungültig',
user_not_found_or_wrong_password: 'Existiert nicht oder falsches Passwort',
email_already_registered: 'Ein Mitglied wurde schon mit dieser Emailadresse erstellt.',
password_already_changed: 'Das Passwort wurde schon geändert.',
user_already_verified: 'Ihre Email Adresse wurde schon bestätigt.',
insecure: 'Unsicheres Passwort, versuchen Sie {message}',
longer: 'oder verwenden Sie ein längeres Passwort',
special: 'mehr Sonderzeichen einzufügen',
@@ -123,6 +131,7 @@ export default {
edit: 'Nutzer bearbeiten',
create: 'Nutzer erstellen',
user: 'Nutzer',
member: 'Mitglied',
management: 'Mitgliederverwaltung',
id: 'Mitgliedsnr',
first_name: 'Vorname',
@@ -130,11 +139,14 @@ export default {
phone: 'Telefonnummer',
dateofbirth: 'Geburtstag',
email: 'Email',
membership: 'Mitgliedschaft',
bank_account: 'Kontodaten',
status: 'Status',
role: 'Nutzerrolle',
supporter: 'Sponsor'
supporter: 'Sponsor',
opponent: 'Unfallgegner'
},
subscription: {
subscriptions: {
name: 'Modellname',
edit: 'Modell bearbeiten',
create: 'Modell erstellen',
@@ -146,30 +158,63 @@ export default {
included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: 'Inkludierte Stunden pro Monat'
},
car: {
car: 'Fahrzeug',
model: 'Modell',
brand: 'Marke',
licence_plate: 'Kennzeichen',
edit: 'Fahrzeug bearbeiten',
create: 'Fahrzeug hinzufügen',
damages: 'Schäden',
start_date: 'Anschaffungsdatum',
end_date: 'Leasingende',
leasing_rate: 'Leasingrate'
},
insurances: {
edit: 'Daten bearbeiten',
create: 'Versicherung erstellen'
},
loading: {
user_data: 'Lade Nutzerdaten',
subscription_data: 'Lade Modelldaten',
insurance_data: 'Lade Versicherungsdaten',
car_data: 'Lade Fahrzeugdaten',
please_wait: 'Bitte warten...',
updating: 'Aktualisiere...'
},
dialog: {
user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?',
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?'
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?',
car_deletion: 'Soll das Fahrzeug {name} wirklich gelöscht werden?',
insurance_deletion: 'Soll die Versicherung {name} wirklich gelöscht werden?',
damage_deletion: 'Soll der Schaden {name} wirklich gelöscht werden?',
backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?'
},
cancel: 'Abbrechen',
confirm: 'Bestätigen',
actions: 'Aktionen',
create: 'Hinzufügen',
edit: 'Bearbeiten',
delete: 'Löschen',
not_set: 'Nicht gesetzt',
noone: 'Niemand',
search: 'Suche:',
name: 'Name',
date: 'Datum',
price: 'Preis',
color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren',
no_insurance: 'Keine Versicherung',
supporter: 'Sponsoren',
mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell',
subscription: 'Mitgliedschatfsmodell',
licence: 'Führerschein',
licence_number: 'Führerscheinnummer',
insurance: 'Versicherung',
insurance_reference: 'Versicherungsnummer',
issued_date: 'Ausgabedatum',
month: 'Monat',
expiration_date: 'Ablaufdatum',
country: 'Land',
details: 'Details',
@@ -188,8 +233,7 @@ export default {
company: 'Firma',
login: 'Anmeldung',
profile: 'Profil',
membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten',
cars: 'Fahrzeuge',
status: 'Status',
start: 'Beginn',
end: 'Ende',
@@ -202,6 +246,7 @@ export default {
payments: 'Zahlungen',
add_new: 'Neu',
email_sent: 'Email wurde gesendet..',
verification: 'Verifikation',
// For payments section
payment: {
id: 'Zahlungs-Nr',

View File

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

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ export function isEmpty(obj) {
* @returns string
*/
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);
return date.toISOString();
}
@@ -88,6 +88,31 @@ export function fromRFC3339(dateString) {
return date.toISOString().split('T')[0];
}
/**
*
* @param {App.Types['car']} car - The car object to format
*/
export function carDatesFromRFC3339(car) {
car.end_date = fromRFC3339(car.end_date);
car.start_date = fromRFC3339(car.start_date);
car.insurances?.forEach((insurance) => {
insurance.start_date = fromRFC3339(insurance.start_date);
insurance.end_date = fromRFC3339(insurance.end_date);
});
}
/**
*
* @param {App.Types['car']} car - The car object to format
*/
export function carDatesToRFC3339(car) {
car.end_date = toRFC3339(car.end_date);
car.start_date = toRFC3339(car.start_date);
car.insurances?.forEach((insurance) => {
insurance.start_date = toRFC3339(insurance.start_date);
insurance.end_date = toRFC3339(insurance.end_date);
});
}
/**
*
* @param {App.Locals['user']} user - The user object to format

View File

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

View File

@@ -30,8 +30,21 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData();
const rawData = formDataToObject(formData);
const processedData = processUserFormData(rawData);
const rawFormData = formDataToObject(formData);
/** @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;
// console.log('Is creating: ', isCreating);

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ export async function load({ locals }) {
console.log('loading login page');
if (locals.user) {
console.log('user is logged in');
throw redirect(302, '/');
throw redirect(302, `${base}/auth/about/${locals.user.id}`);
}
}

View File

@@ -29,6 +29,7 @@ export const actions = {
const response = await res.json();
// redirect the user
console.log('redirecting to: ', `${base}/auth/confirming?message=${response.message}`);
throw redirect(302, `${base}/auth/confirming?message=${response.message}`);
}
};

View File

@@ -45,6 +45,7 @@ export const actions = {
const response = await res.json();
// redirect the user
console.log('redirecting to: ', `${base}/auth/login?message=${response.message}`);
throw redirect(302, `${base}/auth/login?message=${response.message}`);
}
};

View File

@@ -18,18 +18,18 @@ func main() {
config.LoadConfig()
err := database.Open(config.DB.Path, config.Recipients.AdminEmail)
db, err := database.Open(config.DB.Path, config.Recipients.AdminEmail, config.Env == "development")
if err != nil {
logger.Error.Fatalf("Couldn't init database: %v", err)
}
defer func() {
if err := database.Close(); err != nil {
if err := database.Close(db); err != nil {
logger.Error.Fatalf("Failed to close database: %v", err)
}
}()
go server.Run()
go server.Run(db)
gracefulShutdown()
}

View File

@@ -8,6 +8,8 @@
package config
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
@@ -15,7 +17,6 @@ import (
"github.com/kelseyhightower/envconfig"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
)
@@ -61,6 +62,16 @@ type SecurityConfig struct {
Burst int `json:"Burst" default:"60" envconfig:"BURST_LIMIT"`
} `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 {
Auth AuthenticationConfig `json:"auth"`
Site SiteConfig `json:"site"`
@@ -71,6 +82,7 @@ type Config struct {
DB DatabaseConfig `json:"db"`
SMTP SMTPConfig `json:"smtp"`
Security SecurityConfig `json:"security"`
Company CompanyConfig `json:"company"`
}
var (
@@ -84,7 +96,9 @@ var (
Recipients RecipientsConfig
Env string
Security SecurityConfig
Company CompanyConfig
)
var environmentOptions map[string]bool = map[string]bool{
"development": true,
"production": true,
@@ -99,12 +113,12 @@ func LoadConfig() {
readFile(&CFG)
readEnv(&CFG)
logger.Info.Printf("Config file environment: %v", CFGPath)
csrfSecret, err := utils.GenerateRandomString(32)
csrfSecret, err := generateRandomString(32)
if err != nil {
logger.Error.Fatalf("could not generate CSRF secret: %v", err)
}
jwtSecret, err := utils.GenerateRandomString(32)
jwtSecret, err := generateRandomString(32)
if err != nil {
logger.Error.Fatalf("could not generate JWT secret: %v", err)
}
@@ -123,6 +137,7 @@ func LoadConfig() {
Security = CFG.Security
Env = CFG.Env
Site = CFG.Site
Company = CFG.Company
logger.Info.Printf("Config loaded: %#v", CFG)
}
@@ -160,3 +175,12 @@ func readEnv(cfg *Config) {
logger.Error.Fatalf("could not decode env variables: %#v", err)
}
}
func generateRandomString(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}

View File

@@ -9,12 +9,13 @@ const (
DelayedPaymentStatus
SettledPaymentStatus
AwaitingPaymentStatus
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert"
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
SupporterSubscriptionModelName = "Keins"
MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert"
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden"
SupporterSubscriptionName = "Keins"
)
var Licences = struct {
@@ -62,24 +63,28 @@ var VerificationTypes = struct {
}
var Priviliges = struct {
View int8
Create int8
Update int8
Delete int8
View int8
Create int8
Update int8
Delete int8
AccessControl int8
}{
View: 2,
Update: 4,
Create: 4,
Delete: 4,
View: 2,
Update: 4,
Create: 4,
Delete: 4,
AccessControl: 8,
}
var Roles = struct {
Opponent int8
Supporter int8
Member int8
Viewer int8
Editor int8
Admin int8
}{
Opponent: -5,
Supporter: 0,
Member: 1,
Viewer: 2,

View File

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

View File

@@ -45,12 +45,14 @@ type loginInput struct {
}
var (
Uc *UserController
Mc *MembershipController
Cc *ContactController
Uc *UserController
Mc *MembershipController
Cc *ContactController
AdminCookie *http.Cookie
MemberCookie *http.Cookie
)
func TestSuite(t *testing.T) {
func TestMain(t *testing.T) {
_ = deleteTestDB("test.db")
cwd, err := os.Getwd()
@@ -85,7 +87,8 @@ func TestSuite(t *testing.T) {
log.Fatalf("Error setting environment variable: %v", err)
}
config.LoadConfig()
if err := database.Open("test.db", config.Recipients.AdminEmail); err != nil {
db, err := database.Open("test.db", config.Recipients.AdminEmail, true)
if err != nil {
log.Fatalf("Failed to create DB: %#v", err)
}
utils.SMTPStart(Host, Port)
@@ -97,17 +100,16 @@ func TestSuite(t *testing.T) {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
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}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
userService := &services.UserService{Repo: userRepo, Licences: licenceRepo}
userService := &services.UserService{DB: db, Licences: licenceRepo}
licenceService := &services.LicenceService{Repo: licenceRepo}
Uc = &UserController{Service: userService, LicenceService: licenceService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
Mc = &MembershipController{UserController: &MockUserController{}, Service: *membershipService}
Mc = &MembershipController{UserService: userService, Service: membershipService}
Cc = &ContactController{EmailService: emailService}
if err := initSubscriptionPlans(); err != nil {
@@ -117,26 +119,36 @@ func TestSuite(t *testing.T) {
if err := initLicenceCategories(); err != nil {
log.Fatalf("Failed to init Categories: %v", err)
}
password := "securepassword"
admin := models.User{
FirstName: "Ad",
LastName: "min",
Email: "admin@example.com",
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
Company: "SampleCorp",
Phone: "+123456789",
Address: "123 Main Street",
ZipCode: "12345",
City: "SampleCity",
Status: constants.ActiveStatus,
RoleID: 8,
}
admin.SetPassword("securepassword")
database.DB.Create(&admin)
validation.SetupValidators()
FirstName: "Ad",
LastName: "min",
Email: "admin@example.com",
DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
Company: "SampleCorp",
Phone: "+123456789",
Address: "123 Main Street",
ZipCode: "12345",
City: "SampleCity",
Status: constants.ActiveStatus,
Password: password,
Notes: "",
RoleID: constants.Roles.Admin,
Consents: nil,
Verifications: nil,
Membership: nil,
BankAccount: nil,
Licence: &models.Licence{
Status: constants.UnverifiedStatus,
}}
admin.Create(db)
validation.SetupValidators(db)
t.Run("userController", func(t *testing.T) {
testUserController(t)
})
t.Run("Password_Controller", func(t *testing.T) {
})
t.Run("SQL_Injection", func(t *testing.T) {
testSQLInjectionAttempt(t)
})
@@ -152,7 +164,6 @@ func TestSuite(t *testing.T) {
t.Run("XSSAttempt", func(t *testing.T) {
testXSSAttempt(t)
})
if err := utils.SMTPStop(); err != nil {
log.Fatalf("Failed to stop SMTP Mockup Server: %#v", err)
}
@@ -192,7 +203,7 @@ func initLicenceCategories() error {
}
func initSubscriptionPlans() error {
subscriptions := []models.SubscriptionModel{
subscriptions := []models.Subscription{
{
Name: "Basic",
Details: "Test Plan",
@@ -264,24 +275,41 @@ func GetMockedFormContext(formData url.Values, url string) (*gin.Context, *httpt
func getBaseUser() models.User {
return models.User{
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Address: "Pablo Escobar Str. 4",
ZipCode: "25474",
City: "Hasloh",
Phone: "01738484993",
BankAccount: models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: models.Membership{SubscriptionModel: models.SubscriptionModel{Name: "Basic"}},
Licence: nil,
ProfilePicture: "",
Password: "passw@#$#%$!-ord123",
Company: "",
RoleID: 8,
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John",
LastName: "Doe",
Email: "john.doe@example.com",
Address: "Pablo Escobar Str. 4",
ZipCode: "25474",
City: "Hasloh",
Phone: "01738484993",
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil,
Password: "passw@#$#%$!-ord123",
Company: "",
RoleID: 1,
}
}
func getBaseSupporter() models.User {
return models.User{
DateOfBirth: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
FirstName: "John",
LastName: "Rich",
Email: "john.supporter@example.com",
Address: "Pablo Escobar Str. 4",
ZipCode: "25474",
City: "Hasloh",
Phone: "01738484993",
BankAccount: &models.BankAccount{IBAN: "DE89370400440532013000"},
Membership: &models.Membership{Subscription: models.Subscription{Name: "Basic"}},
Licence: nil,
Password: "passw@#$#%$!-ord123",
Company: "",
RoleID: 0,
}
}
func deleteTestDB(dbPath string) error {
err := os.Remove(dbPath)
if err != nil {

View File

@@ -10,7 +10,7 @@ import (
)
type LicenceController struct {
Service services.LicenceService
Service services.LicenceServiceInterface
}
func (lc *LicenceController) GetAllCategories(c *gin.Context) {

View File

@@ -48,7 +48,7 @@ func TestGetAllCategories_Success(t *testing.T) {
service := &services.LicenceService{Repo: mockRepo}
// Create controller with service
lc := &LicenceController{Service: *service}
lc := &LicenceController{Service: service}
// Setup router and request
router := gin.Default()
@@ -76,7 +76,7 @@ func TestGetAllCategories_Error(t *testing.T) {
service := &services.LicenceService{Repo: mockRepo}
// Create controller with service
lc := &LicenceController{Service: *service}
lc := &LicenceController{Service: service}
// Setup router and request
router := gin.Default()

View File

@@ -16,47 +16,40 @@ import (
)
type MembershipController struct {
Service services.MembershipService
UserController interface {
ExtractUserFromContext(*gin.Context) (*models.User, error)
}
}
type MembershipData struct {
// APIKey string `json:"api_key"`
Subscription models.SubscriptionModel `json:"subscription"`
Service services.MembershipServiceInterface
UserService services.UserServiceInterface
}
func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
var regData MembershipData
requestUser, err := mc.UserController.ExtractUserFromContext(c)
requestUser, err := mc.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in subscription registrationHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.Create) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to register subscription", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
if !requestUser.HasPrivilege(constants.Priviliges.Create) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to register subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
if err := c.ShouldBindJSON(&regData); err != nil {
var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err)
return
}
// Register Subscription
id, err := mc.Service.RegisterSubscription(&regData.Subscription)
id, err := mc.Service.RegisterSubscription(&subscription)
if err != nil {
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 {
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
}
logger.Info.Printf("registering subscription: %+v", regData)
logger.Info.Printf("registering subscription: %+v", subscription)
c.JSON(http.StatusCreated, gin.H{
"status": "success",
"id": id,
@@ -64,32 +57,31 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
}
func (mc *MembershipController) UpdateHandler(c *gin.Context) {
var regData MembershipData
requestUser, err := mc.UserController.ExtractUserFromContext(c)
requestUser, err := mc.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in subscription Updatehandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
if !requestUser.HasPrivilege(constants.Priviliges.Update) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
if err := c.ShouldBindJSON(&regData); err != nil {
var subscription models.Subscription
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err)
return
}
// update Subscription
logger.Info.Printf("Updating subscription %v", regData.Subscription.Name)
id, err := mc.Service.UpdateSubscription(&regData.Subscription)
logger.Info.Printf("Updating subscription %v", subscription.Name)
id, err := mc.Service.UpdateSubscription(&subscription)
if err != nil {
utils.HandleSubscriptionUpdateError(c, err)
return
}
logger.Info.Printf("updating subscription: %+v", regData)
c.JSON(http.StatusAccepted, gin.H{
"status": "success",
"id": id,
@@ -98,30 +90,28 @@ func (mc *MembershipController) UpdateHandler(c *gin.Context) {
func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
type deleteData struct {
Subscription struct {
ID uint `json:"id"`
Name string `json:"name"`
} `json:"subscription"`
ID uint `json:"id" binding:"required,numeric,safe_content"`
Name string `json:"name" binding:"required,safe_content"`
}
var data deleteData
requestUser, err := mc.UserController.ExtractUserFromContext(c)
var subscription deleteData
requestUser, err := mc.UserService.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in subscription deleteSubscription", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.Delete) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
if !requestUser.HasPrivilege(constants.Priviliges.Delete) {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update subscription", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
if err := c.ShouldBindJSON(&data); err != nil {
if err := c.ShouldBindJSON(&subscription); err != nil {
utils.HandleValidationError(c, err)
return
}
if err := mc.Service.DeleteSubscription(&data.Subscription.ID, &data.Subscription.Name); err != nil {
if err := mc.Service.DeleteSubscription(&subscription.ID, &subscription.Name); err != nil {
utils.HandleSubscriptionDeleteError(c, err)
return
}
@@ -132,7 +122,7 @@ func (mc *MembershipController) DeleteSubscription(c *gin.Context) {
func (mc *MembershipController) GetSubscriptions(c *gin.Context) {
subscriptions, err := mc.Service.GetSubscriptions(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
}

View File

@@ -6,7 +6,6 @@ import (
"net/http/httptest"
"testing"
"GoMembership/internal/constants"
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/pkg/logger"
@@ -15,6 +14,7 @@ import (
)
type RegisterSubscriptionTest struct {
SetupCookie func(r *http.Request)
WantDBData map[string]interface{}
Input string
Name string
@@ -23,6 +23,7 @@ type RegisterSubscriptionTest struct {
}
type UpdateSubscriptionTest struct {
SetupCookie func(r *http.Request)
WantDBData map[string]interface{}
Input string
Name string
@@ -31,6 +32,7 @@ type UpdateSubscriptionTest struct {
}
type DeleteSubscriptionTest struct {
SetupCookie func(r *http.Request)
WantDBData map[string]interface{}
Input string
Name string
@@ -38,29 +40,8 @@ type DeleteSubscriptionTest struct {
Assert bool
}
type MockUserController struct {
UserController // Embed the UserController
}
func (m *MockUserController) ExtractUserFromContext(c *gin.Context) (*models.User, error) {
return &models.User{
ID: 1,
FirstName: "Admin",
LastName: "User",
Email: "admin@test.com",
RoleID: constants.Roles.Admin,
}, nil
}
func setupMockAuth() {
// Create and assign the mock controller
mockController := &MockUserController{}
Mc.UserController = mockController
}
func testMembershipController(t *testing.T) {
setupMockAuth()
tests := getSubscriptionRegistrationData()
for _, tt := range tests {
logger.Error.Print("==============================================================")
@@ -101,6 +82,7 @@ func (rt *RegisterSubscriptionTest) SetupContext() (*gin.Context, *httptest.Resp
}
func (rt *RegisterSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
rt.SetupCookie(c.Request)
Mc.RegisterSubscription(c)
}
@@ -131,6 +113,7 @@ func (ut *UpdateSubscriptionTest) SetupContext() (*gin.Context, *httptest.Respon
}
func (ut *UpdateSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
ut.SetupCookie(c.Request)
Mc.UpdateHandler(c)
}
@@ -150,6 +133,7 @@ func (dt *DeleteSubscriptionTest) SetupContext() (*gin.Context, *httptest.Respon
}
func (dt *DeleteSubscriptionTest) RunHandler(c *gin.Context, router *gin.Engine) {
dt.SetupCookie(c.Request)
Mc.DeleteSubscription(c)
}
@@ -164,18 +148,16 @@ func (dt *DeleteSubscriptionTest) ValidateResult() error {
return validateSubscription(dt.Assert, dt.WantDBData)
}
func getBaseSubscription() MembershipData {
return MembershipData{
// APIKey: config.Auth.APIKEY,
Subscription: models.SubscriptionModel{
Name: "Premium",
Details: "A subscription detail",
MonthlyFee: 12.0,
HourlyRate: 14.0,
},
func getBaseSubscription() models.Subscription {
return models.Subscription{
Name: "Premium",
Details: "A subscription detail",
MonthlyFee: 12.0,
HourlyRate: 14.0,
}
}
func customizeSubscription(customize func(MembershipData) MembershipData) MembershipData {
func customizeSubscription(customize func(models.Subscription) models.Subscription) models.Subscription {
subscription := getBaseSubscription()
return customize(subscription)
}
@@ -183,61 +165,95 @@ func customizeSubscription(customize func(MembershipData) MembershipData) Member
func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
return []RegisterSubscriptionTest{
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Missing details should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Just a Subscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Details = ""
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = ""
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Missing model name should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": ""},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = ""
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = ""
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Negative monthly fee should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {
sub.Subscription.MonthlyFee = -10.0
Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.MonthlyFee = -10.0
return sub
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Negative hourly rate should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {
sub.Subscription.HourlyRate = -1.0
Input: GenerateInputJSON(customizeSubscription(func(sub models.Subscription) models.Subscription {
sub.HourlyRate = -1.0
return sub
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
Name: "correct entry but not authorized",
WantResponse: http.StatusUnauthorized,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "correct entry should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Duplicate subscription name should fail",
WantResponse: http.StatusConflict,
WantDBData: map[string]interface{}{"name": "Premium"},
@@ -250,82 +266,120 @@ func getSubscriptionRegistrationData() []RegisterSubscriptionTest {
func getSubscriptionUpdateData() []UpdateSubscriptionTest {
return []UpdateSubscriptionTest{
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Modified Monthly Fee, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium", "monthly_fee": "12"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.MonthlyFee = 123.0
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.MonthlyFee = 123.0
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Missing ID, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.ID = 0
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.ID = 0
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Modified Hourly Rate, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium", "hourly_rate": "14"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.HourlyRate = 3254.0
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.HourlyRate = 3254.0
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "IncludedPerYear changed, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium", "included_per_year": "0"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.IncludedPerYear = 9873.0
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerYear = 9873.0
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "IncludedPerMonth changed, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium", "included_per_month": "1"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.IncludedPerMonth = 23415.0
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.IncludedPerMonth = 23415.0
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Update non-existent subscription should fail",
WantResponse: http.StatusNotFound,
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = "NonExistentSubscription"
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription"
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
Name: "Correct Update but unauthorized",
WantResponse: http.StatusUnauthorized,
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Correct Update should pass",
WantResponse: http.StatusAccepted,
WantDBData: map[string]interface{}{"name": "Premium", "details": "Altered Details"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Details = "Altered Details"
subscription.Subscription.Conditions = "Some Condition"
subscription.Subscription.IncludedPerYear = 0
subscription.Subscription.IncludedPerMonth = 1
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Details = "Altered Details"
subscription.Conditions = "Some Condition"
subscription.IncludedPerYear = 0
subscription.IncludedPerMonth = 1
return subscription
})),
},
@@ -334,58 +388,84 @@ func getSubscriptionUpdateData() []UpdateSubscriptionTest {
func getSubscriptionDeleteData() []DeleteSubscriptionTest {
var premiumSub, basicSub models.SubscriptionModel
var premiumSub, basicSub models.Subscription
database.DB.Where("name = ?", "Premium").First(&premiumSub)
database.DB.Where("name = ?", "Basic").First(&basicSub)
logger.Error.Printf("premiumSub.ID: %v", premiumSub.ID)
logger.Error.Printf("basicSub.ID: %v", basicSub.ID)
return []DeleteSubscriptionTest{
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Delete non-existent subscription should fail",
WantResponse: http.StatusNotFound,
WantDBData: map[string]interface{}{"name": "NonExistentSubscription"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = "NonExistentSubscription"
subscription.Subscription.ID = basicSub.ID
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "NonExistentSubscription"
subscription.ID = basicSub.ID
logger.Error.Printf("subscription to delete: %#v", subscription)
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Delete subscription without name should fail",
WantResponse: http.StatusExpectationFailed,
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": ""},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = ""
subscription.Subscription.ID = basicSub.ID
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = ""
subscription.ID = basicSub.ID
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Delete subscription with users should fail",
WantResponse: http.StatusExpectationFailed,
WantDBData: map[string]interface{}{"name": "Basic"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = "Basic"
subscription.Subscription.ID = basicSub.ID
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Basic"
subscription.ID = basicSub.ID
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(MemberCookie)
},
Name: "Delete valid subscription should succeed",
WantResponse: http.StatusUnauthorized,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: true,
Input: GenerateInputJSON(
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium"
subscription.ID = premiumSub.ID
return subscription
})),
},
{
SetupCookie: func(req *http.Request) {
req.AddCookie(AdminCookie)
},
Name: "Delete valid subscription should succeed",
WantResponse: http.StatusOK,
WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false,
Input: GenerateInputJSON(
customizeSubscription(func(subscription MembershipData) MembershipData {
subscription.Subscription.Name = "Premium"
subscription.Subscription.ID = premiumSub.ID
customizeSubscription(func(subscription models.Subscription) models.Subscription {
subscription.Name = "Premium"
subscription.ID = premiumSub.ID
return subscription
})),
},

View File

@@ -12,6 +12,59 @@ import (
"github.com/go-playground/validator/v10"
)
func (uc *UserController) CreatePasswordHandler(c *gin.Context) {
requestUser, err := uc.Service.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Couldn't get User from Request Context", http.StatusBadRequest, errors.Responses.Fields.General, errors.Responses.Keys.NoAuthToken)
return
}
if !requestUser.IsAdmin() {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Requesting user not authorized to grant user access", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
// Expected data from the user
var input struct {
User struct {
ID uint `json:"id" binding:"required,numeric"`
} `json:"user"`
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
// find user
user, err := uc.Service.FromID(&input.User.ID)
if err != nil {
utils.RespondWithError(c, err, "couldn't get user by id", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)
return
}
// Deactivate user and reset Verification
user.Status = constants.DisabledStatus
v, err := user.SetVerification(constants.VerificationTypes.Password)
if err != nil {
utils.RespondWithError(c, err, "couldn't set verification", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
return
}
if _, err := uc.Service.Update(user); err != nil {
utils.RespondWithError(c, err, "Couldn't update user in createPasswordHandler", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
// send email
if err := uc.EmailService.SendGrantBackendAccessEmail(user, &v.VerificationToken); err != nil {
utils.RespondWithError(c, err, "Couldn't send grant backend access email", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusAccepted, gin.H{
"message": "password_change_requested",
})
}
func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) {
// Expected data from the user
@@ -24,27 +77,30 @@ func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) {
return
}
// find user
db_user, err := uc.Service.GetUserByEmail(input.Email)
user, err := uc.Service.FromEmail(&input.Email)
if err != nil {
utils.RespondWithError(c, err, "couldn't get user by email", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)
return
}
// check if user may change the password
if db_user.Status <= constants.DisabledStatus {
utils.RespondWithError(c, errors.ErrNotAuthorized, "User password change request denied, user is disabled", http.StatusForbidden, errors.Responses.Fields.Login, errors.Responses.Keys.UserDisabled)
if !user.IsVerified() {
utils.RespondWithError(c, errors.ErrNotAuthorized, "User password change request denied, user is not verified or disabled", http.StatusForbidden, errors.Responses.Fields.Login, errors.Responses.Keys.UserDisabled)
return
}
// create token
token, err := uc.Service.HandlePasswordChangeRequest(db_user)
user.Status = constants.DisabledStatus
v, err := user.SetVerification(constants.VerificationTypes.Password)
if err != nil {
utils.RespondWithError(c, err, "couldn't handle password change request", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
utils.RespondWithError(c, err, "couldn't set verification", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
return
}
if _, err := uc.Service.Update(user); err != nil {
utils.RespondWithError(c, err, "Couldn't update user in createPasswordHandler", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
// send email
if err := uc.EmailService.SendChangePasswordEmail(db_user, &token); err != nil {
if err := uc.EmailService.SendChangePasswordEmail(user, &v.VerificationToken); err != nil {
utils.RespondWithError(c, err, "Couldn't send change password email", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
@@ -65,27 +121,24 @@ func (uc *UserController) ChangePassword(c *gin.Context) {
utils.RespondWithError(c, err, "Invalid user ID", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.InvalidUserID)
return
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
verification, err := uc.Service.VerifyUser(&input.Token, &constants.VerificationTypes.Password)
if err != nil || uint(userIDint) != verification.UserID {
utils.HandleVerifyUserError(c, err)
return
}
user, err := uc.Service.GetUserByID(verification.UserID)
userID := uint(userIDint)
user, err := uc.Service.FromID(&userID)
if err != nil {
utils.RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.UserNotFoundWrongPassword)
return
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
err = user.Verify(input.Token, constants.VerificationTypes.Password)
if err != nil {
utils.RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
user.Status = constants.ActiveStatus
user.Verification = *verification
user.ID = verification.UserID
user.Password = input.Password
// Get Gin's binding validator engine with all registered validators
@@ -96,7 +149,7 @@ func (uc *UserController) ChangePassword(c *gin.Context) {
utils.HandleValidationError(c, err)
return
}
_, err = uc.Service.UpdateUser(user)
_, err = uc.Service.Update(user)
if err != nil {
utils.HandleUserUpdateError(c, err)
return

View File

@@ -0,0 +1,211 @@
package controllers
import (
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
type TestContext struct {
router *gin.Engine
response *httptest.ResponseRecorder
user *models.User
}
func setupTestContext() (*TestContext, error) {
testEmail := "john.doe@example.com"
user, err := Uc.Service.FromEmail(&testEmail)
if err != nil {
logger.Error.Printf("error fetching user: %#v", err)
return nil, err
}
return &TestContext{
router: gin.Default(),
response: httptest.NewRecorder(),
user: user,
}, nil
}
func testCreatePasswordHandler(t *testing.T) {
invalidCookie := http.Cookie{
Name: "jwt",
Value: "invalid.token.here",
}
tc, err := setupTestContext()
if err != nil {
t.Fatal(err)
}
tc.router.POST("/password", Uc.CreatePasswordHandler)
requestBody := map[string]interface{}{
"user": map[string]interface{}{
"id": tc.user.ID,
},
}
body, _ := json.Marshal(requestBody)
t.Run("successful password creation request from admin", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body))
req.AddCookie(AdminCookie)
tc.router.ServeHTTP(tc.response, req)
assert.Equal(t, http.StatusAccepted, tc.response.Code)
assert.JSONEq(t, `{"message":"password_change_requested"}`, tc.response.Body.String())
err = checkEmailDelivery(tc.user, true)
assert.NoError(t, err)
})
// test token and password change
testChangePassword(t, tc)
logger.Error.Printf("__________END RESULTS---------")
tc.response = httptest.NewRecorder()
t.Run("failed password creation request from member", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body))
req.AddCookie(MemberCookie)
tc.router.ServeHTTP(tc.response, req)
logger.Error.Printf("Test results for %#v", t.Name())
assert.Equal(t, http.StatusUnauthorized, tc.response.Code)
assert.JSONEq(t, `{"errors":[{"field":"user.user","key":"server.error.unauthorized"}]}`, tc.response.Body.String())
err = checkEmailDelivery(tc.user, false)
assert.NoError(t, err)
})
logger.Error.Printf("__________END RESULTS---------")
tc.response = httptest.NewRecorder()
t.Run("failed password creation request for invalid cookie", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body))
req.AddCookie(&invalidCookie)
tc.router.ServeHTTP(tc.response, req)
logger.Error.Printf("Test results for %#v", t.Name())
assert.Equal(t, http.StatusBadRequest, tc.response.Code)
assert.Contains(t, tc.response.Body.String(), `server.error.no_auth_token`)
err = checkEmailDelivery(tc.user, false)
assert.NoError(t, err)
})
logger.Error.Printf("__________END RESULTS---------")
tc.response = httptest.NewRecorder()
}
func testChangePassword(t *testing.T, tc *TestContext) {
var verification models.Verification
result := database.DB.Where("user_id = ? AND type = ?", tc.user.ID, constants.VerificationTypes.Password).First(&verification)
assert.NoError(t, result.Error)
requestBody := map[string]interface{}{
"password": "new-pas9247A@!sword",
"token": verification.VerificationToken,
}
body, _ := json.Marshal(requestBody)
tc.router.PUT("/users/:id/password", Uc.ChangePassword)
tc.response = httptest.NewRecorder()
t.Run("valid password change", func(t *testing.T) {
req, _ := http.NewRequest("PUT", fmt.Sprintf("/users/%v/password", tc.user.ID), bytes.NewBuffer(body))
tc.router.ServeHTTP(tc.response, req)
assert.Equal(t, http.StatusOK, tc.response.Code)
assert.JSONEq(t, `{"message":"password_changed"}`, tc.response.Body.String())
})
tc.response = httptest.NewRecorder()
//User should now be deactivated. Should lack privileges.
t.Run("user lacks privileges", func(t *testing.T) {
req, _ := http.NewRequest("POST", "/password", bytes.NewBuffer(body))
tc.router.ServeHTTP(tc.response, req)
logger.Error.Printf("Test results for %#v", t.Name())
assert.Equal(t, http.StatusBadRequest, tc.response.Code)
assert.Contains(t, tc.response.Body.String(), `server.error.no_auth_token`)
})
logger.Error.Printf("__________END RESULTS---------")
t.Run("invalid user ID", func(t *testing.T) {
req, _ := http.NewRequest("PUT", "/users/invalid/password", nil)
tc.router.ServeHTTP(tc.response, req)
assert.Equal(t, http.StatusBadRequest, tc.response.Code)
})
t.Run("non existant user ID", func(t *testing.T) {
req, _ := http.NewRequest("PUT", "/users/999/password", nil)
tc.router.ServeHTTP(tc.response, req)
assert.Equal(t, http.StatusBadRequest, tc.response.Code)
})
}
func checkEmailDelivery(user *models.User, wantsSuccess bool) error {
//check for email delivery
messages := utils.SMTPGetMessages()
for _, message := range messages {
mail, err := utils.DecodeMail(message.MsgRequest())
if err != nil {
logger.Error.Printf("Error in validateUser: %#v", err)
return err
}
if strings.Contains(mail.Subject, constants.MailChangePasswordSubject) || strings.Contains(mail.Subject, constants.MailGrantBackendAccessSubject) {
if err := checkPasswordMail(mail, user); err != nil && wantsSuccess {
logger.Error.Printf("Error in checkEmailDelivery mail: %#v", err)
return err
}
} else {
return fmt.Errorf("Subject not expected: %v", mail.Subject)
}
}
return nil
}
func checkPasswordMail(message *utils.Email, user *models.User) error {
var verification models.Verification
result := database.DB.Where("user_id = ? AND type = ?", user.ID, constants.VerificationTypes.Password).First(&verification)
if result.Error != nil {
return result.Error
}
logger.Error.Printf("user id: %v token: %#v", user.ID, verification.VerificationToken)
re := regexp.MustCompile(`"([^"]*token[^"]*)"`)
// Find the matching URL in the email content
match := re.FindStringSubmatch(message.Body)
if len(match) == 0 {
return fmt.Errorf("No change Password link found in email body: %#v", message.Body)
}
tokenURL, err := url.QueryUnescape(match[1])
if err != nil {
return fmt.Errorf("Error decoding URL: %v", err)
}
logger.Info.Printf("TokenURL: %#v", tokenURL)
if !strings.Contains(message.To, user.Email) {
return fmt.Errorf("Password Information didn't reach user! Recipient was: %v instead of %v", message.To, user.Email)
}
if !strings.Contains(message.From, config.SMTP.User) {
return fmt.Errorf("Password Information was sent from unexpected address! Sender was: %v instead of %v", message.From, config.SMTP.User)
}
//Check if all the relevant data has been passed to the mail.
if !strings.Contains(message.Body, user.FirstName+" "+user.LastName) {
return fmt.Errorf("User first and last name(%v) has not been rendered in password mail.", user.FirstName+" "+user.LastName)
}
if !strings.Contains(message.Body, verification.VerificationToken) {
return fmt.Errorf("Token(%v) has not been rendered in password mail.", verification.VerificationToken)
}
if strings.Trim(tokenURL, " ") != fmt.Sprintf("%v%v/auth/password/change/%v?token=%v", config.Site.BaseURL, config.Site.FrontendPath, user.ID, verification.VerificationToken) {
return fmt.Errorf("Token has not been rendered correctly in password mail: %v%v/auth/password/change/%v?token=%v", config.Site.BaseURL, config.Site.FrontendPath, user.ID, verification.VerificationToken)
}
return nil
}

View File

@@ -9,11 +9,14 @@ import (
"GoMembership/internal/utils"
"GoMembership/internal/validation"
"fmt"
"strconv"
"strings"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
@@ -25,7 +28,7 @@ type UserController struct {
ConsentService services.ConsentServiceInterface
BankAccountService services.BankAccountServiceInterface
MembershipService services.MembershipServiceInterface
LicenceService services.LicenceInterface
LicenceService services.LicenceServiceInterface
}
type RegistrationData struct {
@@ -33,7 +36,7 @@ type RegistrationData struct {
}
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
requestUser, err := uc.ExtractUserFromContext(c)
requestUser, err := uc.Service.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in CurrentUserHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
@@ -46,19 +49,20 @@ func (uc *UserController) CurrentUserHandler(c *gin.Context) {
func (uc *UserController) GetAllUsers(c *gin.Context) {
requestUser, err := uc.ExtractUserFromContext(c)
requestUser, err := uc.Service.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in UpdateHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.View) {
if !requestUser.HasPrivilege(constants.Priviliges.View) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to handle all users. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.View), http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
users, err := uc.Service.GetUsers(nil)
if err != nil {
utils.RespondWithError(c, err, "Error getting users in GetAllUsers", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
utils.RespondWithError(c, err, "Error getting all users", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
return
}
@@ -69,48 +73,45 @@ func (uc *UserController) GetAllUsers(c *gin.Context) {
for i, user := range *users {
safeUsers[i] = user.Safe()
}
c.JSON(http.StatusOK, gin.H{
"users": users,
"users": safeUsers,
})
}
func (uc *UserController) UpdateHandler(c *gin.Context) {
// 1. Extract and validate the user ID from the route
requestUser, err := uc.ExtractUserFromContext(c)
requestUser, err := uc.Service.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in UpdateHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
var user models.User
var updateData RegistrationData
if err := c.ShouldBindJSON(&updateData); err != nil {
if updateData.User.Password != "" {
logger.Error.Printf("u.password: %#v", updateData.User.Password)
}
utils.HandleValidationError(c, err)
return
}
user = updateData.User
user := updateData.User
if !utils.HasPrivilige(requestUser, constants.Priviliges.Update) && user.ID != requestUser.ID {
if !requestUser.HasPrivilege(constants.Priviliges.Update) && user.ID != requestUser.ID {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to update user", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
existingUser, err := uc.Service.GetUserByID(user.ID)
if err != nil {
utils.RespondWithError(c, err, "Error finding an existing user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)
return
}
user.MembershipID = existingUser.MembershipID
user.Membership.ID = existingUser.Membership.ID
if existingUser.Licence != nil {
user.Licence.ID = existingUser.Licence.ID
}
user.LicenceID = existingUser.LicenceID
user.BankAccount.ID = existingUser.BankAccount.ID
user.BankAccountID = existingUser.BankAccountID
if requestUser.RoleID <= constants.Priviliges.View {
if requestUser.IsMember() {
existingUser, err := uc.Service.FromID(&user.ID)
if err != nil {
utils.RespondWithError(c, err, "Error finding an existing user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)
return
}
// deleting existing Users Password to prevent it from being recognized as changed in any case. (Incoming Password is empty if not changed)
existingUser.Password = ""
if err := utils.FilterAllowedStructFields(&user, existingUser, constants.MemberUpdateFields, ""); err != nil {
if err := validation.FilterAllowedStructFields(&user, existingUser, constants.MemberUpdateFields, ""); err != nil {
if err.Error() == "Not authorized" {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Trying to update unauthorized fields", http.StatusUnauthorized, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
} else {
@@ -120,30 +121,27 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
}
}
updatedUser, err := uc.Service.UpdateUser(&user)
updatedUser, err := uc.Service.Update(&user)
if err != nil {
utils.HandleUserUpdateError(c, err)
return
}
logger.Info.Printf("User %d updated successfully by user %d", updatedUser.ID, requestUser.ID)
logger.Info.Printf("User %v updated successfully by user %v", updatedUser.Email, requestUser.Email)
c.JSON(http.StatusAccepted, gin.H{"message": "User updated successfully", "user": updatedUser.Safe()})
}
func (uc *UserController) DeleteUser(c *gin.Context) {
requestUser, err := uc.ExtractUserFromContext(c)
requestUser, err := uc.Service.FromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in DeleteUser", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
type deleteData struct {
User struct {
ID uint `json:"id"`
LastName string `json:"last_name"`
} `json:"user"`
ID uint `json:"id" binding:"required,numeric"`
}
var data deleteData
@@ -152,13 +150,13 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.Delete) && data.User.ID != requestUser.ID {
if !requestUser.HasPrivilege(constants.Priviliges.Delete) && data.ID != requestUser.ID {
utils.RespondWithError(c, errors.ErrNotAuthorized, "Not allowed to delete user", http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
logger.Error.Printf("Deleting user: %v", data.User)
if err := uc.Service.DeleteUser(data.User.LastName, data.User.ID); err != nil {
logger.Error.Printf("Deleting user: %v", data)
if err := uc.Service.Delete(&data.ID); err != nil {
utils.HandleDeleteUserError(c, err)
return
}
@@ -166,24 +164,6 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
func (uc *UserController) ExtractUserFromContext(c *gin.Context) (*models.User, error) {
tokenString, err := c.Cookie("jwt")
if err != nil {
return nil, err
}
_, claims, err := middlewares.ExtractContentFrom(tokenString)
if err != nil {
return nil, err
}
jwtUserID := uint((*claims)["user_id"].(float64))
user, err := uc.Service.GetUserByID(jwtUserID)
if err != nil {
return nil, err
}
return user, nil
}
func (uc *UserController) LogoutHandler(c *gin.Context) {
tokenString, err := c.Cookie("jwt")
if err != nil {
@@ -207,7 +187,7 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
return
}
user, err := uc.Service.GetUserByEmail(input.Email)
user, err := uc.Service.FromEmail(&input.Email)
if err != nil {
utils.RespondWithError(c, err, "Login Error; user not found", http.StatusNotFound,
errors.Responses.Fields.Login,
@@ -215,9 +195,9 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
return
}
if user.Status <= constants.DisabledStatus {
utils.RespondWithError(c, fmt.Errorf("User banned from login %v %v", user.FirstName, user.LastName),
"Login Error; user is disabled",
if !user.IsVerified() {
utils.RespondWithError(c, fmt.Errorf("User banned from login or not verified %v %v", user.FirstName, user.LastName),
"Login Error; user is disabled or not verified",
http.StatusNotAcceptable,
errors.Responses.Fields.Login,
errors.Responses.Keys.UserDisabled)
@@ -238,8 +218,10 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
return
}
logger.Error.Printf("jwtsecret: %v", config.Auth.JWTSecret)
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
// "user_id": user.ID,
// "role_id": user.RoleID,
claims := map[string]interface{}{"user_id": user.ID, "role_id": user.RoleID}
token, err := middlewares.GenerateToken(&config.Auth.JWTSecret, claims, "")
if err != nil {
utils.RespondWithError(c, err, "Error generating token in LoginHandler", http.StatusInternalServerError, errors.Responses.Fields.Login, errors.Responses.Keys.JwtGenerationFailed)
return
@@ -256,35 +238,38 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
func (uc *UserController) RegisterUser(c *gin.Context) {
var regData RegistrationData
logger.Error.Printf("registering user...")
if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership)
logger.Error.Printf("Failed initial Binding: %#v", &regData.User.Membership.Subscription)
utils.HandleValidationError(c, err)
return
}
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 {
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
}
regData.User.Membership.SubscriptionModel = *selectedModel
if selectedModel.RequiredMembershipField != "" {
if err := validation.CheckParentMembershipID(regData.User.Membership); err != nil {
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't check parent membership id", http.StatusBadRequest, errors.Responses.Fields.ParentMemberShipID, errors.Responses.Keys.NotFound)
return
}
regData.User.Membership.Subscription = *selectedModel
// Get Gin's binding validator engine with all registered validators
validate := binding.Validator.Engine().(*validator.Validate)
// Validate the populated user struct
if err := validate.Struct(regData.User); err != nil {
utils.HandleValidationError(c, err)
return
}
if regData.User.Membership.SubscriptionModel.Name == constants.SupporterSubscriptionModelName {
if regData.User.Membership.Subscription.Name == constants.SupporterSubscriptionName {
regData.User.RoleID = constants.Roles.Supporter
} else {
regData.User.RoleID = constants.Roles.Member
}
// Register User
id, token, err := uc.Service.RegisterUser(&regData.User)
id, token, err := uc.Service.Register(&regData.User)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
if strings.Contains(err.Error(), "UNIQUE constraint failed:") {
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't register user", http.StatusConflict, errors.Responses.Fields.Email, errors.Responses.Keys.Duplicate)
} else {
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't register user", http.StatusConflict, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
@@ -294,7 +279,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
regData.User.ID = id
// if this is a supporter don't send mails and he never did give any consent. So stop here
if regData.User.RoleID == constants.Roles.Supporter {
if regData.User.IsSupporter() {
c.JSON(http.StatusCreated, gin.H{
"message": "Supporter Registration successuful",
@@ -310,14 +295,17 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
LastName: regData.User.LastName,
Email: regData.User.Email,
ConsentType: "TermsOfService",
UserID: &regData.User.ID,
},
{
FirstName: regData.User.FirstName,
LastName: regData.User.LastName,
Email: regData.User.Email,
ConsentType: "Privacy",
UserID: &regData.User.ID,
},
}
for _, consent := range consents {
_, err = uc.ConsentService.RegisterConsent(&consent)
if err != nil {
@@ -326,6 +314,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
}
}
logger.Error.Printf("Sending Verification mail to user with id: %#v", id)
// Send notifications
if err := uc.EmailService.SendVerificationEmail(&regData.User, &token); err != nil {
utils.RespondWithError(c, err, "Error in RegisterUser, couldn't send verification email", http.StatusInternalServerError, errors.Responses.Fields.Email, errors.Responses.Keys.UndeliveredVerificationMail)
@@ -351,26 +340,36 @@ func (uc *UserController) VerifyMailHandler(c *gin.Context) {
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Missing token"})
return
}
verification, err := uc.Service.VerifyUser(&token, &constants.VerificationTypes.Email)
userIDint, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't verify user"})
logger.Error.Println("Missing user ID to verify mail")
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Missing user"})
return
}
user, err := uc.Service.GetUserByID(verification.UserID)
userID := uint(userIDint)
user, err := uc.Service.FromID(&userID)
if err != nil {
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Internal server error, couldn't verify user"})
logger.Error.Printf("Couldn't find user in verifyMailHandler: %#v", err)
c.HTML(http.StatusBadRequest, "verification_error.html", gin.H{"ErrorMessage": "Couldn't find user"})
return
}
err = user.Verify(token, constants.VerificationTypes.Email)
if err != nil {
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"})
return
}
user.Status = constants.VerifiedStatus
user.Verification = *verification
user.ID = verification.UserID
user.Password = ""
uc.Service.UpdateUser(user)
logger.Info.Printf("Verified User: %#v", user.Email)
updatedUser, err := uc.Service.Update(user)
if err != nil {
logger.Error.Printf("Failed to update user(%v) after verification: %v", user.Email, err)
c.HTML(http.StatusInternalServerError, "verification_error.html", gin.H{"ErrorMessage": "Internal server error, couldn't verify user"})
return
}
logger.Info.Printf("Verified User: %#v", updatedUser.Email)
uc.EmailService.SendWelcomeEmail(user)
c.HTML(http.StatusOK, "verification_success.html", gin.H{"FirstName": user.FirstName})

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
@@ -75,9 +76,8 @@ func testUserController(t *testing.T) {
}
// activate user for login
database.DB.Model(&models.User{}).Where("email = ?", "john.doe@example.com").Update("status", constants.ActiveStatus)
loginEmail, loginCookie := testLoginHandler(t)
logoutCookie := testCurrentUserHandler(t, loginEmail, loginCookie)
loginEmail := testLoginHandler(t)
testCurrentUserHandler(t, loginEmail)
// creating a admin cookie
c, w, _ := GetMockedJSONContext([]byte(`{
"email": "admin@example.com",
@@ -91,27 +91,27 @@ func testUserController(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "Login successful", response["message"])
var adminCookie http.Cookie
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
adminCookie = *cookie
AdminCookie = cookie
tokenString := adminCookie.Value
tokenString := AdminCookie.Value
_, claims, err := middlewares.ExtractContentFrom(tokenString)
assert.NoError(t, err, "FAiled getting cookie string")
assert.NoError(t, err, "Failed getting cookie string")
jwtUserID := uint((*claims)["user_id"].(float64))
user, err := Uc.Service.GetUserByID(jwtUserID)
assert.NoError(t, err, "FAiled getting cookie string")
user, err := Uc.Service.FromID(&jwtUserID)
assert.NoError(t, err, "Failed getting cookie string")
logger.Error.Printf("ADMIN USER: %#v", user)
break
}
}
assert.NotEmpty(t, adminCookie)
testUpdateUser(t, loginCookie, adminCookie)
testLogoutHandler(t, logoutCookie)
assert.NotEmpty(t, AdminCookie)
testUpdateUser(t)
testLogoutHandler(t)
testCreatePasswordHandler(t)
}
func testLogoutHandler(t *testing.T, loginCookie http.Cookie) {
func testLogoutHandler(t *testing.T) {
tests := []struct {
name string
@@ -121,7 +121,7 @@ func testLogoutHandler(t *testing.T, loginCookie http.Cookie) {
{
name: "Logout with valid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
expectedStatus: http.StatusOK,
},
@@ -179,9 +179,8 @@ func testLogoutHandler(t *testing.T, loginCookie http.Cookie) {
}
}
func testLoginHandler(t *testing.T) (string, http.Cookie) {
func testLoginHandler(t *testing.T) string {
// This test should run after the user registration test
var loginCookie http.Cookie
var loginInput loginInput
t.Run("LoginHandler", func(t *testing.T) {
// Test cases
@@ -243,14 +242,14 @@ func testLoginHandler(t *testing.T) (string, http.Cookie) {
assert.Equal(t, "Login successful", response["message"])
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
loginCookie = *cookie
MemberCookie = cookie
// tokenString := loginCookie.Value
// _, claims, err := middlewares.ExtractContentFrom(tokenString)
// assert.NoError(t, err, "FAiled getting cookie string")
// jwtUserID := uint((*claims)["user_id"].(float64))
// user, err := Uc.Service.GetUserByID(jwtUserID)
// assert.NoError(t, err, "FAiled getting cookie string")
tokenString := cookie.Value
_, claims, err := middlewares.ExtractContentFrom(tokenString)
assert.NoError(t, err, "FAiled getting cookie string")
jwtUserID := uint((*claims)["user_id"].(float64))
_, err = Uc.Service.FromID(&jwtUserID)
assert.NoError(t, err, "FAiled getting cookie string")
// logger.Error.Printf("cookie user: %#v", user)
err = json.Unmarshal([]byte(tt.input), &loginInput)
@@ -259,7 +258,7 @@ func testLoginHandler(t *testing.T) (string, http.Cookie) {
break
}
}
assert.NotEmpty(t, loginCookie)
assert.NotEmpty(t, MemberCookie)
} else {
assert.Contains(t, response, "errors")
assert.NotEmpty(t, response["errors"])
@@ -268,10 +267,10 @@ func testLoginHandler(t *testing.T) (string, http.Cookie) {
}
})
return loginInput.Email, loginCookie
return loginInput.Email
}
func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Cookie) http.Cookie {
func testCurrentUserHandler(t *testing.T, loginEmail string) http.Cookie {
// This test should run after the user login test
invalidCookie := http.Cookie{
Name: "jwt",
@@ -288,7 +287,7 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
{
name: "With valid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
expectedUserMail: loginEmail,
expectedStatus: http.StatusOK,
@@ -352,8 +351,8 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
if tt.expectedStatus == http.StatusOK {
var response struct {
User models.User `json:"user"`
Subscriptions []models.SubscriptionModel `json:"subscriptions"`
User models.User `json:"user"`
Subscriptions []models.Subscription `json:"subscriptions"`
}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
@@ -368,7 +367,7 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
}
if tt.expectNewCookie {
assert.NotNil(t, newCookie, "New cookie should be set for expired token")
assert.NotEqual(t, loginCookie.Value, newCookie.Value, "Cookie value should be different")
assert.NotEqual(t, MemberCookie.Value, newCookie.Value, "Cookie value should be different")
assert.True(t, newCookie.MaxAge > 0, "New cookie should not be expired")
} else {
assert.Nil(t, newCookie, "No new cookie should be set for non-expired token")
@@ -394,7 +393,7 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
})
}
return loginCookie
return *MemberCookie
}
func validateUser(assert bool, wantDBData map[string]interface{}) error {
@@ -402,23 +401,30 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error {
if err != nil {
return fmt.Errorf("Error in database ops: %#v", err)
}
if assert != (len(*users) != 0) {
return fmt.Errorf("User entry query didn't met expectation: %v != %#v", assert, *users)
}
if assert {
user := (*users)[0]
// 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)
} 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
expected := user.GenerateMandateReference()
expected := user.BankAccount.GenerateMandateReference(user.ID)
if !strings.HasPrefix(user.BankAccount.MandateReference, expected) {
return fmt.Errorf("Mandate reference is invalid. Expected: %s, Got: %s", expected, user.BankAccount.MandateReference)
}
// Supoorter don't get mails
if user.IsSupporter() {
return nil
}
//check for email delivery
messages := utils.SMTPGetMessages()
for _, message := range messages {
@@ -453,18 +459,18 @@ func validateUser(assert bool, wantDBData map[string]interface{}) error {
return nil
}
func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cookie) {
func testUpdateUser(t *testing.T) {
invalidCookie := http.Cookie{
Name: "jwt",
Value: "invalid.token.here",
}
// Get the user we just created
users, err := Uc.Service.GetUsers(map[string]interface{}{"email": "john.doe@example.com"})
if err != nil || len(*users) == 0 {
johnsMail := "john.doe@example.com"
user, err := Uc.Service.FromEmail(&johnsMail)
if err != nil {
t.Fatalf("Failed to get test user: %v", err)
}
user := (*users)[0]
if user.Licence == nil {
user.Licence = &models.Licence{
Number: "Z021AB37X13",
@@ -484,7 +490,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Valid Admin Update",
setupCookie: func(req *http.Request) {
req.AddCookie(&adminCookie)
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -513,7 +519,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Invalid Email Update",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -531,7 +537,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "admin may change licence number",
setupCookie: func(req *http.Request) {
req.AddCookie(&adminCookie)
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -545,7 +551,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Change phone number",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -559,7 +565,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Add category",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -570,14 +576,14 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("B")
assert.NoError(t, err)
u.Licence.Categories = []models.Category{category}
u.Licence.Categories = []*models.Category{&category}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete 1 and add 1 category",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -589,14 +595,14 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
category, err := licenceRepo.FindCategoryByName("A")
category2, err := licenceRepo.FindCategoryByName("BE")
assert.NoError(t, err)
u.Licence.Categories = []models.Category{category, category2}
u.Licence.Categories = []*models.Category{&category, &category2}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete 1 category",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -607,14 +613,14 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
category, err := licenceRepo.FindCategoryByName("A")
assert.NoError(t, err)
u.Licence.Categories = []models.Category{category}
u.Licence.Categories = []*models.Category{&category}
},
expectedStatus: http.StatusAccepted,
},
{
name: "Delete all categories",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -622,14 +628,14 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.Licence.Number = "B072RRE2I50"
u.Licence.Categories = []models.Category{}
u.Licence.Categories = []*models.Category{}
},
expectedStatus: http.StatusAccepted,
},
{
name: "User ID mismatch while not admin",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -648,7 +654,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Password Update low entropy should fail",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.FirstName = "John Updated"
@@ -665,7 +671,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Password Update",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.FirstName = "John Updated"
@@ -683,10 +689,24 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
},
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",
setupCookie: func(req *http.Request) {
req.AddCookie(&adminCookie)
req.AddCookie(AdminCookie)
},
updateFunc: func(u *models.User) {
u.LastName = "Doe Updated"
@@ -699,7 +719,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
{
name: "Non-existent User",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
req.AddCookie(MemberCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
@@ -718,7 +738,7 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
// Create a copy of the user and apply the updates
updatedUser := user
updatedUser := *user
// logger.Error.Printf("users licence to be updated: %+v", user.Licence)
tt.updateFunc(&updatedUser)
@@ -783,13 +803,17 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
assert.Equal(t, "User updated successfully", message)
// Verify the update in the database
updatedUserFromDB, err := Uc.Service.GetUserByID(user.ID)
updatedUserFromDB, err := Uc.Service.FromID(&user.ID)
assert.NoError(t, err)
if updatedUser.Password == "" {
assert.Equal(t, user.Password, (*updatedUserFromDB).Password)
} 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 = ""
@@ -801,11 +825,9 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
assert.Equal(t, updatedUser.Company, updatedUserFromDB.Company, "Company mismatch")
assert.Equal(t, updatedUser.Phone, updatedUserFromDB.Phone, "Phone mismatch")
assert.Equal(t, updatedUser.Notes, updatedUserFromDB.Notes, "Notes mismatch")
assert.Equal(t, updatedUser.ProfilePicture, updatedUserFromDB.ProfilePicture, "ProfilePicture mismatch")
assert.Equal(t, updatedUser.Address, updatedUserFromDB.Address, "Address mismatch")
assert.Equal(t, updatedUser.ZipCode, updatedUserFromDB.ZipCode, "ZipCode mismatch")
assert.Equal(t, updatedUser.City, updatedUserFromDB.City, "City mismatch")
assert.Equal(t, updatedUser.PaymentStatus, updatedUserFromDB.PaymentStatus, "PaymentStatus mismatch")
assert.Equal(t, updatedUser.Status, updatedUserFromDB.Status, "Status mismatch")
assert.Equal(t, updatedUser.RoleID, updatedUserFromDB.RoleID, "RoleID mismatch")
@@ -819,17 +841,32 @@ func testUpdateUser(t *testing.T, loginCookie http.Cookie, adminCookie http.Cook
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.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.Licence.Status, updatedUserFromDB.Licence.Status, "Licence.Status mismatch")
assert.Equal(t, updatedUser.Licence.Number, updatedUserFromDB.Licence.Number, "Licence.Number mismatch")
assert.Equal(t, updatedUser.Licence.IssuedDate, updatedUserFromDB.Licence.IssuedDate, "Licence.IssuedDate mismatch")
assert.Equal(t, updatedUser.Licence.ExpirationDate, updatedUserFromDB.Licence.ExpirationDate, "Licence.ExpirationDate mismatch")
assert.Equal(t, updatedUser.Licence.IssuingCountry, updatedUserFromDB.Licence.IssuingCountry, "Licence.IssuingCountry mismatch")
if updatedUser.Licence == nil {
assert.Nil(t, updatedUserFromDB.Licence, "database licence of user is not nil, but user.licence is nil")
} else {
logger.Error.Printf("updatedUser licence: %#v", updatedUser.Licence)
logger.Error.Printf("dbUser licence: %#v", updatedUserFromDB.Licence)
assert.Equal(t, updatedUser.Licence.Status, updatedUserFromDB.Licence.Status, "Licence.Status mismatch")
assert.Equal(t, updatedUser.Licence.Number, updatedUserFromDB.Licence.Number, "Licence.Number mismatch")
assert.Equal(t, updatedUser.Licence.IssuedDate, updatedUserFromDB.Licence.IssuedDate, "Licence.IssuedDate mismatch")
assert.Equal(t, updatedUser.Licence.ExpirationDate, updatedUserFromDB.Licence.ExpirationDate, "Licence.ExpirationDate mismatch")
assert.Equal(t, updatedUser.Licence.IssuingCountry, updatedUserFromDB.Licence.IssuingCountry, "Licence.IssuingCountry mismatch")
}
// For slices or more complex nested structures, you might want to use deep equality checks
assert.ElementsMatch(t, updatedUser.Consents, updatedUserFromDB.Consents, "Consents mismatch")
if len(updatedUser.Consents) > 0 {
for i := range updatedUser.Consents {
assert.Equal(t, updatedUser.Consents[i].ConsentType, updatedUserFromDB.Consents[i].ConsentType, "ConsentType mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].Email, updatedUserFromDB.Consents[i].Email, "ConsentEmail mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].FirstName, updatedUserFromDB.Consents[i].FirstName, "ConsentFirstName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].LastName, updatedUserFromDB.Consents[i].LastName, "ConsentLastName mismatch at index %d", i)
assert.Equal(t, updatedUser.Consents[i].UserID, updatedUserFromDB.Consents[i].UserID, "Consent UserId mismatch at index %d", i)
}
} else {
assert.Emptyf(t, updatedUserFromDB.Licence.Categories, "Categories aren't empty when they should")
}
if len(updatedUser.Licence.Categories) > 0 {
for i := range updatedUser.Licence.Categories {
assert.Equal(t, updatedUser.Licence.Categories[i].Name, updatedUserFromDB.Licence.Categories[i].Name, "Category Category mismatch at index %d", i)
@@ -855,11 +892,11 @@ func checkWelcomeMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, 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)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", 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.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", 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.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -891,11 +928,11 @@ func checkRegistrationMail(message *utils.Email, user *models.User) error {
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)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/Monat</strong>: %v", user.Membership.SubscriptionModel.MonthlyFee)) {
return fmt.Errorf("Users monthly subscription fee(%v) has not been rendered in registration mail.", 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.Subscription.MonthlyFee)
}
if !strings.Contains(message.Body, fmt.Sprintf("Preis/h</strong>: %v", user.Membership.SubscriptionModel.HourlyRate)) {
return fmt.Errorf("Users hourly subscription fee(%v) has not been rendered in registration mail.", 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.Subscription.HourlyRate)
}
if user.Company != "" && !strings.Contains(message.Body, user.Company) {
return fmt.Errorf("Users Company(%v) has not been rendered in registration mail.", user.Company)
@@ -935,15 +972,19 @@ func checkVerificationMail(message *utils.Email, user *models.User) error {
if err != nil {
return fmt.Errorf("Error parsing verification URL: %#v", err.Error())
}
if !strings.Contains(verificationURL, user.Verification.VerificationToken) {
return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", user.Verification.VerificationToken, verificationURL)
v, err := user.FindVerification(constants.VerificationTypes.Email)
if err != nil {
return fmt.Errorf("Error getting verification token: %v", err.Error())
}
if !strings.Contains(verificationURL, v.VerificationToken) {
return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", v.VerificationToken, verificationURL)
}
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.Site.BaseURL)
}
// open the provided link:
if err := verifyMail(verificationURL); err != nil {
if err := verifyMail(verificationURL, user.ID); err != nil {
return err
}
messages := utils.SMTPGetMessages()
@@ -960,12 +1001,14 @@ func checkVerificationMail(message *utils.Email, user *models.User) error {
}
func verifyMail(verificationURL string) error {
func verifyMail(verificationURL string, user_id uint) error {
gin.SetMode(gin.TestMode)
router := gin.New()
router.LoadHTMLGlob(filepath.Join(config.Templates.HTMLPath, "*"))
router.GET("api/users/verify", Uc.VerifyMailHandler)
expectedUrl := fmt.Sprintf("/api/users/verify/%v", user_id)
log.Printf("Expected URL: %v", expectedUrl)
router.GET("/api/users/verify/:id", Uc.VerifyMailHandler)
wv := httptest.NewRecorder()
cv, _ := gin.CreateTestContext(wv)
var err error
@@ -1108,8 +1151,9 @@ func getTestUsers() []RegisterUserTest {
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = "DE1234234123134"
user.RoleID = 0
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
@@ -1120,8 +1164,9 @@ func getTestUsers() []RegisterUserTest {
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.BankAccount.IBAN = ""
user.RoleID = 0
user.RoleID = constants.Roles.Supporter
user.Email = "john.supporter@example.com"
user.Membership.Subscription.Name = constants.SupporterSubscriptionName
return user
})),
},
@@ -1131,7 +1176,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = ""
user.Membership.Subscription.Name = ""
return user
})),
},
@@ -1141,7 +1186,7 @@ func getTestUsers() []RegisterUserTest {
WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Membership.SubscriptionModel.Name = "NOTEXISTENTPLAN"
user.Membership.Subscription.Name = "NOTEXISTENTPLAN"
return user
})),
},
@@ -1180,7 +1225,7 @@ func getTestUsers() []RegisterUserTest {
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},
@@ -1192,7 +1237,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 200
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},
@@ -1204,7 +1249,7 @@ func getTestUsers() []RegisterUserTest {
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.junior.doe@example.com"
user.Membership.ParentMembershipID = 1
user.Membership.SubscriptionModel.Name = "additional"
user.Membership.Subscription.Name = "additional"
return user
})),
},
@@ -1235,35 +1280,35 @@ func getTestUsers() []RegisterUserTest {
// return user
// })),
// },
// {
// Name: "empty driverslicence number, should fail",
// WantResponse: http.StatusBadRequest,
// WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
// Assert: false,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.wronglicence.doe@example.com"
// user.Licence = &models.Licence{
// Number: "",
// ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0),
// }
// return user
// })),
// },
// {
// Name: "Correct Licence number, should pass",
// WantResponse: http.StatusCreated,
// WantDBData: map[string]interface{}{"email": "john.correctLicenceNumber@example.com"},
// Assert: true,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.correctLicenceNumber@example.com"
// user.Licence = &models.Licence{
// Number: "B072RRE2I55",
// ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0),
// }
// return user
// })),
// },
{
Name: "empty driverslicence number, should fail",
WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.wronglicence.doe@example.com"
user.Licence = &models.Licence{
Number: "",
ExpirationDate: time.Now().AddDate(1, 0, 0),
IssuedDate: time.Now().AddDate(-1, 0, 0),
}
return user
})),
},
{
Name: "Correct Licence number, should pass",
WantResponse: http.StatusCreated,
WantDBData: map[string]interface{}{"email": "john.correctlicencenumber@example.com"},
Assert: true,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
user.Email = "john.correctLicenceNumber@example.com"
user.Licence = &models.Licence{
Number: "B072RRE2I55",
ExpirationDate: time.Now().AddDate(1, 0, 0),
IssuedDate: time.Now().AddDate(-1, 0, 0),
}
return user
})),
},
}
}

View File

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

View File

@@ -2,9 +2,7 @@ package middlewares
import (
"GoMembership/internal/config"
"GoMembership/internal/models"
"GoMembership/internal/utils"
customerrors "GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"errors"
"fmt"
@@ -34,26 +32,43 @@ func verifyAndRenewToken(tokenString string) (string, uint, error) {
return "", 0, fmt.Errorf("Authorization token is required")
}
token, claims, err := ExtractContentFrom(tokenString)
if err != nil {
if err != nil && !errors.Is(err, jwt.ErrTokenExpired) {
logger.Error.Printf("Couldn't parse JWT token String: %v", err)
return "", 0, err
}
sessionID := (*claims)["session_id"].(string)
userID := uint((*claims)["user_id"].(float64))
roleID := int8((*claims)["role_id"].(float64))
if token.Valid {
// token is valid, so we can return the old tokenString
return tokenString, uint((*claims)["user_id"].(float64)), nil
}
// Token is expired but valid
sessionID, ok := (*claims)["session_id"].(string)
if !ok || sessionID == "" {
return "", 0, fmt.Errorf("invalid session ID")
}
id, ok := (*claims)["user_id"]
if !ok {
return "", 0, fmt.Errorf("missing user_id claim")
}
userID := uint(id.(float64))
id, ok = (*claims)["role_id"]
if !ok {
return "", 0, fmt.Errorf("missing role_id claim")
}
roleID := int8(id.(float64))
session, ok := sessions[sessionID]
if !ok {
logger.Error.Printf("session not found")
return "", 0, fmt.Errorf("session not found")
}
if userID != session.UserID {
return "", 0, fmt.Errorf("Cookie has been altered, aborting..")
}
if token.Valid {
// token is valid, so we can return the old tokenString
return tokenString, session.UserID, customerrors.ErrValidToken
}
if time.Now().After(sessions[sessionID].ExpiresAt) {
delete(sessions, sessionID)
@@ -64,8 +79,8 @@ func verifyAndRenewToken(tokenString string) (string, uint, error) {
logger.Error.Printf("Session still valid generating new token")
// Session is still valid, generate a new token
user := models.User{ID: userID, RoleID: roleID}
newTokenString, err := GenerateToken(config.Auth.JWTSecret, &user, sessionID)
user := map[string]interface{}{"user_id": userID, "role_id": roleID}
newTokenString, err := GenerateToken(&config.Auth.JWTSecret, user, sessionID)
if err != nil {
return "", 0, err
}
@@ -89,11 +104,6 @@ func AuthMiddleware() gin.HandlerFunc {
newToken, userID, err := verifyAndRenewToken(tokenString)
if err != nil {
if err == customerrors.ErrValidToken {
c.Set("user_id", uint(userID))
c.Next()
return
}
logger.Error.Printf("Token(%v) is invalid: %v\n", tokenString, err)
c.JSON(http.StatusUnauthorized,
gin.H{"errors": []gin.H{{
@@ -104,24 +114,30 @@ func AuthMiddleware() gin.HandlerFunc {
return
}
utils.SetCookie(c, newToken)
if newToken != tokenString {
utils.SetCookie(c, newToken)
}
c.Set("user_id", uint(userID))
c.Next()
}
}
func GenerateToken(jwtKey string, user *models.User, sessionID string) (string, error) {
// GenerateToken generates a new JWT token with the given claims and session ID.
// "user_id": user.ID, "role_id": user.RoleID
func GenerateToken(jwtKey *string, claims map[string]interface{}, sessionID string) (string, error) {
if sessionID == "" {
sessionID = uuid.New().String()
}
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": user.ID,
"role_id": user.RoleID,
"session_id": sessionID,
"exp": time.Now().Add(time.Minute * 1).Unix(), // Token expires in 10 Minutes
})
UpdateSession(sessionID, user.ID)
return token.SignedString([]byte(jwtKey))
claims["session_id"] = sessionID
claims["exp"] = time.Now().Add(time.Minute * 1).Unix() // Token expires in 10 Minutes
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims(claims))
userID, ok := claims["user_id"].(uint)
if !ok {
return "", fmt.Errorf("invalid user_id in claims")
}
UpdateSession(sessionID, userID)
return token.SignedString([]byte(*jwtKey))
}
func ExtractContentFrom(tokenString string) (*jwt.Token, *jwt.MapClaims, error) {
@@ -130,23 +146,33 @@ func ExtractContentFrom(tokenString string) (*jwt.Token, *jwt.MapClaims, error)
return []byte(config.Auth.JWTSecret), nil
})
if !errors.Is(err, jwt.ErrTokenExpired) && err != nil {
logger.Error.Printf("Error during token(%v) parsing: %#v", tokenString, err)
// Handle parsing errors (excluding expiration error)
if err != nil && !errors.Is(err, jwt.ErrTokenExpired) {
logger.Error.Printf("Error parsing token: %v", err)
return nil, nil, err
}
// Token is expired, check if session is still valid
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
logger.Error.Printf("Invalid Token Claims")
return nil, nil, fmt.Errorf("invalid token claims")
// Ensure token is not nil (e.g., malformed tokens)
if token == nil {
logger.Error.Print("Token is nil after parsing")
return nil, nil, fmt.Errorf("invalid token")
}
// Extract and validate claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
logger.Error.Printf("invalid session_id in token")
return nil, nil, fmt.Errorf("invalid session_id in token")
logger.Error.Print("Invalid token claims structure")
return nil, nil, fmt.Errorf("invalid token claims format")
}
return token, &claims, nil
// Validate required session_id claim
if _, exists := claims["session_id"]; !exists {
logger.Error.Print("Missing session_id in token claims")
return nil, nil, fmt.Errorf("missing session_id claim")
}
// Return token, claims, and original error (might be expiration)
return token, &claims, err
}
func UpdateSession(sessionID string, userID uint) {

View File

@@ -3,7 +3,6 @@ package middlewares
import (
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/internal/models"
"GoMembership/pkg/logger"
"encoding/json"
"log"
@@ -56,8 +55,11 @@ func TestAuthMiddleware(t *testing.T) {
{
name: "Valid Token",
setupAuth: func(r *http.Request) {
user := models.User{ID: 123, RoleID: constants.Roles.Member}
token, _ := GenerateToken(config.Auth.JWTSecret, &user, "")
claims := map[string]interface{}{"user_id": uint(123), "role_id": constants.Roles.Member}
token, err := GenerateToken(&config.Auth.JWTSecret, claims, "")
if err != nil {
t.Fatal(err)
}
r.AddCookie(&http.Cookie{Name: "jwt", Value: token})
},
expectedStatus: http.StatusOK,
@@ -82,7 +84,7 @@ func TestAuthMiddleware(t *testing.T) {
setupAuth: func(r *http.Request) {
sessionID := "test-session"
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": 123,
"user_id": uint(123),
"role_id": constants.Roles.Member,
"session_id": sessionID,
"exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago
@@ -100,7 +102,7 @@ func TestAuthMiddleware(t *testing.T) {
setupAuth: func(r *http.Request) {
sessionID := "expired-session"
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": 123,
"user_id": uint(123),
"role_id": constants.Roles.Member,
"session_id": sessionID,
"exp": time.Now().Add(-time.Hour).Unix(), // Expired 1 hour ago
@@ -116,7 +118,7 @@ func TestAuthMiddleware(t *testing.T) {
name: "Invalid Signature",
setupAuth: func(r *http.Request) {
token := jwt.NewWithClaims(jwtSigningMethod, jwt.MapClaims{
"user_id": 123,
"user_id": uint(123),
"session_id": "some-session",
"exp": time.Now().Add(time.Hour).Unix(),
})
@@ -130,7 +132,7 @@ func TestAuthMiddleware(t *testing.T) {
name: "Invalid Signing Method",
setupAuth: func(r *http.Request) {
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
"user_id": 123,
"user_id": uint(123),
"session_id": "some-session",
"role_id": constants.Roles.Member,
"exp": time.Now().Add(time.Hour).Unix(),

View File

@@ -9,7 +9,6 @@ import (
)
func CSPMiddleware() gin.HandlerFunc {
logger.Error.Printf("applying CSP")
return func(c *gin.Context) {
policy := "default-src 'self'; " +
"script-src 'self' 'unsafe-inline'" +
@@ -35,7 +34,6 @@ func CSPMiddleware() gin.HandlerFunc {
func CSPReportHandling(c *gin.Context) {
var report map[string]interface{}
if err := c.BindJSON(&report); err != nil {
logger.Error.Printf("Couldn't Bind JSON: %#v", err)
return
}

View File

@@ -1,13 +1,10 @@
package middlewares
import (
"GoMembership/pkg/logger"
"github.com/gin-gonic/gin"
)
func SecurityHeadersMiddleware() gin.HandlerFunc {
logger.Error.Printf("applying headers")
return func(c *gin.Context) {
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,67 +1,175 @@
package models
import (
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"fmt"
"slices"
"strings"
"time"
"github.com/alexedwards/argon2id"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type User struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"`
Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" binding:"safe_content"`
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
Password string `json:"password" binding:"safe_content"`
Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"`
Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
BankAccountID uint
Verification Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
VerificationID uint
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"`
MembershipID uint
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"`
LicenceID uint
PaymentStatus int8 `json:"payment_status"`
Status int8 `json:"status"`
RoleID int8 `json:"role_id"`
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"`
Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" binding:"safe_content"`
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
Password string `json:"password" binding:"safe_content"`
Email string `gorm:"uniqueIndex:idx_users_email,not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
Consents []Consent `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
BankAccount *BankAccount `gorm:"foreignkey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"bank_account"`
Verifications []Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Membership *Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"membership"`
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"licence"`
Status int8 `json:"status"`
RoleID int8 `json:"role_id"`
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.BankAccount.ID != 0 && u.BankAccount.MandateReference == "" {
mandateReference := u.GenerateMandateReference()
return tx.Model(&u.BankAccount).Update("MandateReference", mandateReference).Error
if u.BankAccount != nil && u.BankAccount.MandateReference == "" {
u.BankAccount.MandateReference = u.BankAccount.GenerateMandateReference(u.ID)
u.BankAccount.Update(tx)
}
return nil
}
func (u *User) GenerateMandateReference() string {
return fmt.Sprintf("%s%d%s", time.Now().Format("20060102"), u.ID, u.BankAccount.IBAN)
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
u.Email = strings.ToLower(u.Email)
if u.Password != "" {
hash, err := argon2id.CreateHash(u.Password, argon2id.DefaultParams)
if err != nil {
return err
}
u.Password = hash
}
return nil
}
func (u *User) SetPassword(plaintextPassword string) error {
if plaintextPassword == "" {
return nil
}
func (u *User) Create(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Preload(clause.Associations).Create(u).Error; err != nil {
return err
}
return nil
})
}
func (u *User) Update(db *gorm.DB) error {
err := db.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingUser User
logger.Info.Printf("updating user: %#v", u)
if err := tx.
First(&existingUser, u.ID).Error; err != nil {
return err
}
// Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Verifications", "Licence.Categories").Updates(u)
if result.Error != nil {
logger.Error.Printf("User update error in update user: %#v", result.Error)
return result.Error
}
if result.RowsAffected == 0 {
return errors.ErrNoRowsAffected
}
if u.Verifications != nil {
if err := tx.Save(u.Verifications).Error; err != nil {
return err
}
}
if u.Licence != nil {
if err := tx.Model(u.Licence).Association("Categories").Replace(u.Licence.Categories); err != nil {
return err
}
}
return nil
})
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil {
return err
}
u.Password = hash
return db.
Preload(clause.Associations).
Preload("Membership.Subscription").
Preload("Licence.Categories").
First(&u, u.ID).Error
}
func (u *User) Delete(db *gorm.DB) error {
return db.Unscoped().Delete(&User{}, "id = ?", u.ID).Error
// return db.Delete(&User{}, "id = ?", u.ID).Error
}
func (u *User) FromID(db *gorm.DB, userID *uint) error {
var user User
result := db.
Preload(clause.Associations).
Preload("Membership.Subscription").
Preload("Licence.Categories").
First(&user, userID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return gorm.ErrRecordNotFound
}
return result.Error
}
*u = user
return nil
}
func (u *User) FromEmail(db *gorm.DB, email *string) error {
var user User
result := db.
Preload(clause.Associations).
Preload("Membership.Subscription").
Preload("Licence.Categories").
Where("email = ?", email).First(&user)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return gorm.ErrRecordNotFound
}
return result.Error
}
*u = user
return nil
}
func (u *User) FromContext(db *gorm.DB, c *gin.Context) error {
tokenString, err := c.Cookie("jwt")
if err != nil {
return err
}
jwtUserID, err := extractUserIDFrom(tokenString)
if err != nil {
return err
}
if err = u.FromID(db, &jwtUserID); err != nil {
return err
}
return nil
}
@@ -69,53 +177,100 @@ 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 {
return u.Status > constants.DisabledStatus
}
func (u *User) HasPrivilege(privilege int8) bool {
return u.RoleID >= privilege
}
func (u *User) IsAdmin() bool {
return u.RoleID == constants.Roles.Admin
}
func (u *User) IsMember() bool {
return u.RoleID == constants.Roles.Member
}
func (u *User) IsSupporter() bool {
return u.RoleID == constants.Roles.Supporter
}
func (u *User) SetVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil {
u.Verifications = []Verification{}
}
v, err := CreateVerification(verificationType)
if err != nil {
return nil, err
}
v.UserID = u.ID
if vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == v.Type }); vi > -1 {
u.Verifications[vi] = *v
} else {
u.Verifications = append(u.Verifications, *v)
}
return v, nil
}
func (u *User) FindVerification(verificationType string) (*Verification, error) {
if u.Verifications == nil {
return nil, errors.ErrNoData
}
vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool { return vsl.Type == verificationType })
if vi == -1 {
return nil, errors.ErrNotFound
}
return &u.Verifications[vi], nil
}
func (u *User) Verify(token string, verificationType string) error {
if token == "" || verificationType == "" {
logger.Error.Printf("token or verification type are empty in user.Verify")
return errors.ErrNoData
}
vi := slices.IndexFunc(u.Verifications, func(vsl Verification) bool {
return vsl.Type == verificationType && vsl.VerificationToken == token
})
if vi == -1 {
logger.Error.Printf("Couldn't find verification in users verifications")
return errors.ErrNotFound
}
return u.Verifications[vi].Validate()
}
func (u *User) Safe() map[string]interface{} {
result := map[string]interface{}{
"email": u.Email,
"first_name": u.FirstName,
"last_name": u.LastName,
"phone": u.Phone,
"notes": u.Notes,
"address": u.Address,
"zip_code": u.ZipCode,
"city": u.City,
"status": u.Status,
"id": u.ID,
"role_id": u.RoleID,
"company": u.Company,
"dateofbirth": u.DateOfBirth,
"membership": map[string]interface{}{
var membership map[string]interface{} = nil
var licence map[string]interface{} = nil
var bankAccount map[string]interface{} = nil
if u.Membership != nil {
membership = map[string]interface{}{
"id": u.Membership.ID,
"start_date": u.Membership.StartDate,
"end_date": u.Membership.EndDate,
"status": u.Membership.Status,
"subscription_model": map[string]interface{}{
"id": u.Membership.SubscriptionModel.ID,
"name": u.Membership.SubscriptionModel.Name,
"details": u.Membership.SubscriptionModel.Details,
"conditions": u.Membership.SubscriptionModel.Conditions,
"monthly_fee": u.Membership.SubscriptionModel.MonthlyFee,
"hourly_rate": u.Membership.SubscriptionModel.HourlyRate,
"included_per_year": u.Membership.SubscriptionModel.IncludedPerYear,
"included_per_month": u.Membership.SubscriptionModel.IncludedPerMonth,
"subscription": map[string]interface{}{
"id": u.Membership.Subscription.ID,
"name": u.Membership.Subscription.Name,
"details": u.Membership.Subscription.Details,
"conditions": u.Membership.Subscription.Conditions,
"monthly_fee": u.Membership.Subscription.MonthlyFee,
"hourly_rate": u.Membership.Subscription.HourlyRate,
"included_per_year": u.Membership.Subscription.IncludedPerYear,
"included_per_month": u.Membership.Subscription.IncludedPerMonth,
},
},
"licence": map[string]interface{}{
"id": 0,
},
"bank_account": map[string]interface{}{
"id": u.BankAccount.ID,
"mandate_date_signed": u.BankAccount.MandateDateSigned,
"bank": u.BankAccount.Bank,
"account_holder_name": u.BankAccount.AccountHolderName,
"iban": u.BankAccount.IBAN,
"bic": u.BankAccount.BIC,
"mandate_reference": u.BankAccount.MandateReference,
},
}
}
if u.Licence != nil {
result["licence"] = map[string]interface{}{
licence = map[string]interface{}{
"id": u.Licence.ID,
"number": u.Licence.Number,
"status": u.Licence.Status,
@@ -126,5 +281,91 @@ func (u *User) Safe() map[string]interface{} {
}
}
if u.BankAccount != nil {
bankAccount = map[string]interface{}{
"id": u.BankAccount.ID,
"mandate_date_signed": u.BankAccount.MandateDateSigned,
"bank": u.BankAccount.Bank,
"account_holder_name": u.BankAccount.AccountHolderName,
"iban": u.BankAccount.IBAN,
"bic": u.BankAccount.BIC,
"mandate_reference": u.BankAccount.MandateReference,
}
}
result := map[string]interface{}{
"email": u.Email,
"first_name": u.FirstName,
"last_name": u.LastName,
"phone": u.Phone,
"notes": u.Notes,
"address": u.Address,
"zip_code": u.ZipCode,
"city": u.City,
"status": u.Status,
"id": u.ID,
"role_id": u.RoleID,
"company": u.Company,
"dateofbirth": u.DateOfBirth,
"membership": membership,
"licence": licence,
"bank_account": bankAccount,
}
return result
}
func extractUserIDFrom(tokenString string) (uint, error) {
jwtSigningMethod := jwt.SigningMethodHS256
jwtParser := jwt.NewParser(jwt.WithValidMethods([]string{jwtSigningMethod.Alg()}))
token, err := jwtParser.Parse(tokenString, func(_ *jwt.Token) (interface{}, error) {
return []byte(config.Auth.JWTSecret), nil
})
// Handle parsing errors (excluding expiration error)
if err != nil && !errors.Is(err, jwt.ErrTokenExpired) || token == nil {
logger.Error.Printf("Error parsing token: %v", err)
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
logger.Error.Print("Invalid token claims structure")
return 0, fmt.Errorf("invalid token claims format")
}
// Validate required session_id claim
if _, exists := claims["session_id"]; !exists {
logger.Error.Print("Missing session_id in token claims")
return 0, fmt.Errorf("missing session_id claim")
}
// Return token, claims, and original error (might be expiration)
if _, exists := claims["session_id"]; !exists {
logger.Error.Print("Missing session_id in token claims")
return 0, fmt.Errorf("missing session_id claim")
}
id, ok := claims["user_id"]
if !ok {
return 0, fmt.Errorf("missing user_id claim")
}
return uint(id.(float64)), nil
}
func GetUsersWhere(db *gorm.DB, where map[string]interface{}) (*[]User, error) {
logger.Error.Printf("where: %#v", where)
var users []User
result := db.
Preload(clause.Associations).
Preload("Membership.Subscription").
Preload("Licence.Categories").
Where(where).Find(&users)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &users, nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,159 +0,0 @@
package repositories
import (
"gorm.io/gorm"
"GoMembership/internal/database"
"gorm.io/gorm/clause"
"GoMembership/internal/models"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
)
type UserRepositoryInterface interface {
CreateUser(user *models.User) (uint, error)
UpdateUser(user *models.User) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error)
GetUserByEmail(email string) (*models.User, error)
IsVerified(userID *uint) (bool, error)
GetVerificationOfToken(token *string, verificationType *string) (*models.Verification, error)
SetVerificationToken(verification *models.Verification) (token string, err error)
DeleteVerification(id uint, verificationType string) error
DeleteUser(id uint) error
SetUserStatus(id uint, status uint) error
}
type UserRepository struct{}
func (ur *UserRepository) DeleteUser(id uint) error {
return database.DB.Delete(&models.User{}, "id = ?", id).Error
}
func PasswordExists(userID *uint) (bool, error) {
var user models.User
result := database.DB.Select("password").First(&user, userID)
if result.Error != nil {
return false, result.Error
}
return user.Password != "", nil
}
func (ur *UserRepository) CreateUser(user *models.User) (uint, error) {
result := database.DB.Create(user)
if result.Error != nil {
logger.Error.Printf("Create User error: %#v", result.Error)
return 0, result.Error
}
return user.ID, nil
}
func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
if user == nil {
return nil, errors.ErrNoData
}
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Check if the user exists in the database
var existingUser models.User
if err := tx.Preload(clause.Associations).
Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("Licence").
Preload("Licence.Categories").
First(&existingUser, user.ID).Error; err != nil {
return err
}
// Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Omit("Password").Updates(user)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.ErrNoRowsAffected
}
if user.Password != "" {
if err := tx.Model(&models.User{}).
Where("id = ?", user.ID).
Update("Password", user.Password).Error; err != nil {
return err
}
}
// Update the Membership if provided
if user.Membership.ID != 0 {
if err := tx.Model(&existingUser.Membership).Updates(user.Membership).Error; err != nil {
return err
}
}
// Replace categories if Licence and Categories are provided
if user.Licence != nil {
if err := tx.Model(&user.Licence).Association("Categories").Replace(user.Licence.Categories); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
var updatedUser models.User
if err := database.DB.Preload("Licence.Categories").
Preload("Membership").
First(&updatedUser, user.ID).Error; err != nil {
return nil, err
}
return &updatedUser, nil
}
func (ur *UserRepository) GetUsers(where map[string]interface{}) (*[]models.User, error) {
var users []models.User
result := database.DB.
Preload(clause.Associations).
Preload("Membership.SubscriptionModel").
Preload("Licence.Categories").
Where(where).Find(&users)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &users, nil
}
func GetUserByID(userID *uint) (*models.User, error) {
var user models.User
result := database.DB.
Preload(clause.Associations).
Preload("Membership").
Preload("Membership.SubscriptionModel").
Preload("Licence.Categories").
First(&user, userID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &user, nil
}
func (ur *UserRepository) GetUserByEmail(email string) (*models.User, error) {
var user models.User
result := database.DB.Where("email = ?", email).First(&user)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, gorm.ErrRecordNotFound
}
return nil, result.Error
}
return &user, nil
}

View File

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

View File

@@ -7,8 +7,8 @@ import (
"github.com/gin-gonic/gin"
)
func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController, licenceController *controllers.LicenceController) {
router.GET("/api/users/verify", userController.VerifyMailHandler)
func RegisterRoutes(router *gin.Engine, userController *controllers.UserController, membershipcontroller *controllers.MembershipController, contactController *controllers.ContactController, licenceController *controllers.LicenceController, carController *controllers.CarController) {
router.GET("/api/users/verify/:id", userController.VerifyMailHandler)
router.POST("/api/users/register", userController.RegisterUser)
router.POST("/api/users/contact", contactController.RelayContactRequest)
router.POST("/api/users/password/request-change", userController.RequestPasswordChangeHandler)
@@ -19,12 +19,17 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
userRouter := router.Group("/api/auth")
userRouter.Use(middlewares.AuthMiddleware())
{
userRouter.GET("/cars", carController.GetAll)
userRouter.PUT("/cars", carController.Update)
userRouter.POST("/cars", carController.Create)
userRouter.DELETE("/cars", carController.Delete)
userRouter.GET("/users/current", userController.CurrentUserHandler)
userRouter.POST("/logout", userController.LogoutHandler)
userRouter.PUT("/users", userController.UpdateHandler)
userRouter.POST("/users", userController.RegisterUser)
userRouter.GET("/users", userController.GetAllUsers)
userRouter.DELETE("/users", userController.DeleteUser)
userRouter.PATCH("/users/activate", userController.CreatePasswordHandler)
userRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions)
userRouter.PUT("/subscriptions", membershipcontroller.UpdateHandler)
userRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription)

View File

@@ -20,13 +20,14 @@ import (
"GoMembership/pkg/logger"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var shutdownChannel = make(chan struct{})
var srv *http.Server
// Run initializes the server configuration, sets up services and controllers, and starts the HTTP server.
func Run() {
func Run(db *gorm.DB) {
emailService := services.NewEmailService(config.SMTP.Host, config.SMTP.Port, config.SMTP.User, config.SMTP.Password)
var consentRepo repositories.ConsentRepositoryInterface = &repositories.ConsentRepository{}
@@ -36,19 +37,19 @@ func Run() {
bankAccountService := &services.BankAccountService{Repo: bankAccountRepo}
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}
var licenceRepo repositories.LicenceInterface = &repositories.LicenceRepository{}
licenceService := &services.LicenceService{Repo: licenceRepo}
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
userService := &services.UserService{Repo: userRepo, Licences: licenceRepo}
userService := &services.UserService{DB: db, Licences: licenceRepo}
userController := &controllers.UserController{Service: userService, EmailService: emailService, ConsentService: consentService, LicenceService: licenceService, BankAccountService: bankAccountService, MembershipService: membershipService}
membershipController := &controllers.MembershipController{Service: *membershipService, UserController: userController}
licenceController := &controllers.LicenceController{Service: *licenceService}
membershipController := &controllers.MembershipController{Service: membershipService, UserService: userService}
licenceController := &controllers.LicenceController{Service: licenceService}
contactController := &controllers.ContactController{EmailService: emailService}
carService := &services.CarService{DB: db}
carController := &controllers.CarController{S: carService, UserService: userService}
router := gin.Default()
// gin.SetMode(gin.ReleaseMode)
@@ -64,8 +65,8 @@ func Run() {
limiter := middlewares.NewIPRateLimiter(config.Security.Ratelimits.Limit, config.Security.Ratelimits.Burst)
router.Use(middlewares.RateLimitMiddleware(limiter))
routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController)
validation.SetupValidators()
routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController, carController)
validation.SetupValidators(db)
logger.Info.Println("Starting server on :8080")
srv = &http.Server{

View File

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

View File

@@ -46,7 +46,7 @@ func (s *EmailService) SendEmail(to string, subject string, body string, bodyTXT
func ParseTemplate(filename string, data interface{}) (string, error) {
// Read the email template file
logger.Error.Printf("Data: %#v", data)
templateDir := config.Templates.MailPath
tpl, err := template.ParseFiles(templateDir + "/" + filename)
if err != nil {
@@ -71,13 +71,18 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
LastName string
Token string
BASEURL string
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
logger.Error.Printf("USERIID: %#v", user.ID)
subject := constants.MailVerificationSubject
body, err := ParseTemplate("mail_verification.tmpl", data)
if err != nil {
@@ -87,6 +92,39 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
return s.SendEmail(user.Email, subject, body, "", "")
}
func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
data := struct {
FirstName string
LastName string
Token string
BASEURL string
FRONTEND_PATH string
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
subject := constants.MailGrantBackendAccessSubject
htmlBody, err := ParseTemplate("mail_grant_backend_access.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't send grant backend access mail")
return err
}
plainBody, err := ParseTemplate("mail_grant_backend_access.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
}
func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
@@ -97,6 +135,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
BASEURL string
FRONTEND_PATH string
UserID uint
Logo string
}{
FirstName: user.FirstName,
LastName: user.LastName,
@@ -104,6 +143,7 @@ func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string)
FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
Logo: config.Templates.LogoURI,
}
subject := constants.MailChangePasswordSubject
@@ -136,10 +176,10 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
}{
Company: user.Company,
FirstName: user.FirstName,
MembershipModel: user.Membership.SubscriptionModel.Name,
MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.Subscription.HourlyRate),
BASEURL: config.Site.BaseURL,
WebsiteTitle: config.Site.WebsiteTitle,
Logo: config.Templates.LogoURI,
@@ -183,10 +223,10 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
Company: user.Company,
FirstName: user.FirstName,
LastName: user.LastName,
MembershipModel: user.Membership.SubscriptionModel.Name,
MembershipModel: user.Membership.Subscription.Name,
MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
MembershipFee: float32(user.Membership.Subscription.MonthlyFee),
RentalFee: float32(user.Membership.Subscription.HourlyRate),
Address: user.Address,
ZipCode: user.ZipCode,
City: user.City,

View File

@@ -5,7 +5,7 @@ import (
"GoMembership/internal/repositories"
)
type LicenceInterface interface {
type LicenceServiceInterface interface {
GetAllCategories() ([]models.Category, error)
}

View File

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

View File

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

View File

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

View File

@@ -8,65 +8,81 @@ import (
"GoMembership/internal/repositories"
"GoMembership/pkg/errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type UserServiceInterface interface {
RegisterUser(user *models.User) (id uint, token string, err error)
GetUserByEmail(email string) (*models.User, error)
GetUserByID(id uint) (*models.User, error)
Register(user *models.User) (id uint, token string, err error)
Update(user *models.User) (*models.User, error)
Delete(id *uint) error
FromContext(c *gin.Context) (*models.User, error)
FromID(id *uint) (*models.User, error)
FromEmail(email *string) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error)
UpdateUser(user *models.User) (*models.User, error)
DeleteUser(lastname string, id uint) error
SetUserStatus(id uint, status uint) error
VerifyUser(token *string, verificationType *string) (*models.Verification, error)
SetVerificationToken(id *uint, verificationType *string) (string, error)
RevokeVerification(id *uint, verificationType string) error
HandlePasswordChangeRequest(user *models.User) (token string, err error)
}
type UserService struct {
Repo repositories.UserRepositoryInterface
Licences repositories.LicenceInterface
DB *gorm.DB
}
func (service *UserService) DeleteUser(lastname string, id uint) error {
if id == 0 || lastname == "" {
return errors.ErrNoData
func (s *UserService) FromContext(c *gin.Context) (*models.User, error) {
var user models.User
if err := user.FromContext(s.DB, c); err != nil {
return nil, err
}
return &user, nil
}
user, err := service.GetUserByID(id)
if err != nil {
func (s *UserService) FromID(id *uint) (*models.User, error) {
var user models.User
if err := user.FromID(s.DB, id); err != nil {
return nil, err
}
return &user, nil
}
func (s *UserService) FromEmail(email *string) (*models.User, error) {
var user models.User
if err := user.FromEmail(s.DB, email); err != nil {
return nil, err
}
return &user, nil
}
func (s *UserService) Delete(id *uint) error {
var user models.User
if err := user.FromID(s.DB, id); err != nil {
return err
}
if user == nil {
return errors.ErrUserNotFound
}
return service.Repo.DeleteUser(id)
return user.Delete(s.DB)
}
func (service *UserService) UpdateUser(user *models.User) (*models.User, error) {
func (s *UserService) Update(user *models.User) (*models.User, error) {
if user.ID == 0 {
return nil, errors.ErrUserNotFound
var existingUser models.User
if err := existingUser.FromID(s.DB, &user.ID); err != nil {
return nil, err
}
user.SetPassword(user.Password)
user.Membership.ID = existingUser.Membership.ID
if existingUser.Licence != nil {
user.Licence.ID = existingUser.Licence.ID
}
user.BankAccount.ID = existingUser.BankAccount.ID
// Validate subscription model
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
return nil, errors.ErrSubscriptionNotFound
}
user.Membership.SubscriptionModel = *selectedModel
user.Membership.SubscriptionModelID = selectedModel.ID
user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionID = selectedModel.ID
updatedUser, err := service.Repo.UpdateUser(user)
if err != nil {
if err := user.Update(s.DB); err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.ErrUserNotFound
}
@@ -75,42 +91,36 @@ func (service *UserService) UpdateUser(user *models.User) (*models.User, error)
}
return nil, err
}
return updatedUser, nil
return user, nil
}
func (service *UserService) RegisterUser(user *models.User) (id uint, token string, err error) {
user.SetPassword(user.Password)
func (s *UserService) Register(user *models.User) (id uint, token string, err error) {
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
return 0, "", errors.ErrSubscriptionNotFound
}
user.Membership.Subscription = *selectedModel
user.Membership.SubscriptionID = selectedModel.ID
user.Status = constants.UnverifiedStatus
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
user.PaymentStatus = constants.AwaitingPaymentStatus
user.BankAccount.MandateDateSigned = time.Now()
id, err = service.Repo.CreateUser(user)
v, err := user.SetVerification(constants.VerificationTypes.Email)
if err != nil {
return 0, "", err
}
token, err = service.SetVerificationToken(&id, &constants.VerificationTypes.Email)
if err != nil {
if err := user.Create(s.DB); err != nil {
return 0, "", err
}
return id, token, nil
return user.ID, v.VerificationToken, nil
}
func (service *UserService) GetUserByID(id uint) (*models.User, error) {
return repositories.GetUserByID(&id)
}
func (service *UserService) GetUserByEmail(email string) (*models.User, error) {
return service.Repo.GetUserByEmail(email)
}
func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) {
// GetUsers returns a list of users based on the provided where clause.
// if where == nil: all users are returned
func (s *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) {
if where == nil {
where = map[string]interface{}{}
}
return service.Repo.GetUsers(where)
return models.GetUsersWhere(s.DB, where)
}

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ func HandleUserUpdateError(c *gin.Context, err error) {
case errors.ErrDuplicateEntry:
RespondWithError(c, err, "User Unique constraint failed", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.Duplicate)
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:
RespondWithError(c, err, "Couldn't update user", http.StatusInternalServerError, errors.Responses.Fields.User, errors.Responses.Keys.InternalServerError)
}
@@ -53,46 +53,34 @@ func HandleUserUpdateError(c *gin.Context, err error) {
func HandleSubscriptionDeleteError(c *gin.Context, err error) {
switch err {
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:
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:
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:
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:
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) {
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 {
switch err {
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:
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:
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)
}
}
}
func HandleVerifyUserError(c *gin.Context, err error) {
if err.Error() == "record not found" {
RespondWithError(c, err, "Couldn't find verification. This is most probably a outdated token.", http.StatusGone, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
}
switch err {
case errors.ErrAlreadyVerified:
RespondWithError(c, err, "User already changed password", http.StatusConflict, errors.Responses.Fields.User, errors.Responses.Keys.PasswordAlreadyChanged)
default:
RespondWithError(c, err, "Couldn't verify user", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
}
}
func HandleDeleteUserError(c *gin.Context, err error) {
if err.Error() == "record not found" {
RespondWithError(c, err, "Couldn't find user", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)

View File

@@ -1,15 +1,10 @@
package utils
package validation
import (
"GoMembership/internal/models"
"errors"
"reflect"
)
func HasPrivilige(user *models.User, privilige int8) bool {
return user.RoleID >= privilige
}
// FilterAllowedStructFields filters allowed fields recursively in a struct and modifies structToModify in place.
func FilterAllowedStructFields(input interface{}, existing interface{}, allowedFields map[string]bool, prefix string) error {
v := reflect.ValueOf(input)

View File

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

View File

@@ -2,39 +2,37 @@ package validation
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
func validateMembership(sl validator.StructLevel) {
membership := sl.Current().Interface().(models.User).Membership
if membership.SubscriptionModel.RequiredMembershipField != "" {
switch membership.SubscriptionModel.RequiredMembershipField {
func validateMembership(db *gorm.DB, user *models.User, sl validator.StructLevel) {
if user.Membership.Subscription.RequiredMembershipField != "" {
switch user.Membership.Subscription.RequiredMembershipField {
case "ParentMembershipID":
if err := CheckParentMembershipID(membership); err != nil {
if err := CheckParentMembershipID(db, user); err != nil {
logger.Error.Printf("Error ParentMembershipValidation: %v", err.Error())
sl.ReportError(membership.ParentMembershipID, membership.SubscriptionModel.RequiredMembershipField,
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "invalid", "")
}
default:
logger.Error.Printf("Error no matching RequiredMembershipField: %v", errors.ErrInvalidValue.Error())
sl.ReportError(membership.ParentMembershipID, membership.SubscriptionModel.RequiredMembershipField,
sl.ReportError(user.Membership.ParentMembershipID, user.Membership.Subscription.RequiredMembershipField,
"RequiredMembershipField", "not_implemented", "")
}
}
}
func CheckParentMembershipID(membership models.Membership) error {
func CheckParentMembershipID(db *gorm.DB, user *models.User) error {
if membership.ParentMembershipID == 0 {
if user.Membership.ParentMembershipID == 0 {
return errors.ValErrParentIDNotSet
} else {
_, err := repositories.GetUserByID(&membership.ParentMembershipID)
if err != nil {
var parent models.User
if err := parent.FromID(db, &user.Membership.ParentMembershipID); err != nil {
return errors.ValErrParentIDNotFound
}
}

View File

@@ -1,20 +1,19 @@
package validation
import (
"GoMembership/internal/models"
"github.com/gin-gonic/gin/binding"
"gorm.io/gorm"
"github.com/go-playground/validator/v10"
)
func SetupValidators() {
func SetupValidators(db *gorm.DB) {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validators
v.RegisterValidation("safe_content", ValidateSafeContent)
// Register struct-level validations
v.RegisterStructValidation(ValidateUser, models.User{})
v.RegisterStructValidation(ValidateSubscription, models.SubscriptionModel{})
// v.RegisterStructValidation(ValidateUserFactory(db), models.User{})
// v.RegisterStructValidation(ValidateSubscription, models.Subscription{})
}
}

View File

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

View File

@@ -1,7 +1,6 @@
package validation
import (
"GoMembership/internal/constants"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/pkg/logger"
@@ -10,6 +9,7 @@ import (
"github.com/go-playground/validator/v10"
passwordvalidator "github.com/wagslane/go-password-validator"
"gorm.io/gorm"
)
var passwordErrorTranslations = map[string]string{
@@ -21,24 +21,28 @@ var passwordErrorTranslations = map[string]string{
"using numbers": "server.validation.numbers",
}
func ValidateUser(sl validator.StructLevel) {
func ValidateUserFactory(db *gorm.DB) validator.StructLevelFunc {
return func(sl validator.StructLevel) {
validateUser(db, sl)
}
}
func validateUser(db *gorm.DB, sl validator.StructLevel) {
user := sl.Current().Interface().(models.User)
isSuper := user.RoleID >= constants.Roles.Admin
isSupporter := user.RoleID == constants.Roles.Supporter
// validate subscriptionModel
if user.Membership.SubscriptionModel.Name == "" {
sl.ReportError(user.Membership.SubscriptionModel.Name, "subscription.name", "name", "required", "")
// validate subscription
if user.Membership.Subscription.Name == "" {
sl.ReportError(user.Membership.Subscription.Name, "subscription.name", "name", "required", "")
} else {
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.SubscriptionModel.Name)
selectedModel, err := repositories.GetSubscriptionByName(&user.Membership.Subscription.Name)
if err != nil {
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 {
user.Membership.SubscriptionModel = *selectedModel
user.Membership.Subscription = *selectedModel
}
}
if isSupporter {
if user.IsSupporter() {
if user.BankAccount.IBAN != "" {
validateBankAccount(sl)
}
@@ -54,9 +58,9 @@ func ValidateUser(sl validator.StructLevel) {
if user.DateOfBirth.After(time.Now().AddDate(-18, 0, 0)) {
sl.ReportError(user.DateOfBirth, "user.user", "user.dateofbirth", "age", "")
}
validateMembership(sl)
validateMembership(db, &user, sl)
if isSuper {
if user.IsAdmin() {
return
}

View File

@@ -2,34 +2,6 @@ package errors
import "errors"
type ValidationKeys struct {
Invalid string
InternalServerError string
InvalidJson string
Unauthorized string
InvalidSubscriptionModel string
UserNotFoundWrongPassword string
JwtGenerationFailed string
Duplicate string
InvalidUserID string
PasswordAlreadyChanged string
UserDisabled string
NoAuthToken string
NotFound string
InUse string
UndeliveredVerificationMail string
}
type ValidationFields struct {
General string
ParentMemberShipID string
SubscriptionModel string
Login string
Email string
User string
Licences string
}
var (
ErrNotFound = errors.New("not found")
ErrUserNotFound = errors.New("user not found")
@@ -54,6 +26,37 @@ var (
ErrInvalidSubscriptionData = errors.New("Provided subscription data is invalid. Immutable fields where changed.")
)
type ValidationKeys struct {
Invalid string
InternalServerError string
InvalidJSON string
InvalidUserID string
InvalidSubscription string
Unauthorized string
UserNotFoundWrongPassword string
JwtGenerationFailed string
Duplicate string
UserDisabled string
PasswordAlreadyChanged string
NoAuthToken string
NotFound string
InUse string
UndeliveredVerificationMail string
UserAlreadyVerified string
}
type ValidationFields struct {
General string
ParentMembershipID string
Subscription string
Login string
Email string
User string
Licences string
Verification string
Car string
}
var Responses = struct {
Keys ValidationKeys
Fields ValidationFields
@@ -61,7 +64,9 @@ var Responses = struct {
Keys: ValidationKeys{
Invalid: "server.validation.invalid",
InternalServerError: "server.error.internal_server_error",
InvalidJson: "server.error.invalid_json",
InvalidJSON: "server.error.invalid_json",
InvalidUserID: "server.validation.invalid_user_id",
InvalidSubscription: "server.validation.invalid_subscription",
Unauthorized: "server.error.unauthorized",
UserNotFoundWrongPassword: "server.validation.user_not_found_or_wrong_password",
JwtGenerationFailed: "server.error.jwt_generation_failed",
@@ -72,14 +77,21 @@ var Responses = struct {
NotFound: "server.error.not_found",
InUse: "server.error.in_use",
UndeliveredVerificationMail: "server.error.undelivered_verification_mail",
UserAlreadyVerified: "server.validation.user_already_verified",
},
Fields: ValidationFields{
General: "server.general",
ParentMemberShipID: "parent_membership_id",
SubscriptionModel: "subscription_model",
ParentMembershipID: "parent_membership_id",
Subscription: "subscription",
Login: "user.login",
Email: "user.email",
User: "user.user",
Licences: "licence",
Verification: "verification",
Car: "car",
},
}
func Is(err error, target error) bool {
return errors.Is(err, target)
}

View File

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

View File

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

View File

@@ -0,0 +1,177 @@
<!doctype html>
<html>
<body>
<div
style="
background-color: #f2f5f7;
color: #242424;
font-family: Optima, Candara, &quot;Noto Sans&quot;, source-sans-pro,
sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: 0.15008px;
line-height: 1.5;
margin: 0;
padding: 32px 0;
min-height: 100%;
width: 100%;
"
>
<table
align="center"
width="100%"
style="margin: 0 auto; max-width: 600px; background-color: #ffffff"
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
>
<tbody>
<tr style="width: 100%">
<td>
<div style="padding: 24px 24px 24px 24px; text-align: center">
<a
href="{{.BASEURL}}"
style="text-decoration: none"
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.Logo}}"
style="
outline: none;
border: none;
text-decoration: none;
vertical-align: middle;
display: inline-block;
max-width: 100%;
"
/></a>
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
Moin {{.FirstName}} {{.LastName}} 👋,
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
hiermit erhältst Du Zugnag zu Deinem Dörpsmobil Hasloh Account.
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div style="font-weight: normal; padding: 16px 24px; text-align: center; font-family: Arial, sans-serif; color: #333;">
<p style="margin-bottom: 16px;">
Mit diesem Link kannst Du Dich dann in Deinen <strong>Dörpsmobil Hasloh</strong> Account anmelden:
</p>
<p style="margin: 16px 0; font-size: 18px;">
<a href="{{.BASEURL}}{{.FRONTEND_PATH}}/"
style="display: inline-block; padding: 12px 20px; background-color: #007bff; color: white; text-decoration: none; font-weight: bold; border-radius: 5px;">
{{.BASEURL}}{{.FRONTEND_PATH}}/
</a>
</p>
<p style="margin-top: 16px;">
Dafür musst Du aber zunächst noch Dein Passwort erstellen.
</p>
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href=" {{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?token={{.Token}}"
style="
color: #ffffff;
font-size: 26px;
font-weight: bold;
background-color: #3e9bfc;
border-radius: 4px;
display: block;
padding: 16px 32px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 32px;
mso-font-width: -100%;
mso-text-raise: 48;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>
Passwort erstellen
</span
><span
><!--[if mso
]><i
style="letter-spacing: 32px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div
style="
font-weight: normal;
text-align: center;
padding: 24px 24px 0px 24px;
"
>
Alternativ kannst Du auch diesen Link in Deinem Browser öffnen:
</div>
<div
style="
font-weight: bold;
text-align: center;
padding: 4px 24px 16px 24px;
"
>
{{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?token={{.Token}}
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Mit Freundlichen Grüßen,
</div>
<div
style="
font-weight: bold;
text-align: left;
padding: 16px 24px 16px 24px;
"
>
Der Vorstand
</div>
<div style="padding: 16px 24px 16px 24px">
<img
alt=""
src="{{.BASEURL}}/images/favicon_hu5543b2b337a87a169e2c722ef0122802_211442_96x0_resize_lanczos_3.png"
height="80"
width="80"
style="
outline: none;
border: none;
text-decoration: none;
object-fit: cover;
height: 80px;
width: 80px;
max-width: 100%;
display: inline-block;
vertical-align: middle;
text-align: center;
border-radius: 80px;
"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
Moin {{.FirstName}} {{.LastName}} 👋,
hiermit erhältst Du Zugnag zu Deinem Dörpsmobil Hasloh Account.
Mit diesem Link kannst Du Dich dann in Deinen Dörpsmobil Hasloh Account anmelden:
{{.BASEURL}}{{.FRONTEND_PATH}}/
einloggen.
Dafür musst Du aber zunächst noch Dein Passwort erstellen:
{{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?token={{.Token}}
Mit Freundlichen Grüßen,
Der Vorstand

View File

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

View File

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

View File

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

View File

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

View File

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