Compare commits

...

4 Commits

Author SHA1 Message Date
Alex
d5a8b16e43 frontend:add user count badge on subscriptions page 2025-02-20 09:07:31 +01:00
Alex
48e21736ea frontend: admin/users/layout.server cleanup 2025-02-20 09:06:51 +01:00
Alex
ab168311a9 new routes 2025-02-20 09:06:27 +01:00
Alex
54faee731d frontend: fix empty date handling 2025-02-20 09:05:49 +01:00
8 changed files with 80 additions and 77 deletions

View File

@@ -76,7 +76,7 @@ export function processUserFormData(rawData) {
email: String(rawData.object.email), email: String(rawData.object.email),
phone: String(rawData.object.phone || ''), phone: String(rawData.object.phone || ''),
company: String(rawData.object.company || ''), company: String(rawData.object.company || ''),
dateofbirth: toRFC3339(String(rawData.object.dateofbirth)), dateofbirth: toRFC3339(String(rawData.object.dateofbirth || '')),
address: String(rawData.object.address || ''), address: String(rawData.object.address || ''),
zip_code: String(rawData.object.zip_code || ''), zip_code: String(rawData.object.zip_code || ''),
city: String(rawData.object.city || ''), city: String(rawData.object.city || ''),
@@ -86,8 +86,8 @@ export function processUserFormData(rawData) {
membership: { membership: {
id: Number(rawData.object.membership?.id) || 0, id: Number(rawData.object.membership?.id) || 0,
status: Number(rawData.object.membership?.status), status: Number(rawData.object.membership?.status),
start_date: toRFC3339(String(rawData.object.membership?.start_date)), start_date: toRFC3339(String(rawData.object.membership?.start_date || '')),
end_date: toRFC3339(String(rawData.object.membership?.end_date)), end_date: toRFC3339(String(rawData.object.membership?.end_date || '')),
parent_member_id: Number(rawData.object.membership?.parent_member_id) || 0, parent_member_id: Number(rawData.object.membership?.parent_member_id) || 0,
subscription_model: { subscription_model: {
id: Number(rawData.object.membership?.subscription_model?.id) || 0, id: Number(rawData.object.membership?.subscription_model?.id) || 0,
@@ -107,8 +107,8 @@ export function processUserFormData(rawData) {
id: Number(rawData.object.licence?.id) || 0, id: Number(rawData.object.licence?.id) || 0,
status: Number(rawData.object.licence?.status), status: Number(rawData.object.licence?.status),
number: String(rawData.object.licence?.number || ''), number: String(rawData.object.licence?.number || ''),
issued_date: toRFC3339(String(rawData.object.licence?.issued_date)), issued_date: toRFC3339(String(rawData.object.licence?.issued_date || '')),
expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date)), expiration_date: toRFC3339(String(rawData.object.licence?.expiration_date || '')),
country: String(rawData.object.licence?.country || ''), country: String(rawData.object.licence?.country || ''),
categories: rawData.object.licence?.categories || [] categories: rawData.object.licence?.categories || []
}, },
@@ -120,7 +120,9 @@ export function processUserFormData(rawData) {
iban: String(rawData.object.bank_account?.iban || ''), iban: String(rawData.object.bank_account?.iban || ''),
bic: String(rawData.object.bank_account?.bic || ''), bic: String(rawData.object.bank_account?.bic || ''),
mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''), mandate_reference: String(rawData.object.bank_account?.mandate_reference || ''),
mandate_date_signed: toRFC3339(String(rawData.object.bank_account?.mandate_date_signed)) mandate_date_signed: toRFC3339(
String(rawData.object.bank_account?.mandate_date_signed || '')
)
} }
} }
}; };

View File

@@ -1,11 +1,8 @@
import { BASE_API_URI } from "$lib/utils/constants";
import { refreshCookie } from "$lib/utils/helpers";
import { redirect } from "@sveltejs/kit";
/** @type {import('./$types').LayoutServerLoad} */ /** @type {import('./$types').LayoutServerLoad} */
export async function load({ locals, cookies }) { export async function load({ locals }) {
return { return {
user: locals.user, user: locals.user,
licence_categories: locals.licence_categories, licence_categories: locals.licence_categories,
subscriptions: locals.subscriptions, subscriptions: locals.subscriptions
}; };
} }

View File

