Compare commits

..

10 Commits

Author SHA1 Message Date
Alex
5e32c6b431 mod: db migration 2024-10-13 13:43:03 +02:00
Alex
7ed986d122 add: getAllUsers 2024-10-13 13:42:39 +02:00
Alex
58daf7bf30 removed library remains, add helper usage in hooks 2024-10-13 13:41:36 +02:00
Alex
d3365ae065 moved user sanitation to helper.js 2024-10-13 13:39:51 +02:00
Alex
8b3e8a0579 del: about layout 2024-10-13 13:39:06 +02:00
Alex
ab8d143aeb add usermamagement page 2024-10-13 13:38:30 +02:00
Alex
47e4e8ce55 add: UserManagement to header 2024-10-13 13:37:30 +02:00
Alex
20012b729e delete obsolote utils.js 2024-10-13 13:07:37 +02:00
Alex
975e3121a5 locales 2024-10-13 12:21:27 +02:00
Alex
56a28bbff7 add:prep for create user 2024-10-13 12:19:46 +02:00
21 changed files with 587 additions and 336 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
};
}

View File

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

View File

@@ -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,
};
}
}

View File

@@ -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}`);
},
/**

View File

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

View 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,
};
}

View 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=/");
}
}

View 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}`);
},
};

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

View File

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

View File

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

View File

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

View File

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

View File

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