Compare commits

..

14 Commits

Author SHA1 Message Date
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
29 changed files with 1321 additions and 258 deletions

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

@@ -68,12 +68,52 @@ interface User {
notes: string | ''; 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;
damages: Damage[] | [];
insurances: Insurance[] | [];
notes: string | '';
}
interface Location {
latitude: number | 0;
longitude: number | 0;
}
interface Damage {
id: number | -1;
opponent: User;
insurance: Insurance | null;
notes: string | '';
}
interface Insurance {
id: number | -1;
company: string | '';
reference: string | '';
start_date: string | '';
end_date: string | '';
notes: string | '';
}
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
interface Locals { interface Locals {
user: User; user: User;
users: User[]; users: User[];
cars: Cars[];
subscriptions: Subscription[]; subscriptions: Subscription[];
licence_categories: LicenceCategory[]; licence_categories: LicenceCategory[];
} }
@@ -84,6 +124,10 @@ declare global {
licence: Licence; licence: Licence;
licenceCategory: LicenceCategory; licenceCategory: LicenceCategory;
bankAccount: BankAccount; bankAccount: BankAccount;
car: Car;
insurance: Insurance;
location: Location;
damage: Damage;
} }
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}

View File

@@ -0,0 +1,252 @@
<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 { defaultCar } from '$lib/utils/defaults';
import { PERMISSIONS } from '$lib/utils/constants';
const dispatch = createEventDispatcher();
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form;
/** @type {App.Locals['user'] } */
export let editor;
/** @type {App.Types['car'] | null} */
export let car;
console.log('Opening car modal with:', car);
$: car = car || { ...defaultCar() };
$: isLoading = car === undefined || editor === undefined;
let isUpdating = false;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
const TABS = ['car.car', 'insurance', 'car.damages'];
let activeTab = TABS[0];
/** @type {import('../../routes/auth/about/[id]/$types').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="susbscription[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'}">
{#each car.insurances as insurance}
<InputField
name="car[insurance][company]"
label={$t('company')}
bind:value={insurance.company}
placeholder={$t('placeholder.company')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][reference]"
label={$t('insurance.reference')}
bind:value={insurance.reference}
placeholder={$t('placeholder.insurance_reference')}
required={true}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][start_date]"
type="date"
label={$t('start')}
bind:value={insurance.start_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][end_date]"
type="date"
label={$t('end')}
bind:value={insurance.end_date}
readonly={readonlyUser}
/>
<InputField
name="car[insurance][notes]"
type="textarea"
label={$t('notes')}
bind:value={insurance.notes}
placeholder={$t('placeholder.notes', {
values: { name: insurance.company || '' }
})}
rows={10}
/>
{/each}
</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}
<style>
.tab-content {
padding: 1rem;
border-radius: 0 0 3px 3px;
background-color: var(--surface0);
border: 1px solid var(--surface1);
margin-top: 1rem;
}
.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

@@ -5,6 +5,7 @@
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers'; import { receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { defaultSubscription } from '$lib/utils/defaults';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -17,20 +18,8 @@
/** @type {App.Types['subscription'] | null} */ /** @type {App.Types['subscription'] | null} */
export let subscription; export let subscription;
/** @type {App.Types['subscription']} */
const blankSubscription = {
id: 0,
name: '',
details: '',
conditions: '',
monthly_fee: 0,
hourly_rate: 0,
included_hours_per_year: 0,
included_hours_per_month: 0
};
console.log('Opening subscription modal with:', subscription); console.log('Opening subscription modal with:', subscription);
$: subscription = subscription || { ...blankSubscription }; $: subscription = subscription || { ...defaultSubscription() };
$: isLoading = subscription === undefined || user === undefined; $: isLoading = subscription === undefined || user === undefined;
let isUpdating = false; let isUpdating = false;

View File

