Compare commits
10 Commits
fcfc8ad1e0
...
5e32c6b431
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e32c6b431 | ||
|
|
7ed986d122 | ||
|
|
58daf7bf30 | ||
|
|
d3365ae065 | ||
|
|
8b3e8a0579 | ||
|
|
ab8d143aeb | ||
|
|
47e4e8ce55 | ||
|
|
20012b729e | ||
|
|
975e3121a5 | ||
|
|
56a28bbff7 |
@@ -1,4 +1,5 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants.js";
|
||||
import { refreshCookie, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
export async function handle({ event, resolve }) {
|
||||
@@ -30,52 +31,29 @@ export async function handle({ event, resolve }) {
|
||||
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
if (newToken) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
event.cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
refreshCookie(newToken, event);
|
||||
|
||||
event.locals.subscriptions = data.subscriptions;
|
||||
userDatesFromRFC3339(data.user);
|
||||
|
||||
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
]);
|
||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||
subscriptionsResponse.json(),
|
||||
licenceCategoriesResponse.json(),
|
||||
]);
|
||||
event.locals.user = data.user;
|
||||
event.locals.licence_categories = data.licence_categories;
|
||||
console.dir(event.locals.user);
|
||||
if (event.locals.user.date_of_birth) {
|
||||
event.locals.user.date_of_birth =
|
||||
event.locals.user.date_of_birth.split("T")[0];
|
||||
}
|
||||
if (event.locals.user.membership) {
|
||||
if (event.locals.user.membership.start_date) {
|
||||
event.locals.user.membership.start_date =
|
||||
event.locals.user.membership.start_date.split("T")[0];
|
||||
}
|
||||
if (event.locals.user.membership.end_date) {
|
||||
event.locals.user.membership.end_date =
|
||||
event.locals.user.membership.end_date.split("T")[0];
|
||||
}
|
||||
}
|
||||
if (event.locals.user.licence?.issued_date) {
|
||||
event.locals.user.licence.issued_date =
|
||||
event.locals.user.licence.issued_date.split("T")[0];
|
||||
}
|
||||
if (event.locals.user.licence?.expiration_date) {
|
||||
event.locals.user.licence.expiration_date =
|
||||
event.locals.user.licence.expiration_date.split("T")[0];
|
||||
}
|
||||
if (
|
||||
event.locals.user.bank_account &&
|
||||
event.locals.user.bank_account.mandate_date_signed
|
||||
) {
|
||||
event.locals.user.bank_account.mandate_date_signed =
|
||||
event.locals.user.bank_account.mandate_date_signed.split("T")[0];
|
||||
}
|
||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
||||
// console.log("hooks.server: Printing locals:");
|
||||
// console.dir(event.locals);
|
||||
|
||||
// load page as normal
|
||||
return await resolve(event);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { applyAction, enhance } from "$app/forms";
|
||||
import { page } from "$app/stores";
|
||||
// import Developer from "$lib/img/hero-image.png";
|
||||
import Avatar from "$lib/img/TeamAvatar.jpeg";
|
||||
// import Avatar from "$lib/img/TeamAvatar.jpeg";
|
||||
import { t } from "svelte-i18n";
|
||||
onMount(() => {
|
||||
console.log("Page data in Header:", $page);
|
||||
});
|
||||
@@ -45,14 +46,22 @@
|
||||
{:else}
|
||||
<div class="header-nav-item">
|
||||
<a href="/auth/about/{$page.data.user.id}">
|
||||
<img
|
||||
src={$page.data.user.profile_picture
|
||||
? $page.data.user.profile_picture
|
||||
: Avatar}
|
||||
<!-- <img
|
||||
src={$page.data.user.profile_picture ? $page.data.user.profile_picture : Avatar}
|
||||
alt={`${$page.data.user.first_name} ${$page.data.user.last_name}`}
|
||||
/>
|
||||
/> -->
|
||||
{$page.data.user.first_name}
|
||||
{$page.data.user.last_name}
|
||||
</a>
|
||||
</div>
|
||||
{#if $page.data.user.role_id > 0}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
class:active={$page.url.pathname.startsWith("/auth/admin/users")}
|
||||
>
|
||||
<a href="/auth/admin/users">{$t("user.management")}</a>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- {#if $page.data.user.is_superuser}
|
||||
<div
|
||||
class="header-nav-item"
|
||||
|
||||
@@ -11,8 +11,56 @@
|
||||
/** @type {App.Locals['subscriptions']}*/
|
||||
export let subscriptions;
|
||||
|
||||
/** @type {App.Locals['user']}*/
|
||||
/** @type {App.Locals['user'] | null} */
|
||||
export let user;
|
||||
if (user == null) {
|
||||
user = {
|
||||
id: 0,
|
||||
email: "",
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
zip_code: "",
|
||||
city: "",
|
||||
company: "",
|
||||
date_of_birth: "",
|
||||
notes: "",
|
||||
profile_picture: "",
|
||||
payment_status: 0,
|
||||
status: 1,
|
||||
role_id: 0,
|
||||
membership: {
|
||||
id: 0,
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
status: 3,
|
||||
parent_member_id: 0,
|
||||
subscription_model: {
|
||||
id: 0,
|
||||
name: "",
|
||||
},
|
||||
},
|
||||
licence: {
|
||||
id: 0,
|
||||
status: 1,
|
||||
licence_number: "",
|
||||
issued_date: "",
|
||||
expiration_date: "",
|
||||
country: "",
|
||||
licence_categories: [],
|
||||
},
|
||||
bank_account: {
|
||||
id: 0,
|
||||
mandate_date_signed: "",
|
||||
bank: "",
|
||||
account_holder_name: "",
|
||||
iban: "",
|
||||
bic: "",
|
||||
mandate_reference: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {App.Locals['licence_categories']} */
|
||||
export let licence_categories;
|
||||
@@ -113,7 +161,7 @@
|
||||
use:enhance={handleUpdate}
|
||||
>
|
||||
<input name="id" type="number" hidden bind:value={user.id} />
|
||||
<h1 class="step-title" style="text-align: center;">{$t("user_edit")}</h1>
|
||||
<h1 class="step-title" style="text-align: center;">{$t("user.edit")}</h1>
|
||||
{#if form?.success}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
@@ -170,7 +218,7 @@
|
||||
<InputField
|
||||
name="role_id"
|
||||
type="select"
|
||||
label={$t("user_role")}
|
||||
label={$t("user.role")}
|
||||
bind:value={user.role_id}
|
||||
options={userRoleOptions}
|
||||
/>
|
||||
|
||||
@@ -98,8 +98,22 @@ export default {
|
||||
L: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 40km/h",
|
||||
T: "Land-, Forstwirtschaftsfahrzeuge, Stapler max 60km/h",
|
||||
},
|
||||
user: {
|
||||
login: "Nutzer Anmeldung",
|
||||
edit: "Nutzer bearbeiten",
|
||||
user: "Nutzer",
|
||||
management: "Mitgliederverwaltung",
|
||||
id: "Mitgliedsnr",
|
||||
name: "Name",
|
||||
email: "Email",
|
||||
status: "Status",
|
||||
role: "Nutzerrolle",
|
||||
},
|
||||
cancel: "Abbrechen",
|
||||
confirm: "Bestätigen",
|
||||
actions: "Aktionen",
|
||||
edit: "Bearbeiten",
|
||||
delete: "Löschen",
|
||||
mandate_date_signed: "Mandatserteilungsdatum",
|
||||
licence_categories: "Führerscheinklassen",
|
||||
subscription_model: "Mitgliedschatfsmodell",
|
||||
@@ -112,7 +126,6 @@ export default {
|
||||
hourly_rate: "Stundensatz",
|
||||
details: "Details",
|
||||
conditions: "Bedingungen",
|
||||
user_role: "Nutzerrolle",
|
||||
unknown: "Unbekannt",
|
||||
notes: "Notizen",
|
||||
address: "Straße & Hausnummer",
|
||||
@@ -124,14 +137,12 @@ export default {
|
||||
email: "Email",
|
||||
company: "Firma",
|
||||
login: "Anmeldung",
|
||||
user: "Nutzer",
|
||||
user_login: "Nutzer Anmeldung",
|
||||
user_edit: "Nutzer bearbeiten",
|
||||
profile: "Profil",
|
||||
membership: "Mitgliedschaft",
|
||||
bankaccount: "Kontodaten",
|
||||
first_name: "Vorname",
|
||||
last_name: "Nachname",
|
||||
name: "Name",
|
||||
phone: "Telefonnummer",
|
||||
birth_date: "Geburtstag",
|
||||
status: "Status",
|
||||
|
||||
@@ -69,6 +69,81 @@ export function isEmpty(obj) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function toRFC3339(dateString) {
|
||||
if (!dateString) dateString = "0001-01-01T00:00:00.000Z";
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function fromRFC3339(dateString) {
|
||||
if (!dateString) dateString = "0001-01-01T00:00:00.000Z";
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {App.Locals.User} user - The user object to format
|
||||
*/
|
||||
export function userDatesFromRFC3339(user) {
|
||||
if (user.date_of_birth) {
|
||||
user.date_of_birth = fromRFC3339(user.date_of_birth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = fromRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = fromRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = fromRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = fromRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = fromRFC3339(
|
||||
user.bank_account.mandate_date_signed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats dates in the user object to RFC3339 format
|
||||
* @param {App.Locals.User} user - The user object to format
|
||||
*/
|
||||
export function userDatesToRFC3339(user) {
|
||||
if (user.date_of_birth) {
|
||||
user.date_of_birth = toRFC3339(user.date_of_birth);
|
||||
}
|
||||
if (user.membership) {
|
||||
if (user.membership.start_date) {
|
||||
user.membership.start_date = toRFC3339(user.membership.start_date);
|
||||
}
|
||||
if (user.membership.end_date) {
|
||||
user.membership.end_date = toRFC3339(user.membership.end_date);
|
||||
}
|
||||
}
|
||||
if (user.licence?.issued_date) {
|
||||
user.licence.issued_date = toRFC3339(user.licence.issued_date);
|
||||
}
|
||||
if (user.licence?.expiration_date) {
|
||||
user.licence.expiration_date = toRFC3339(user.licence.expiration_date);
|
||||
}
|
||||
if (user.bank_account && user.bank_account.mandate_date_signed) {
|
||||
user.bank_account.mandate_date_signed = toRFC3339(
|
||||
user.bank_account.mandate_date_signed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} obj - The error object to format
|
||||
* @returns {array} The formatted error object
|
||||
*/
|
||||
export function formatError(obj) {
|
||||
const errors = [];
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
@@ -98,3 +173,33 @@ export function formatError(obj) {
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | null} newToken - The new token for the cookie to set
|
||||
* @param {import('RequestEvent<Partial<Record<string, string>>, string | null>')} event - The event object
|
||||
*/
|
||||
export function refreshCookie(newToken, event) {
|
||||
if (newToken) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
if (event) {
|
||||
event.cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
} else {
|
||||
cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// @ts-nocheck
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { crossfade } from "svelte/transition";
|
||||
|
||||
export const [send, receive] = crossfade({
|
||||
duration: (d) => Math.sqrt(d * 200),
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
fallback(node, params) {
|
||||
const style = getComputedStyle(node);
|
||||
const transform = style.transform === "none" ? "" : style.transform;
|
||||
|
||||
return {
|
||||
duration: 600,
|
||||
easing: quintOut,
|
||||
css: (t) => `
|
||||
transform: ${transform} scale(${t});
|
||||
opacity: ${t}
|
||||
`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates an email field
|
||||
* @file lib/utils/helpers/input.validation.ts
|
||||
* @param {string} email - The email to validate
|
||||
*/
|
||||
export const isValidEmail = (email) => {
|
||||
const EMAIL_REGEX =
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
|
||||
return EMAIL_REGEX.test(email.trim());
|
||||
};
|
||||
/**
|
||||
* Validates a strong password field
|
||||
* @file lib/utils/helpers/input.validation.ts
|
||||
* @param {string} password - The password to validate
|
||||
*/
|
||||
export const isValidPasswordStrong = (password) => {
|
||||
const strongRegex = new RegExp(
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})"
|
||||
);
|
||||
|
||||
return strongRegex.test(password.trim());
|
||||
};
|
||||
/**
|
||||
* Validates a medium password field
|
||||
* @file lib/utils/helpers/input.validation.ts
|
||||
* @param {string} password - The password to validate
|
||||
*/
|
||||
export const isValidPasswordMedium = (password) => {
|
||||
const mediumRegex = new RegExp(
|
||||
"^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})"
|
||||
);
|
||||
|
||||
return mediumRegex.test(password.trim());
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether or not an object is empty.
|
||||
* @param {Record<string, string>} obj - The object to test
|
||||
* @returns `true` or `false`
|
||||
*/
|
||||
|
||||
export function isEmpty(obj) {
|
||||
for (const _i in obj) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} FormattedError
|
||||
* @property {string} error - The error message
|
||||
* @property {number} id - A unique identifier for the error
|
||||
*/
|
||||
/**
|
||||
* Format Error object(s)
|
||||
* @param {any} obj - The object to test
|
||||
* @returns @type {FormattedError[]}
|
||||
*/
|
||||
|
||||
export function formatError(obj) {
|
||||
/** @type {FormattedError[]} */
|
||||
const errors = [];
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((/** @type {Object} */ error) => {
|
||||
Object.keys(error).map((k) => {
|
||||
errors.push({
|
||||
error: error[k],
|
||||
id: Math.random() * 1000,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(obj).map((k) => {
|
||||
errors.push({
|
||||
error: obj[k],
|
||||
id: Math.random() * 1000,
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push({
|
||||
error: obj.charAt(0).toUpperCase() + obj.slice(1),
|
||||
id: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function toRFC3339(dateString) {
|
||||
if (!dateString) dateString = "0001-01-01T00:00:00.000Z";
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString();
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/** @type {import('./$types').LayoutLoad} */
|
||||
export async function load({ fetch, url, data }) {
|
||||
const { user } = data;
|
||||
return { fetch, url: url.pathname, user };
|
||||
const user = data.user;
|
||||
const subscriptions = data.subscriptions;
|
||||
const licence_categories = data.licence_categories;
|
||||
return { fetch, url: url.pathname, user, subscriptions, licence_categories };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { refreshCookie } from "$lib/utils/helpers";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ locals }) {
|
||||
export async function load({ locals, cookies }) {
|
||||
return {
|
||||
user: locals.user,
|
||||
licence_categories: locals.licence_categories,
|
||||
subscriptions: locals.subscriptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/** @type {import('./$types').LayoutLoad} */
|
||||
export async function load({ fetch, url, data }) {
|
||||
const { user, subscriptions, licence_categories } = data;
|
||||
return { fetch, url: url.pathname, user, subscriptions, licence_categories };
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, fetch, locals }) {
|
||||
const jwt = cookies.get("jwt");
|
||||
try {
|
||||
// Fetch user data, subscriptions, and licence categories in parallel
|
||||
const [subscriptionsResponse, licenceCategoriesResponse] =
|
||||
await Promise.all([
|
||||
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Check if any of the responses are not ok
|
||||
if (!subscriptionsResponse.ok || !licenceCategoriesResponse.ok) {
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
throw new Error("One or more API requests failed");
|
||||
}
|
||||
|
||||
// Parse the JSON responses
|
||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||
subscriptionsResponse.json(),
|
||||
licenceCategoriesResponse.json(),
|
||||
]);
|
||||
// Check if the server sent a new token
|
||||
const newToken =
|
||||
subscriptionsResponse.headers.get("Set-Cookie") == null
|
||||
? licenceCategoriesResponse.headers.get("Set-Cookie")
|
||||
: subscriptionsResponse.headers.get("Set-Cookie");
|
||||
|
||||
if (newToken) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
cookies.set("jwt", match[1], {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production", // Secure in production
|
||||
sameSite: "lax",
|
||||
maxAge: 5 * 24 * 60 * 60, // 5 days in seconds
|
||||
});
|
||||
}
|
||||
}
|
||||
console.dir(subscriptionsData);
|
||||
console.dir(licence_categoriesData);
|
||||
return {
|
||||
user: locals.user,
|
||||
subscriptions: subscriptionsData.subscriptions,
|
||||
licence_categories: licence_categoriesData.licence_categories,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
// In case of any error, clear the JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
return {
|
||||
user: locals.user,
|
||||
subscriptions: null,
|
||||
licence_categories: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { formatError } from "$lib/utils/helpers";
|
||||
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { toRFC3339 } from "$lib/utils/utils";
|
||||
import { toRFC3339 } from "$lib/utils/helpers";
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals, params }) {
|
||||
@@ -36,6 +36,7 @@ export const actions = {
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
/** @type {Partial<App.Locals['user']>} */
|
||||
const updateData = {
|
||||
id: Number(formData.get("id")),
|
||||
@@ -111,31 +112,7 @@ export const actions = {
|
||||
|
||||
const response = await res.json();
|
||||
locals.user = response;
|
||||
|
||||
// Format dates
|
||||
if (locals.user.date_of_birth) {
|
||||
locals.user.date_of_birth = response.date_of_birth.split("T")[0];
|
||||
}
|
||||
if (locals.user.membership?.start_date) {
|
||||
locals.user.membership.start_date =
|
||||
locals.user.membership.start_date.split("T")[0];
|
||||
}
|
||||
if (locals.user.membership?.end_date) {
|
||||
locals.user.membership.end_date =
|
||||
locals.user.membership.end_date.split("T")[0];
|
||||
}
|
||||
if (locals.user.bank_account?.mandate_date_signed) {
|
||||
locals.user.bank_account.mandate_date_signed =
|
||||
locals.user.bank_account.mandate_date_signed.split("T")[0];
|
||||
}
|
||||
if (locals.user.licence?.issued_date) {
|
||||
locals.user.licence.issued_date =
|
||||
locals.user.licence.issued_date.split("T")[0];
|
||||
}
|
||||
if (locals.user.licence?.expiration_date) {
|
||||
locals.user.licence.expiration_date =
|
||||
locals.user.licence.expiration_date.split("T")[0];
|
||||
}
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `/auth/about/${response.id}`);
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script>
|
||||
import SmallLoader from "$lib/components/SmallLoader.svelte";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { applyAction, enhance } from "$app/forms";
|
||||
import { page } from "$app/stores";
|
||||
import { receive, send } from "$lib/utils/helpers";
|
||||
import { t } from "svelte-i18n";
|
||||
import { fly } from "svelte/transition";
|
||||
import UserEditForm from "$lib/components/UserEditForm.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
$: ({ user, subscriptions, licence_categories } = $page.data);
|
||||
$: ({ user, licence_categories, subscriptions } = $page.data);
|
||||
|
||||
let showModal = false;
|
||||
|
||||
|
||||
8
frontend/src/routes/auth/admin/users/+layout.js
Normal file
8
frontend/src/routes/auth/admin/users/+layout.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('./$types').LayoutLoad} */
|
||||
export async function load({ fetch, url, data }) {
|
||||
const { users } = data;
|
||||
return {
|
||||
users: data.users,
|
||||
user: data.user,
|
||||
};
|
||||
}
|
||||
56
frontend/src/routes/auth/admin/users/+layout.server.js
Normal file
56
frontend/src/routes/auth/admin/users/+layout.server.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { userDatesFromRFC3339, refreshCookie } from "$lib/utils/helpers";
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, fetch, locals }) {
|
||||
// if (locals.users) {
|
||||
// return {
|
||||
// users: locals.users,
|
||||
// user: locals.user,
|
||||
// };
|
||||
// }
|
||||
|
||||
const jwt = cookies.get("jwt");
|
||||
try {
|
||||
// Fetch user data, subscriptions, and licence categories in parallel
|
||||
const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
throw redirect(302, "/auth/login?next=/");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
refreshCookie(newToken, null);
|
||||
|
||||
/** @type {App.Locals['users']}*/
|
||||
const users = data.users;
|
||||
|
||||
users.forEach((user) => {
|
||||
userDatesFromRFC3339(user);
|
||||
});
|
||||
|
||||
locals.users = users;
|
||||
return {
|
||||
subscriptions: locals.subscriptions,
|
||||
licence_categories: locals.licence_categories,
|
||||
users: locals.users,
|
||||
user: locals.user,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
// In case of any error, clear the JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
|
||||
throw redirect(302, "/auth/login?next=/");
|
||||
}
|
||||
}
|
||||
122
frontend/src/routes/auth/admin/users/+page.server.js
Normal file
122
frontend/src/routes/auth/admin/users/+page.server.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// - Add authentication check to ensure only admins can access this route.
|
||||
// - Implement a load function to fetch a list of all users.
|
||||
// - Create actions for updating user information (similar to the about/[id] route).
|
||||
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { formatError, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { toRFC3339 } from "$lib/utils/helpers";
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ locals, params }) {
|
||||
// redirect user if not logged in
|
||||
if (!locals.user) {
|
||||
throw redirect(302, `/auth/login?next=/auth/users`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions} */
|
||||
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 user
|
||||
* @returns Error data or redirects user to the home page or the previous page
|
||||
*/
|
||||
updateUser: async ({ request, fetch, cookies, locals }) => {
|
||||
let formData = await request.formData();
|
||||
|
||||
const licenceCategories = formData
|
||||
.getAll("licence_categories[]")
|
||||
.filter((value) => typeof value === "string")
|
||||
.map((value) => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse licence category:", value);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
/** @type {Partial<App.Locals['user']>} */
|
||||
const updateData = {
|
||||
id: Number(formData.get("id")),
|
||||
first_name: String(formData.get("first_name")),
|
||||
last_name: String(formData.get("last_name")),
|
||||
email: String(formData.get("email")),
|
||||
phone: String(formData.get("phone")),
|
||||
notes: String(formData.get("notes")),
|
||||
address: String(formData.get("address")),
|
||||
zip_code: String(formData.get("zip_code")),
|
||||
city: String(formData.get("city")),
|
||||
date_of_birth: toRFC3339(formData.get("birth_date")),
|
||||
company: String(formData.get("company")),
|
||||
profile_picture: String(formData.get("profile_picture")),
|
||||
membership: {
|
||||
id: Number(formData.get("membership_id")),
|
||||
start_date: toRFC3339(formData.get("membership_start_date")),
|
||||
end_date: toRFC3339(formData.get("membership_end_date")),
|
||||
status: Number(formData.get("membership_status")),
|
||||
parent_member_id: Number(formData.get("parent_member_id")),
|
||||
subscription_model: {
|
||||
id: Number(formData.get("subscription_model_id")),
|
||||
name: String(formData.get("subscription_model_name")),
|
||||
},
|
||||
},
|
||||
bank_account: {
|
||||
id: Number(formData.get("bank_account_id")),
|
||||
mandate_date_signed: toRFC3339(
|
||||
String(formData.get("mandate_date_signed"))
|
||||
),
|
||||
bank: String(formData.get("bank")),
|
||||
account_holder_name: String(formData.get("account_holder_name")),
|
||||
iban: String(formData.get("iban")),
|
||||
bic: String(formData.get("bic")),
|
||||
mandate_reference: String(formData.get("mandate_reference")),
|
||||
},
|
||||
licence: {
|
||||
id: Number(formData.get("drivers_licence_id")),
|
||||
status: Number(formData.get("licence_status")),
|
||||
licence_number: String(formData.get("licence_number")),
|
||||
issued_date: toRFC3339(formData.get("issued_date")),
|
||||
expiration_date: toRFC3339(formData.get("expiration_date")),
|
||||
country: String(formData.get("country")),
|
||||
licence_categories: licenceCategories,
|
||||
},
|
||||
};
|
||||
// Remove undefined or null properties
|
||||
const cleanUpdateData = JSON.parse(
|
||||
JSON.stringify(updateData),
|
||||
(key, value) => (value !== null && value !== "" ? value : undefined)
|
||||
);
|
||||
console.dir(formData);
|
||||
console.dir(cleanUpdateData);
|
||||
const apiURL = `${BASE_API_URI}/backend/users/update/`;
|
||||
|
||||
/** @type {RequestInit} */
|
||||
const requestUpdateOptions = {
|
||||
method: "PATCH",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: `jwt=${cookies.get("jwt")}`,
|
||||
},
|
||||
body: JSON.stringify(cleanUpdateData),
|
||||
};
|
||||
const res = await fetch(apiURL, requestUpdateOptions);
|
||||
|
||||
if (!res.ok) {
|
||||
const response = await res.json();
|
||||
const errors = formatError(response.errors);
|
||||
return fail(400, { errors: errors });
|
||||
}
|
||||
|
||||
const response = await res.json();
|
||||
locals.user = response;
|
||||
userDatesFromRFC3339(locals.user);
|
||||
throw redirect(303, `/auth/about/${response.id}`);
|
||||
},
|
||||
};
|
||||
92
frontend/src/routes/auth/admin/users/+page.svelte
Normal file
92
frontend/src/routes/auth/admin/users/+page.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<!-- - Create a table or list view of all users.
|
||||
- Implement a search or filter functionality.
|
||||
- Add a modal component for editing user details (reuse the modal from about/[id]). -->
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import UserEditForm from "$lib/components/UserEditForm.svelte";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
import { page } from "$app/stores";
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
$: ({ user, users, licence_categories, subscriptions } = $page.data);
|
||||
|
||||
/** @type(App.Locals['user'] | null) */
|
||||
let selectedUser = null;
|
||||
let showModal = false;
|
||||
|
||||
/**
|
||||
* Opens the edit modal for the selected user.
|
||||
* @param {App.Locals['user']} user The user to edit.
|
||||
*/
|
||||
const openEditModal = (user) => {
|
||||
selectedUser = user;
|
||||
showModal = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the delete modal for the selected user.
|
||||
* @param {App.Locals['user']} user The user to edit.
|
||||
*/
|
||||
const openDelete = (user) => {};
|
||||
|
||||
const close = () => {
|
||||
showModal = false;
|
||||
selectedUser = null;
|
||||
if (form) {
|
||||
form.errors = undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="admin-users-page">
|
||||
<h1>{$t("user.management")}</h1>
|
||||
|
||||
<div class="search-filter" />
|
||||
|
||||
<table class="user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{$t("user.id")}</th>
|
||||
<th>{$t("name")}</th>
|
||||
<th>{$t("email")}</th>
|
||||
<th>{$t("status")}</th>
|
||||
<th>{$t("actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user}
|
||||
<tr>
|
||||
<td>{user.id}</td>
|
||||
<td>{user.first_name} {user.last_name}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>{$t("userStatus." + user.status)}</td>
|
||||
<td>
|
||||
<button on:click={() => openEditModal(user)}>{$t("edit")}</button>
|
||||
<button on:click={() => openDelete(user)}>{$t("delete")}</button>
|
||||
</td>
|
||||
</tr>{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="pagination" />
|
||||
|
||||
{#if showModal}
|
||||
<Modal on:close={close}>
|
||||
<UserEditForm
|
||||
{form}
|
||||
user={selectedUser}
|
||||
{subscriptions}
|
||||
{licence_categories}
|
||||
on:cancel={close}
|
||||
/>
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@
|
||||
action="?/login"
|
||||
use:enhance={handleLogin}
|
||||
>
|
||||
<h1 class="step-title">{$t("user_login")}</h1>
|
||||
<h1 class="step-title">{$t("user.login")}</h1>
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
|
||||
@@ -31,6 +31,58 @@ type RegistrationData struct {
|
||||
User models.User `json:"user"`
|
||||
}
|
||||
|
||||
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
|
||||
userIDInterface, ok := c.Get("user_id")
|
||||
if !ok || userIDInterface == nil {
|
||||
logger.Error.Printf("Error getting user_id from header")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "server.validation.no_user_id_provided",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
userID, ok := userIDInterface.(uint)
|
||||
|
||||
if !ok {
|
||||
logger.Error.Printf("Error: user_id is not of type uint")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "user",
|
||||
"key": "server.error.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := uc.Service.GetUserByID(uint(userID))
|
||||
if err != nil {
|
||||
logger.Error.Printf("Error retrieving valid user: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "server.error.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": user.Safe(),
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) GetAllUsers(c *gin.Context) {
|
||||
users, err := uc.Service.GetUsers(nil)
|
||||
if err != nil {
|
||||
logger.Error.Printf("Error retrieving users: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "server.error.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": users,
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) UpdateHandler(c *gin.Context) {
|
||||
var user models.User
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
@@ -141,42 +193,6 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusAccepted, gin.H{"message": "User updated successfully", "user": updatedUser})
|
||||
}
|
||||
|
||||
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
|
||||
userIDInterface, ok := c.Get("user_id")
|
||||
if !ok || userIDInterface == nil {
|
||||
logger.Error.Printf("Error getting user_id from header")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "server.validation.no_user_id_provided",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
userID, ok := userIDInterface.(uint)
|
||||
|
||||
if !ok {
|
||||
logger.Error.Printf("Error: user_id is not of type uint")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "user",
|
||||
"key": "server.error.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := uc.Service.GetUserByID(uint(userID))
|
||||
if err != nil {
|
||||
logger.Error.Printf("Error retrieving valid user: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
|
||||
"field": "general",
|
||||
"key": "server.error.internal_server_error",
|
||||
}}})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": user.Safe(),
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *UserController) LogoutHandler(c *gin.Context) {
|
||||
tokenString, err := c.Cookie("jwt")
|
||||
if err != nil {
|
||||
|
||||
@@ -37,21 +37,9 @@ func Open(dbPath string, adminMail string) error {
|
||||
|
||||
logger.Info.Print("Opened DB")
|
||||
|
||||
var count int64
|
||||
db.Model(&models.User{}).Count(&count)
|
||||
if count == 0 {
|
||||
subscriptionModels := createSubscriptionModels()
|
||||
for _, model := range subscriptionModels {
|
||||
result := db.Create(&model)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
var createdModel models.SubscriptionModel
|
||||
if err := db.First(&createdModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var categoriesCount int64
|
||||
db.Model(&models.Category{}).Count(&categoriesCount)
|
||||
if categoriesCount == 0 {
|
||||
categories := createLicenceCategories()
|
||||
for _, model := range categories {
|
||||
result := db.Create(&model)
|
||||
@@ -59,6 +47,28 @@ func Open(dbPath string, adminMail string) error {
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionsCount int64
|
||||
db.Model(&models.SubscriptionModel{}).Count(&subscriptionsCount)
|
||||
if subscriptionsCount == 0 {
|
||||
subscriptionModels := createSubscriptionModels()
|
||||
for _, model := range subscriptionModels {
|
||||
result := db.Create(&model)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var userCount int64
|
||||
db.Model(&models.User{}).Count(&userCount)
|
||||
if userCount == 0 {
|
||||
var createdModel models.SubscriptionModel
|
||||
if err := db.First(&createdModel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin, err := createAdmin(adminMail, createdModel.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -68,6 +78,7 @@ func Open(dbPath string, adminMail string) error {
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
|
||||
userRouter.GET("/current", userController.CurrentUserHandler)
|
||||
userRouter.POST("/logout", userController.LogoutHandler)
|
||||
userRouter.PATCH("/update", userController.UpdateHandler)
|
||||
userRouter.GET("/all", userController.GetAllUsers)
|
||||
}
|
||||
|
||||
membershipRouter := router.Group("/backend/membership")
|
||||
|
||||
@@ -115,6 +115,9 @@ func (service *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||
}
|
||||
|
||||
func (service *UserService) GetUsers(where map[string]interface{}) (*[]models.User, error) {
|
||||
if where == nil {
|
||||
where = map[string]interface{}{}
|
||||
}
|
||||
return service.Repo.GetUsers(where)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user