@@ -36,7 +36,7 @@ export const actions = {
console.log('Is creating: ', isCreating); console.log('Is creating: ', isCreating);
// console.dir(formData); // console.dir(formData);
console.dir(processedData.user.membership); console.dir(processedData.user.membership);
const apiURL = `${BASE_API_URI}/backend/users/upsert/`; const apiURL = `${BASE_API_URI}/backend/users/`;
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestUpdateOptions = { const requestUpdateOptions = {

View File

@@ -6,7 +6,6 @@ import { userDatesFromRFC3339, refreshCookie } from '$lib/utils/helpers';
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 {
// Fetch user data, subscriptions, and licence categories in parallel
const response = await fetch(`${BASE_API_URI}/backend/users/all`, { const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
credentials: 'include', credentials: 'include',
headers: { headers: {
@@ -16,7 +15,7 @@ export async function load({ cookies, fetch, locals }) {
if (!response.ok) { if (!response.ok) {
// Clear the invalid JWT cookie // Clear the invalid JWT cookie
cookies.delete('jwt', { path: '/' }); cookies.delete('jwt', { path: '/' });
throw redirect(302, '/auth/login?next=/'); throw redirect(302, '/auth/login?next=admin/users/');
} }
const data = await response.json(); const data = await response.json();
@@ -43,6 +42,6 @@ export async function load({ cookies, fetch, locals }) {
// In case of any error, clear the JWT cookie // In case of any error, clear the JWT cookie
cookies.delete('jwt', { path: '/' }); cookies.delete('jwt', { path: '/' });
throw redirect(302, '/auth/login?next=/'); throw redirect(302, '/auth/login?next=admin/users/');
} }
} }

View File

@@ -15,7 +15,7 @@ import {
export async function load({ locals }) { export async function load({ locals }) {
// redirect user if not logged in // redirect user if not logged in
if (!locals.user) { if (!locals.user) {
throw redirect(302, `/auth/login?next=/auth/users`); throw redirect(302, `/auth/login?next=/auth/admin/users`);
} }
} }
@@ -38,7 +38,7 @@ export const actions = {
console.dir(processedData.user.membership); console.dir(processedData.user.membership);
const isCreating = !processedData.user.id || processedData.user.id === 0; const isCreating = !processedData.user.id || processedData.user.id === 0;
console.log('Is creating: ', isCreating); console.log('Is creating: ', isCreating);
const apiURL = `${BASE_API_URI}/backend/users/upsert`; const apiURL = `${BASE_API_URI}/backend/users`;
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestOptions = { const requestOptions = {
@@ -122,7 +122,7 @@ export const actions = {
const rawData = formDataToObject(formData); const rawData = formDataToObject(formData);
const processedData = processUserFormData(rawData); const processedData = processUserFormData(rawData);
const apiURL = `${BASE_API_URI}/backend/users/delete`; const apiURL = `${BASE_API_URI}/backend/users`;
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestOptions = { const requestOptions = {

View File

@@ -179,7 +179,6 @@
backgroundColor="--base" backgroundColor="--base"
/> />
</div> </div>
<!-- <input type="text" bind:value={searchTerm} placeholder={$t('placeholder.search')} /> -->
<div> <div>
<button <button
class="btn primary" class="btn primary"
@@ -201,7 +200,7 @@
<details class="accordion-item"> <details class="accordion-item">
<summary class="accordion-header"> <summary class="accordion-header">
{user.first_name} {user.first_name}
{user.last_name} - {user.email} {user.last_name}
</summary> </summary>
<div class="accordion-content"> <div class="accordion-content">
<table class="table"> <table class="table">
@@ -218,6 +217,10 @@
<th>{$t('user.email')}</th> <th>{$t('user.email')}</th>
<td>{user.email}</td> <td>{user.email}</td>
</tr> </tr>
<tr>
<th>{$t('subscription.subscription')}</th>
<td>{user.membership?.subscription_model?.name}</td>
</tr>
<tr> <tr>
<th>{$t('status')}</th> <th>{$t('status')}</th>
<td>{$t('userStatus.' + user.status)}</td> <td>{$t('userStatus.' + user.status)}</td>
@@ -281,6 +284,12 @@
<details class="accordion-item"> <details class="accordion-item">
<summary class="accordion-header"> <summary class="accordion-header">
{subscription.name} {subscription.name}
<span class="nav-badge"
>{users.filter(
(/** @type{App.Locals['user']}*/ user) =>
user.membership?.subscription_model?.name === subscription.name
).length}</span
>
</summary> </summary>
<div class="accordion-content"> <div class="accordion-content">
<table class="table"> <table class="table">
@@ -507,6 +516,7 @@
} }
.accordion-header { .accordion-header {
display: flex;
padding: 1rem; padding: 1rem;
cursor: pointer; cursor: pointer;
font-family: 'Roboto Mono', monospace; font-family: 'Roboto Mono', monospace;
@@ -586,7 +596,6 @@
gap: 0.5rem; gap: 0.5rem;
} }
/* Style for the nav badge */
.nav-badge { .nav-badge {
background: var(--surface2); background: var(--surface2);
color: var(--text); color: var(--text);
@@ -596,7 +605,6 @@
margin-left: auto; margin-left: auto;
} }
/* Improved focus states */
.nav-link:focus, .nav-link:focus,
.accordion-header:focus { .accordion-header:focus {
outline: 2px solid var(--mauve); outline: 2px solid var(--mauve);

View File

@@ -1,5 +1,5 @@
import { BASE_API_URI } from "$lib/utils/constants"; import { BASE_API_URI } from '$lib/utils/constants';
import { fail, redirect } from "@sveltejs/kit"; import { fail, redirect } from '@sveltejs/kit';
/** @type {import('./$types').PageServerLoad} */ /** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) { export async function load({ locals }) {
@@ -14,18 +14,15 @@ export const actions = {
default: async ({ fetch, cookies }) => { default: async ({ fetch, cookies }) => {
/** @type {RequestInit} */ /** @type {RequestInit} */
const requestInitOptions = { const requestInitOptions = {
method: "POST", method: 'POST',
credentials: "include", credentials: 'include',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get("jwt")}`, Cookie: `jwt=${cookies.get('jwt')}`
}, }
}; };
const res = await fetch( const res = await fetch(`${BASE_API_URI}/backend/logout/`, requestInitOptions);
`${BASE_API_URI}/backend/users/logout/`,
requestInitOptions
);
if (!res.ok) { if (!res.ok) {
const response = await res.json(); const response = await res.json();
@@ -35,23 +32,23 @@ export const actions = {
} }
// eat the cookie // eat the cookie
cookies.delete("jwt", { path: "/" }); cookies.delete('jwt', { path: '/' });
// The server should clear the cookie, so we don't need to handle it here // The server should clear the cookie, so we don't need to handle it here
// Just check if the cookie is cleared in the response // Just check if the cookie is cleared in the response
const setCookieHeader = res.headers.get("set-cookie"); const setCookieHeader = res.headers.get('set-cookie');
if (!setCookieHeader || !setCookieHeader.includes("jwt=;")) { if (!setCookieHeader || !setCookieHeader.includes('jwt=;')) {
console.error("JWT cookie not cleared in response"); console.error('JWT cookie not cleared in response');
return fail(500, { return fail(500, {
errors: [ errors: [
{ {
error: "Server error: Failed to clear authentication token", error: 'Server error: Failed to clear authentication token',
id: Date.now(), id: Date.now()
}, }
], ]
}); });
} }
// redirect the user // redirect the user
throw redirect(302, "/auth/login"); throw redirect(302, '/auth/login');
}, }
}; };

View File

@@ -21,15 +21,15 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
// apiRouter.POST("/v1/subscription", membershipcontroller.RegisterSubscription) // apiRouter.POST("/v1/subscription", membershipcontroller.RegisterSubscription)
// } // }
userRouter := router.Group("/backend/users") userRouter := router.Group("/backend")
userRouter.Use(middlewares.AuthMiddleware()) userRouter.Use(middlewares.AuthMiddleware())
{ {
userRouter.GET("/current", userController.CurrentUserHandler) userRouter.GET("/users/current", userController.CurrentUserHandler)
userRouter.POST("/logout", userController.LogoutHandler) userRouter.POST("/logout", userController.LogoutHandler)
userRouter.PATCH("/upsert", userController.UpdateHandler) userRouter.PATCH("/users", userController.UpdateHandler)
userRouter.POST("/upsert", userController.RegisterUser) userRouter.POST("/users", userController.RegisterUser)
userRouter.GET("/all", userController.GetAllUsers) userRouter.GET("/users/all", userController.GetAllUsers)
userRouter.DELETE("/delete", userController.DeleteUser) userRouter.DELETE("/users", userController.DeleteUser)
} }
membershipRouter := router.Group("/backend/membership") membershipRouter := router.Group("/backend/membership")