implemented permission system

This commit is contained in:
Alex
2025-03-02 10:27:56 +01:00
parent 298ef9843e
commit 29f405385e
6 changed files with 311 additions and 167 deletions

View File

@@ -36,7 +36,7 @@
default: 'unknown status'
})}</span
>
<span>{$t(`userRole.${user.role_id}`, { default: 'unknown role' })}</span>
<span>{$t(`userRole.${user.role_id}`, { default: 'unknown' })}</span>
</span>
</h3>
{/if}
@@ -93,7 +93,7 @@
{licence_categories}
on:close={close}
on:cancel={close}
role_id={user.role_id}
editor={user}
/>
</Modal>
{/if}

View File

@@ -2,8 +2,8 @@
// - 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 { BASE_API_URI, PERMISSIONS } from '$lib/utils/constants';
import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit';
import {
formDataToObject,
@@ -18,7 +18,7 @@ export async function load({ locals }) {
if (!locals.user) {
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users`);
}
if (locals.user.role_id === 0) {
if (!hasPrivilige(locals.user, PERMISSIONS.View)) {
throw redirect(302, `${base}/auth/about/${locals.user.id}`);
}
}

View File

@@ -6,7 +6,8 @@
import { t } from 'svelte-i18n';
import { page } from '$app/stores';
import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers';
import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { PERMISSIONS } from '$lib/utils/constants';
/** @type {import('./$types').ActionData} */
export let form;
@@ -19,7 +20,7 @@
payments = []
} = $page.data);
let activeSection = 'users';
let activeSection = 'members';
/** @type{App.Locals['user'] | null} */
let selectedUser = null;
/** @type{App.Types['subscription'] | null} */
@@ -28,9 +29,21 @@
let showUserModal = false;
let searchTerm = '';
$: filteredUsers = searchTerm ? getFilteredUsers() : users;
$: members = users.filter((/** @type{App.Locals['user']} */ user) => {
return user.role_id >= PERMISSIONS.Member;
});
$: supporters = users.filter((/** @type{App.Locals['user']} */ user) => {
return user.role_id < PERMISSIONS.Member;
});
$: filteredMembers = searchTerm ? getFilteredUsers(members) : members;
function handleMailButtonClick() {
$: filteredSupporters = searchTerm ? getFilteredUsers(supporters) : supporters;
/**
* Handles Mail button click to open a formatted mailto link
* @param {App.Locals['user'][]} filteredUsers - the users to send the mail to
*/
function handleMailButtonClick(filteredUsers) {
const subject = 'Important Announcement';
const body = `Hello everyone,\n\nThis is an important message.`;
const bccEmails = filteredUsers
@@ -43,14 +56,15 @@
}
/**
* returns a set of users depending on the entered search query
* returns a set of members depending on the entered search query
* @param {App.Locals['user'][]} userSet Set to filter
* @return {App.Locals['user'][]}*/
const getFilteredUsers = () => {
if (!searchTerm.trim()) return users;
const getFilteredUsers = (userSet) => {
if (!searchTerm.trim()) return userSet;
const term = searchTerm.trim().toLowerCase();
return users.filter((/** @type{App.Locals['user']}*/ user) => {
return userSet.filter((/** @type{App.Locals['user']}*/ user) => {
const basicMatch = [
user.first_name?.toLowerCase(),
user.last_name?.toLowerCase(),
@@ -124,12 +138,22 @@
<ul class="nav-list">
<li>
<button
class="nav-link {activeSection === 'users' ? 'active' : ''}"
on:click={() => setActiveSection('users')}
class="nav-link {activeSection === 'members' ? 'active' : ''}"
on:click={() => setActiveSection('members')}
>
<i class="fas fa-users"></i>
{$t('users')}
<span class="nav-badge">{users.length}</span>
<span class="nav-badge">{members.length}</span>
</button>
</li>
<li>
<button
class="nav-link {activeSection === 'supporter' ? 'active' : ''}"
on:click={() => setActiveSection('supporter')}
>
<i class="fas fa-hand-holding-dollar"></i>
{$t('supporter')}
<span class="nav-badge">{supporters.length}</span>
</button>
</li>
<li>
@@ -168,7 +192,7 @@
{/each}
{/if}
{#if activeSection === 'users'}
{#if activeSection === 'members'}
<div class="section-header">
<h2>{$t('users')}</h2>
<div class="title-container">
@@ -183,7 +207,7 @@
<button
class="btn primary"
aria-label="Mail Users"
on:click={() => handleMailButtonClick()}
on:click={() => handleMailButtonClick(filteredMembers)}
>
<i class="fas fa-envelope"></i>
</button>
@@ -196,7 +220,108 @@
</div>
</div>
<div class="accordion">
{#each filteredUsers as user}
{#each filteredMembers as user}
<details class="accordion-item">
<summary class="accordion-header">
{user.first_name}
{user.last_name}
</summary>
<div class="accordion-content">
<table class="table">
<tbody>
<tr>
<th>{$t('user.id')}</th>
<td>{user.id}</td>
</tr>
<tr>
<th>{$t('name')}</th>
<td>{user.first_name} {user.last_name}</td>
</tr>
<tr>
<th>{$t('user.email')}</th>
<td>{user.email}</td>
</tr>
<tr>
<th>{$t('subscription.subscription')}</th>
<td>{user.membership?.subscription_model?.name}</td>
</tr>
<tr>
<th>{$t('status')}</th>
<td>{$t('userStatus.' + user.status)}</td>
</tr>
</tbody>
</table>
<div class="button-group">
<button class="btn primary" on:click={() => openEditUserModal(user)}>
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
<form
method="POST"
action="?/userDelete"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.user_deletion', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<input type="hidden" name="user[last_name]" value={user.last_name} />
<button class="btn danger" type="submit">
<i class="fas fa-trash"></i>
{$t('delete')}
</button>
</form>
</div>
</div>
</details>
{/each}
</div>
{:else if activeSection === 'supporter'}
<div class="section-header">
<h2>{$t('supporter')}</h2>
<div class="title-container">
<InputField
name="search"
bind:value={searchTerm}
placeholder={$t('placeholder.search')}
backgroundColor="--base"
/>
</div>
<div>
<button
class="btn primary"
aria-label="Mail Supporter"
on:click={() => handleMailButtonClick(filteredSupporters)}
>
<i class="fas fa-envelope"></i>
</button>
</div>
<div>
<button class="btn primary" on:click={() => openEditUserModal(null)}>
<i class="fas fa-plus"></i>
{$t('add_new')}
</button>
</div>
</div>
<div class="accordion">
{#each filteredSupporters as user}
<details class="accordion-item">
<summary class="accordion-header">
{user.first_name}
@@ -272,7 +397,7 @@
{:else if activeSection === 'subscriptions'}
<div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2>
{#if user.role_id == 8}
{#if hasPrivilige(user, PERMISSIONS.Super)}
<button class="btn primary" on:click={() => openEditSubscriptionModal(null)}>
<i class="fas fa-plus"></i>
{$t('add_new')}
@@ -285,7 +410,7 @@
<summary class="accordion-header">
{subscription.name}
<span class="nav-badge"
>{users.filter(
>{members.filter(
(/** @type{App.Locals['user']}*/ user) =>
user.membership?.subscription_model?.name === subscription.name
).length}</span
@@ -328,7 +453,7 @@
</tr>
</tbody>
</table>
{#if user.role_id == 8}
{#if hasPrivilige(user, PERMISSIONS.Super)}
<div class="button-group">
<button
class="btn primary"
@@ -337,7 +462,7 @@
<i class="fas fa-edit"></i>
{$t('edit')}
</button>
{#if !users.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription_model?.id === subscription.id)}
{#if !members.some(/** @param{App.Locals['user']} user */ (user) => user.membership?.subscription_model?.id === subscription.id)}
<form
method="POST"
action="?/subscriptionDelete"
@@ -415,7 +540,7 @@
<Modal on:close={close}>
<UserEditForm
{form}
role_id={user.role_id}
editor={user}
user={selectedUser}
{subscriptions}
{licence_categories}