@@ -11,9 +11,15 @@ export default {
1: 'Mitglied', 1: 'Mitglied',
2: 'Betrachter', 2: 'Betrachter',
4: 'Bearbeiter', 4: 'Bearbeiter',
8: 'Administrator' 8: 'Adm/endinistrator'
}, },
placeholder: { placeholder: {
car_name: 'Hat das Fahrzeug einen Namen?',
car_brand: 'Fahrzeughersteller eingeben...',
car_model: 'Fahrzeugmodell eingeben...',
car_color: 'Fahrzeugfarbe eingeben...',
car_licence_plate: 'Fahrzeugkennzeichen eingeben...',
insurance_reference: 'Versicherungsnummer eingeben...',
password: 'Passwort eingeben...', password: 'Passwort eingeben...',
email: 'Emailadresse eingeben...', email: 'Emailadresse eingeben...',
company: 'Firmennamen eingeben...', company: 'Firmennamen eingeben...',
@@ -147,15 +153,29 @@ export default {
included_hours_per_year: 'Inkludierte Stunden pro Jahr', included_hours_per_year: 'Inkludierte Stunden pro Jahr',
included_hours_per_month: 'Inkludierte Stunden pro Monat' included_hours_per_month: 'Inkludierte Stunden pro Monat'
}, },
car: {
car: 'Fahrzeug',
model: 'Modell',
brand: 'Marke',
licence_plate: 'Kennzeichen',
edit: 'Fahrzeug bearbeiten',
create: 'Fahrzeug hinzufügen',
damages: 'Schäden',
start_date: 'Anschaffungsdatum',
end_date: 'Leasingende',
leasing_rate: 'Leasingrate'
},
loading: { loading: {
user_data: 'Lade Nutzerdaten', user_data: 'Lade Nutzerdaten',
subscription_data: 'Lade Modelldaten', subscription_data: 'Lade Modelldaten',
car_data: 'Lade Fahrzeugdaten',
please_wait: 'Bitte warten...', please_wait: 'Bitte warten...',
updating: 'Aktualisiere...' updating: 'Aktualisiere...'
}, },
dialog: { dialog: {
user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?', user_deletion: 'Soll der Nutzer {firstname} {lastname} wirklich gelöscht werden?',
subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?', subscription_deletion: 'Soll das Tarifmodell {name} wirklich gelöscht werden?',
car_deletion: 'Soll das Fahrzeug {name} wirklich gelöscht werden?',
backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?' backend_access: 'Soll {firstname} {lastname} Backend Zugriff gewährt werden?'
}, },
cancel: 'Abbrechen', cancel: 'Abbrechen',
@@ -165,14 +185,20 @@ export default {
delete: 'Löschen', delete: 'Löschen',
search: 'Suche:', search: 'Suche:',
name: 'Name', name: 'Name',
price: 'Preis',
color: 'Farbe',
grant_backend_access: 'Backend Zugriff gewähren', grant_backend_access: 'Backend Zugriff gewähren',
no_insurance: 'Keine Versicherung',
supporter: 'Sponsoren', supporter: 'Sponsoren',
mandate_date_signed: 'Mandatserteilungsdatum', mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen', licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell', subscription_model: 'Mitgliedschatfsmodell',
licence: 'Führerschein', licence: 'Führerschein',
licence_number: 'Führerscheinnummer', licence_number: 'Führerscheinnummer',
insurance: 'Versicherung',
insurance_reference: 'Versicherungsnummer',
issued_date: 'Ausgabedatum', issued_date: 'Ausgabedatum',
month: 'Monat',
expiration_date: 'Ablaufdatum', expiration_date: 'Ablaufdatum',
country: 'Land', country: 'Land',
details: 'Details', details: 'Details',
@@ -191,6 +217,7 @@ export default {
company: 'Firma', company: 'Firma',
login: 'Anmeldung', login: 'Anmeldung',
profile: 'Profil', profile: 'Profil',
cars: 'Fahrzeuge',
membership: 'Mitgliedschaft', membership: 'Mitgliedschaft',
bankaccount: 'Kontodaten', bankaccount: 'Kontodaten',
status: 'Status', status: 'Status',

View File

@@ -30,7 +30,7 @@ export default {
bic: 'Enter BIC (for non-German accounts)...', bic: 'Enter BIC (for non-German accounts)...',
mandate_reference: 'Enter SEPA mandate reference...', mandate_reference: 'Enter SEPA mandate reference...',
notes: 'Your notes about {name}...', notes: 'Your notes about {name}...',
licence_number: 'On the drivers license under field 5', licence_number: 'On the drivers licence under field 5',
issued_date: 'Issue date under field 4a', issued_date: 'Issue date under field 4a',
expiration_date: 'Expiration date under field 4b', expiration_date: 'Expiration date under field 4b',
issuing_country: 'Issuing country', issuing_country: 'Issuing country',
@@ -81,7 +81,7 @@ export default {
bic: 'Invalid. Format: BELADEBEXXX', bic: 'Invalid. Format: BELADEBEXXX',
email: 'Invalid format', email: 'Invalid format',
number: 'Is not a number', number: 'Is not a number',
euDriversLicence: 'Is not a European drivers license', euDriversLicence: 'Is not a European drivers licence',
lte: 'Is too large/new', lte: 'Is too large/new',
gt: 'Is too small/old', gt: 'Is too small/old',
required: 'Field is required', required: 'Field is required',
@@ -159,10 +159,10 @@ export default {
name: 'Name', name: 'Name',
supporter: 'Sponsors', supporter: 'Sponsors',
mandate_date_signed: 'Mandate Signing Date', mandate_date_signed: 'Mandate Signing Date',
licence_categories: 'Drivers License Categories', licence_categories: 'Drivers licence Categories',
subscription_model: 'Membership Model', subscription_model: 'Membership Model',
licence: 'Drivers License', licence: 'Drivers licence',
licence_number: 'Drivers License Number', licence_number: 'Drivers licence Number',
issued_date: 'Issue Date', issued_date: 'Issue Date',
expiration_date: 'Expiration Date', expiration_date: 'Expiration Date',
country: 'Country', country: 'Country',

View File

@@ -117,3 +117,62 @@ export function defaultSupporter() {
supporter.membership.subscription_model.name = SUPPORTER_SUBSCRIPTION_MODEL_NAME; supporter.membership.subscription_model.name = SUPPORTER_SUBSCRIPTION_MODEL_NAME;
return supporter; return supporter;
} }
/**
* @returns {App.Types['location']}
*/
export function defaultLocation() {
return {
latitude: 0,
longitude: 0
};
}
/**
* @returns {App.Types['damage']}
*/
export function defaultDamage() {
return {
id: 0,
opponent: defaultUser(),
insurance: null,
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: '',
brand: '',
model: '',
price: 0,
rate: 0,
start_date: '',
end_date: '',
color: '',
licence_plate: '',
location: defaultLocation(),
damages: [],
insurances: [],
notes: ''
};
}

View File

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

View File

@@ -3,10 +3,10 @@ import { toRFC3339 } from './helpers';
/** /**
* Converts FormData to a nested object structure * Converts FormData to a nested object structure
* @param {FormData} formData - The FormData object to convert * @param {FormData} formData - The FormData object to convert
* @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']>, confirm_password: string }} Nested object representation of the form data * @returns {{ object: Partial<App.Locals['user']> | Partial<App.Types['subscription']> | Partial<App.Types['car']>, confirm_password: string }} Nested object representation of the form data
*/ */
export function formDataToObject(formData) { export function formDataToObject(formData) {
/** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> } */ /** @type { Partial<App.Locals['user']> | Partial<App.Types['subscription']> | Partial<App.Types['car']> } */
const object = {}; const object = {};
let confirm_password = ''; let confirm_password = '';
@@ -142,8 +142,8 @@ export function processUserFormData(rawData) {
} }
/** /**
* Processes the raw form data into the expected user data structure * Processes the raw form data into the expected subscription data structure
* @param {{ object: Partial<App.Types['subscription']>} } rawData - The raw form data object * @param {{ object: Partial<App.Types['subscription']>, confirm_password: string }} rawData - The raw form data object
* @returns {{ subscription: Partial<App.Types['subscription']> }} Processed user data * @returns {{ subscription: Partial<App.Types['subscription']> }} Processed user data
*/ */
export function processSubscriptionFormData(rawData) { export function processSubscriptionFormData(rawData) {
@@ -166,3 +166,39 @@ export function processSubscriptionFormData(rawData) {
console.dir(clean); console.dir(clean);
return clean; return clean;
} }
/**
* Processes the raw form data into the expected car data structure
* @param {{ object: Partial<App.Types['car']>, confirm_password: string }} rawData - The raw form data object
* @returns {{ car: Partial<App.Types['car']> }} Processed user data
*/
export function processCarFormData(rawData) {
/** @type {{ car: Partial<App.Types['car']> }} */
let processedData = {
car: {
id: Number(rawData.object.id) || 0,
name: String(rawData.object.name) || '',
status: Number(rawData.object.status) || 0,
brand: String(rawData.object.brand) || '',
model: String(rawData.object.model) || '',
price: Number(rawData.object.price) || 0,
rate: Number(rawData.object.rate) || 0,
licence_plate: String(rawData.object.licence_plate) || '',
start_date: toRFC3339(String(rawData.object.start_date)) || '',
end_date: toRFC3339(String(rawData.object.end_date)) || '',
color: String(rawData.object.color) || '',
notes: String(rawData.object.notes) || '',
location: {
latitude: Number(rawData.object.location?.latitude) || 0,
longitude: Number(rawData.object.location?.longitude) || 0
},
damages: rawData.object.damages || [],
insurances: rawData.object.insurances || []
}
};
const clean = JSON.parse(JSON.stringify(processedData), (key, value) =>
value !== null && value !== '' ? value : undefined
);
console.dir(clean);
return clean;
}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/help
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { import {
formDataToObject, formDataToObject,
processCarFormData,
processSubscriptionFormData, processSubscriptionFormData,
processUserFormData processUserFormData
} from '$lib/utils/processing'; } from '$lib/utils/processing';
@@ -36,7 +37,12 @@ export const actions = {
updateUser: async ({ request, fetch, cookies, locals }) => { updateUser: async ({ request, fetch, cookies, locals }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
console.dir(processedData.user.membership); console.dir(processedData.user.membership);
@@ -96,7 +102,55 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify(processedData.subscription)
};
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 Error data or redirects user to the home page or the previous page
*/
updateCar: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['car']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['car']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processCarFormData(rawData);
const isCreating = !processedData.car.id || processedData.car.id === 0;
console.log('Is creating: ', isCreating);
console.log('sending: ', JSON.stringify(processedData.car));
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(processedData.car)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -123,7 +177,12 @@ export const actions = {
userDelete: async ({ request, fetch, cookies }) => { userDelete: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Locals['user']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Locals['user']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
const apiURL = `${BASE_API_URI}/auth/users`; const apiURL = `${BASE_API_URI}/auth/users`;
@@ -176,7 +235,7 @@ export const actions = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}` Cookie: `jwt=${cookies.get('jwt')}`
}, },
body: JSON.stringify(processedData) body: JSON.stringify(processedData.subscription)
}; };
const res = await fetch(apiURL, requestOptions); const res = await fetch(apiURL, requestOptions);
@@ -191,10 +250,23 @@ export const actions = {
console.log('Server success response:', response); console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`); throw redirect(303, `${base}/auth/admin/users`);
}, },
/**
*
* @param request - The request object
* @param fetch - Fetch object from sveltekit
* @param cookies - SvelteKit's cookie object
* @returns
*/
grantBackendAccess: async ({ request, fetch, cookies }) => { grantBackendAccess: async ({ request, fetch, cookies }) => {
let formData = await request.formData(); let formData = await request.formData();
const rawData = formDataToObject(formData); const rawFormData = formDataToObject(formData);
/** @type {{object: Partial<App.Types['subscription']>, confirm_password: string}} */
const rawData = {
object: /** @type {Partial<App.Types['subscription']>} */ (rawFormData.object),
confirm_password: rawFormData.confirm_password
};
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
console.dir(processedData); console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`; const apiURL = `${BASE_API_URI}/auth/users/activate`;

View File

@@ -8,7 +8,13 @@
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { hasPrivilige, receive, send } from '$lib/utils/helpers'; import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { PERMISSIONS } from '$lib/utils/constants'; import { PERMISSIONS } from '$lib/utils/constants';
import { defaultSupporter, defaultUser } from '$lib/utils/defaults'; import {
defaultCar,
defaultSubscription,
defaultSupporter,
defaultUser
} from '$lib/utils/defaults';
import CarEditForm from '$lib/components/CarEditForm.svelte';
/** @type {import('./$types').ActionData} */ /** @type {import('./$types').ActionData} */
export let form; export let form;
@@ -16,18 +22,17 @@
$: ({ $: ({
user = [], user = [],
users = [], users = [],
cars = [],
licence_categories = [], licence_categories = [],
subscriptions = [], subscriptions = [],
payments = [] payments = []
} = $page.data); } = $page.data);
let activeSection = 'members'; let activeSection = 'members';
/** @type{App.Locals['user'] | null} */
let selectedUser = null; /** @type{App.Types['car'] | App.Types['subscription'] | App.Locals['user'] | null} */
/** @type{App.Types['subscription'] | null} */ let selected = null;
let selectedSubscription = null;
let showSubscriptionModal = false;
let showUserModal = false;
let searchTerm = ''; let searchTerm = '';
$: members = users.filter((/** @type{App.Locals['user']} */ user) => { $: members = users.filter((/** @type{App.Locals['user']} */ user) => {
@@ -95,29 +100,8 @@
}); });
}; };
/**
* Opens the edit modal for the selected user.
* @param {App.Locals['user']} user The user to edit.
*/
const openEditUserModal = (user) => {
selectedUser = user;
showUserModal = true;
};
/**
* Opens the edit modal for the selected subscription.
* @param {App.Types['subscription'] | null} subscription The user to edit.
*/
const openEditSubscriptionModal = (subscription) => {
selectedSubscription = subscription;
showSubscriptionModal = true;
};
const close = () => { const close = () => {
showUserModal = false; selected = null;
showSubscriptionModal = false;
selectedUser = null;
selectedSubscription = null;
if (form) { if (form) {
form.errors = []; form.errors = [];
} }
@@ -167,6 +151,16 @@
<span class="nav-badge">{subscriptions.length}</span> <span class="nav-badge">{subscriptions.length}</span>
</button> </button>
</li> </li>
<li>
<button
class="nav-link {activeSection === 'cars' ? 'active' : ''}"
on:click={() => setActiveSection('cars')}
>
<i class="fas fa-car"></i>
{$t('cars')}
<span class="nav-badge">{cars.length}</span>
</button>
</li>
<li> <li>
<button <button
class="nav-link {activeSection === 'payments' ? 'active' : ''}" class="nav-link {activeSection === 'payments' ? 'active' : ''}"
@@ -214,7 +208,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultUser())}> <button
class="btn primary"
on:click={() => {
selected = defaultUser();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -288,7 +287,12 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> <button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -350,7 +354,12 @@
</button> </button>
</div> </div>
<div> <div>
<button class="btn primary" on:click={() => openEditUserModal(defaultSupporter())}> <button
class="btn primary"
on:click={() => {
selected = defaultSupporter();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -384,7 +393,12 @@
</tbody> </tbody>
</table> </table>
<div class="button-group"> <div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}> <button
class="btn primary"
on:click={() => {
selected = user;
}}
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </button>
@@ -429,7 +443,12 @@
<div class="section-header"> <div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2> <h2>{$t('subscription.subscriptions')}</h2>
{#if hasPrivilige(user, PERMISSIONS.Super)} {#if hasPrivilige(user, PERMISSIONS.Super)}
<button class="btn primary" on:click={() => openEditSubscriptionModal(null)}> <button
class="btn primary"
on:click={() => {
selected = defaultSubscription();
}}
>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
</button> </button>
@@ -488,7 +507,9 @@
<div class="button-group"> <div class="button-group">
<button <button
class="btn primary" class="btn primary"
on:click={() => openEditSubscriptionModal(subscription)} on:click={() => {
selected = subscription;
}}
> >
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
@@ -537,6 +558,112 @@
</details> </details>
{/each} {/each}
</div> </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
>
</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>
{#if hasPrivilige(user, PERMISSIONS.Update)}
<div class="button-group">
<button
class="btn primary"
on:click={() => {
selected = car;
}}
>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
<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="subscription[id]" value={car.id} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
</div>
{/if}
</div>
</details>
{/each}
</div>
{:else if activeSection === 'payments'} {:else if activeSection === 'payments'}
<h2>{$t('payments')}</h2> <h2>{$t('payments')}</h2>
<div class="accordion"> <div class="accordion">
@@ -567,28 +694,34 @@
</div> </div>
</div> </div>
{#if showUserModal && selectedUser !== null} {#if selected && 'email' in selected}
// user
<Modal on:close={close}> <Modal on:close={close}>
<UserEditForm <UserEditForm
{form} {form}
editor={user} editor={user}
user={selectedUser} user={selected}
{subscriptions} {subscriptions}
{licence_categories} {licence_categories}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if showSubscriptionModal} {:else if selected && 'monthly_fee' in selected}
//subscription
<Modal on:close={close}> <Modal on:close={close}>
<SubscriptionEditForm <SubscriptionEditForm
{form} {form}
{user} {user}
subscription={selectedSubscription} subscription={selected}
on:cancel={close} on:cancel={close}
on:close={close} on:close={close}
/> />
</Modal> </Modal>
{:else if selected && 'brand' in selected}
<Modal on:close={close}>
<CarEditForm {form} editor={user} car={selected} on:cancel={close} on:close={close} />
</Modal>
{/if} {/if}
<style> <style>

View File

@@ -0,0 +1,118 @@
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 {
Car struct {
ID uint `json:"id" binding:"required,numeric"`
} `json:"car"`
}
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.Car.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

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

View File

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

View File

@@ -75,7 +75,7 @@ func (uc *UserController) GetAllUsers(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"users": users, "users": safeUsers,
}) })
} }
@@ -138,8 +138,7 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
type deleteData struct { type deleteData struct {
User struct { User struct {
ID uint `json:"id" binding:"required,numeric"` ID uint `json:"id" binding:"required,numeric"`
LastName string `json:"last_name"`
} `json:"user"` } `json:"user"`
} }

View File

@@ -245,12 +245,12 @@ func testLoginHandler(t *testing.T) string {
if cookie.Name == "jwt" { if cookie.Name == "jwt" {
MemberCookie = cookie MemberCookie = cookie
// tokenString := loginCookie.Value tokenString := cookie.Value
// _, claims, err := middlewares.ExtractContentFrom(tokenString) _, claims, err := middlewares.ExtractContentFrom(tokenString)
// assert.NoError(t, err, "FAiled getting cookie string") assert.NoError(t, err, "FAiled getting cookie string")
// jwtUserID := uint((*claims)["user_id"].(float64)) jwtUserID := uint((*claims)["user_id"].(float64))
// user, err := Uc.Service.GetUserByID(jwtUserID) _, err = Uc.Service.FromID(&jwtUserID)
// assert.NoError(t, err, "FAiled getting cookie string") assert.NoError(t, err, "FAiled getting cookie string")
// logger.Error.Printf("cookie user: %#v", user) // logger.Error.Printf("cookie user: %#v", user)
err = json.Unmarshal([]byte(tt.input), &loginInput) err = json.Unmarshal([]byte(tt.input), &loginInput)
@@ -1254,35 +1254,35 @@ func getTestUsers() []RegisterUserTest {
// return user // return user
// })), // })),
// }, // },
// { {
// Name: "empty driverslicence number, should fail", Name: "empty driverslicence number, should fail",
// WantResponse: http.StatusBadRequest, WantResponse: http.StatusBadRequest,
// WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
// Assert: false, Assert: false,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.wronglicence.doe@example.com" user.Email = "john.wronglicence.doe@example.com"
// user.Licence = &models.Licence{ user.Licence = &models.Licence{
// Number: "", Number: "",
// ExpirationDate: time.Now().AddDate(1, 0, 0), ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0), IssuedDate: time.Now().AddDate(-1, 0, 0),
// } }
// return user return user
// })), })),
// }, },
// { {
// Name: "Correct Licence number, should pass", Name: "Correct Licence number, should pass",
// WantResponse: http.StatusCreated, WantResponse: http.StatusCreated,
// WantDBData: map[string]interface{}{"email": "john.correctLicenceNumber@example.com"}, WantDBData: map[string]interface{}{"email": "john.correctLicenceNumber@example.com"},
// Assert: true, Assert: true,
// Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
// user.Email = "john.correctLicenceNumber@example.com" user.Email = "john.correctLicenceNumber@example.com"
// user.Licence = &models.Licence{ user.Licence = &models.Licence{
// Number: "B072RRE2I55", Number: "B072RRE2I55",
// ExpirationDate: time.Now().AddDate(1, 0, 0), ExpirationDate: time.Now().AddDate(1, 0, 0),
// IssuedDate: time.Now().AddDate(-1, 0, 0), IssuedDate: time.Now().AddDate(-1, 0, 0),
// } }
// return user return user
// })), })),
// }, },
} }
} }

View File

@@ -29,6 +29,10 @@ func Open(dbPath string, adminMail string) (*gorm.DB, error) {
&models.Verification{}, &models.Verification{},
&models.Licence{}, &models.Licence{},
&models.Category{}, &models.Category{},
&models.Insurance{},
&models.Car{},
&models.Location{},
&models.Damage{},
&models.BankAccount{}); err != nil { &models.BankAccount{}); err != nil {
logger.Error.Fatalf("Couldn't create database: %v", err) logger.Error.Fatalf("Couldn't create database: %v", err)
return nil, err return nil, err

View File

@@ -0,0 +1,13 @@
package models
import "time"
type Insurance struct {
ID uint `gorm:"primary_key" json:"id"`
OwnerID uint `gorm:"not null" json:"owner_id" binding:"numeric"`
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"`
}

View File

@@ -3,13 +3,13 @@ package models
import "time" import "time"
type BankAccount struct { type BankAccount struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
MandateDateSigned time.Time `gorm:"not null" json:"mandate_date_signed"` MandateDateSigned time.Time `gorm:"not null" json:"mandate_date_signed"`
Bank string `json:"bank_name" binding:"safe_content"` Bank string `json:"bank_name" binding:"safe_content"`
AccountHolderName string `json:"account_holder_name" binding:"safe_content"` AccountHolderName string `json:"account_holder_name" binding:"safe_content"`
IBAN string `json:"iban"` IBAN string `json:"iban" binding:"safe_content"`
BIC string `json:"bic"` BIC string `json:"bic" binding:"safe_content"`
MandateReference string `gorm:"not null" json:"mandate_reference"` MandateReference string `gorm:"not null" json:"mandate_reference" binding:"safe_content"`
ID uint `gorm:"primaryKey"`
} }

View File

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

View File

@@ -5,13 +5,13 @@ import (
) )
type SubscriptionModel struct { type SubscriptionModel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Name string `gorm:"unique" json:"name" binding:"required"` Name string `gorm:"uniqueIndex:idx_subscriptions_name" json:"name" binding:"required,safe_content"`
Details string `json:"details"` Details string `json:"details" binding:"safe_content"`
Conditions string `json:"conditions"` Conditions string `json:"conditions" binding:"safe_content"`
RequiredMembershipField string `json:"required_membership_field"` RequiredMembershipField string `json:"required_membership_field" binding:"safe_content"`
ID uint `json:"id" gorm:"primaryKey"`
MonthlyFee float32 `json:"monthly_fee"` MonthlyFee float32 `json:"monthly_fee"`
HourlyRate float32 `json:"hourly_rate"` HourlyRate float32 `json:"hourly_rate"`
IncludedPerYear int16 `json:"included_hours_per_year"` IncludedPerYear int16 `json:"included_hours_per_year"`

View File

@@ -21,26 +21,26 @@ type User struct {
ID uint `gorm:"primarykey" json:"id"` ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time
DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"` DateOfBirth time.Time `gorm:"not null" json:"dateofbirth" binding:"required_unless=RoleID 0,safe_content"`
Company string `json:"company" binding:"omitempty,omitnil,safe_content"` Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"` Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" binding:"safe_content"` Notes string `json:"notes" binding:"safe_content"`
FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
Password string `json:"password" binding:"safe_content"` Password string `json:"password" binding:"safe_content"`
Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"` Email string `gorm:"uniqueIndex:idx_users_email,not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"` ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"`
Address string `gorm:"not null" json:"address" binding:"required,safe_content"` Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"` ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"` City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"` Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"` BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"bank_account"`
BankAccountID uint BankAccountID uint
Verifications *[]Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` Verifications *[]Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"` Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"membership"`
MembershipID uint MembershipID uint
Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"licence"` Licence *Licence `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"licence"`
LicenceID uint LicenceID uint
PaymentStatus int8 `json:"payment_status"` PaymentStatus int8 `json:"payment_status"`
Status int8 `json:"status"` Status int8 `json:"status"`

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

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

View File

@@ -48,6 +48,8 @@ func Run(db *gorm.DB) {
membershipController := &controllers.MembershipController{Service: membershipService, UserService: userService} membershipController := &controllers.MembershipController{Service: membershipService, UserService: userService}
licenceController := &controllers.LicenceController{Service: licenceService} licenceController := &controllers.LicenceController{Service: licenceService}
contactController := &controllers.ContactController{EmailService: emailService} contactController := &controllers.ContactController{EmailService: emailService}
carService := &services.CarService{DB: db}
carController := &controllers.CarController{S: carService, UserService: userService}
router := gin.Default() router := gin.Default()
// gin.SetMode(gin.ReleaseMode) // gin.SetMode(gin.ReleaseMode)
@@ -63,7 +65,7 @@ func Run(db *gorm.DB) {
limiter := middlewares.NewIPRateLimiter(config.Security.Ratelimits.Limit, config.Security.Ratelimits.Burst) limiter := middlewares.NewIPRateLimiter(config.Security.Ratelimits.Limit, config.Security.Ratelimits.Burst)
router.Use(middlewares.RateLimitMiddleware(limiter)) router.Use(middlewares.RateLimitMiddleware(limiter))
routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController) routes.RegisterRoutes(router, userController, membershipController, contactController, licenceController, carController)
validation.SetupValidators(db) validation.SetupValidators(db)
logger.Info.Println("Starting server on :8080") logger.Info.Println("Starting server on :8080")

View File

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

View File

@@ -3,6 +3,7 @@ package validation
import ( import (
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
@@ -14,8 +15,8 @@ func ValidateSubscription(sl validator.StructLevel) {
if subscription.Name == "" { if subscription.Name == "" {
sl.ReportError(subscription.Name, "Name", "name", "required", "") sl.ReportError(subscription.Name, "Name", "name", "required", "")
} }
logger.Error.Printf("parent.type.name: %#v", sl.Parent().Type().Name())
if sl.Parent().Type().Name() == "MembershipData" { if sl.Parent().Type().Name() == "" {
// This is modifying a subscription directly // This is modifying a subscription directly
if subscription.Details == "" { if subscription.Details == "" {
sl.ReportError(subscription.Details, "Details", "details", "required", "") sl.ReportError(subscription.Details, "Details", "details", "required", "")

View File

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