Compare commits

...

3 Commits

Author SHA1 Message Date
Alex
29f405385e implemented permission system 2025-03-02 10:27:56 +01:00
Alex
298ef9843e frontend permission system 2025-03-02 10:27:16 +01:00
Alex
aa1bd00e80 add en locale 2025-03-02 10:26:53 +01:00
10 changed files with 547 additions and 186 deletions

View File

@@ -5,6 +5,8 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { PERMISSIONS } from '$lib/utils/constants';
import { hasPrivilige } from '$lib/utils/helpers';
let isMobileMenuOpen = false; let isMobileMenuOpen = false;
@@ -104,7 +106,7 @@
{$page.data.user.last_name} {$page.data.user.last_name}
</a> </a>
</div> </div>
{#if $page.data.user.role_id > 0} {#if hasPrivilige($page.data.user, PERMISSIONS.View)}
<div <div
class="header-nav-item" class="header-nav-item"
class:active={$page.url.pathname.startsWith(`${base}/auth/admin/users`)} class:active={$page.url.pathname.startsWith(`${base}/auth/admin/users`)}

View File

@@ -3,8 +3,9 @@
import SmallLoader from '$lib/components/SmallLoader.svelte'; import SmallLoader from '$lib/components/SmallLoader.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { applyAction, enhance } from '$app/forms'; import { applyAction, enhance } from '$app/forms';
import { receive, send } from '$lib/utils/helpers'; import { hasPrivilige, receive, send } from '$lib/utils/helpers';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { PERMISSIONS } from '$lib/utils/constants';
/** @type {import('../../routes/auth/about/[id]/$types').ActionData} */ /** @type {import('../../routes/auth/about/[id]/$types').ActionData} */
export let form; export let form;
@@ -29,7 +30,7 @@
profile_picture: '', profile_picture: '',
payment_status: 0, payment_status: 0,
status: 1, status: 1,
role_id: 0, role_id: 1,
membership: { membership: {
id: 0, id: 0,
start_date: '', start_date: '',
@@ -70,12 +71,14 @@
/** @type {App.Locals['user'] | null} */ /** @type {App.Locals['user'] | null} */
export let user; export let user;
/** @type {Number} */ /** @type {App.Locals['user']} */
export let role_id; export let editor;
/** @type {App.Locals['user'] } */ /** @type {App.Locals['user'] } */
let localUser; let localUser;
let readonlyUser = !hasPrivilige(editor, PERMISSIONS.Update);
$: { $: {
if (user !== undefined && !localUser) { if (user !== undefined && !localUser) {
localUser = localUser =
@@ -106,8 +109,9 @@
const userRoleOptions = [ const userRoleOptions = [
{ value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert" { value: 0, label: $t('userRole.0'), color: '--subtext1' }, // Grey for "Nicht verifiziert"
{ value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert" { value: 1, label: $t('userRole.1'), color: '--light-green' }, // Light green for "Verifiziert"
{ value: 4, label: $t('userRole.4'), color: '--green' }, // Green for "Aktiv" { value: 2, label: $t('userRole.2'), color: '--green' }, // Light green for "Verifiziert"
{ value: 8, label: $t('userRole.8'), color: '--pink' } // Pink for "Passiv" { value: 4, label: $t('userRole.4'), color: '--pink' }, // Green for "Aktiv"
{ value: 8, label: $t('userRole.8'), color: '--red' } // Pink for "Passiv"
]; ];
const membershipStatusOptions = [ const membershipStatusOptions = [
{ value: 3, label: $t('userStatus.3'), color: '--green' }, // Green for "Aktiv" { value: 3, label: $t('userStatus.3'), color: '--green' }, // Green for "Aktiv"
@@ -232,9 +236,9 @@
label={$t('status')} label={$t('status')}
bind:value={localUser.status} bind:value={localUser.status}
options={userStatusOptions} options={userStatusOptions}
readonly={role_id === 0} readonly={readonlyUser}
/> />
{#if role_id === 8} {#if hasPrivilige(editor, PERMISSIONS.Super)}
<InputField <InputField
name="user[role_id]" name="user[role_id]"
type="select" type="select"
@@ -243,29 +247,31 @@
options={userRoleOptions} options={userRoleOptions}
/> />
{/if} {/if}
<InputField {#if hasPrivilige(localUser, PERMISSIONS.Member)}
name="user[password]" <InputField
type="password" name="user[password]"
label={$t('password')} type="password"
placeholder={$t('placeholder.password')} label={$t('password')}
bind:value={password} placeholder={$t('placeholder.password')}
otherPasswordValue={confirm_password} bind:value={password}
/> otherPasswordValue={confirm_password}
<InputField />
name="confirm_password" <InputField
type="password" name="confirm_password"
label={$t('confirm_password')} type="password"
placeholder={$t('placeholder.password')} label={$t('confirm_password')}
bind:value={confirm_password} placeholder={$t('placeholder.password')}
otherPasswordValue={password} bind:value={confirm_password}
/> otherPasswordValue={password}
/>
{/if}
<InputField <InputField
name="user[first_name]" name="user[first_name]"
label={$t('user.first_name')} label={$t('user.first_name')}
bind:value={localUser.first_name} bind:value={localUser.first_name}
placeholder={$t('placeholder.first_name')} placeholder={$t('placeholder.first_name')}
required={true} required={true}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<InputField <InputField
name="user[last_name]" name="user[last_name]"
@@ -273,7 +279,7 @@
bind:value={localUser.last_name} bind:value={localUser.last_name}
placeholder={$t('placeholder.last_name')} placeholder={$t('placeholder.last_name')}
required={true} required={true}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<InputField <InputField
name="user[company]" name="user[company]"
@@ -296,14 +302,16 @@
bind:value={localUser.phone} bind:value={localUser.phone}
placeholder={$t('placeholder.phone')} placeholder={$t('placeholder.phone')}
/> />
<InputField {#if hasPrivilige(localUser, PERMISSIONS.Member)}
name="user[dateofbirth]" <InputField
type="date" name="user[dateofbirth]"
label={$t('user.dateofbirth')} type="date"
bind:value={localUser.dateofbirth} label={$t('user.dateofbirth')}
placeholder={$t('placeholder.dateofbirth')} bind:value={localUser.dateofbirth}
readonly={role_id === 0} placeholder={$t('placeholder.dateofbirth')}
/> readonly={readonlyUser}
/>
{/if}
<InputField <InputField
name="user[address]" name="user[address]"
label={$t('address')} label={$t('address')}
@@ -322,7 +330,7 @@
bind:value={localUser.city} bind:value={localUser.city}
placeholder={$t('placeholder.city')} placeholder={$t('placeholder.city')}
/> />
{#if role_id > 0} {#if !readonlyUser}
<InputField <InputField
name="user[notes]" name="user[notes]"
type="textarea" type="textarea"
@@ -335,77 +343,80 @@
/> />
{/if} {/if}
</div> </div>
<div class="tab-content" style="display: {activeTab === 'licence' ? 'block' : 'none'}">
<InputField {#if hasPrivilige(localUser, PERMISSIONS.Member)}
name="user[licence][status]" <div class="tab-content" style="display: {activeTab === 'licence' ? 'block' : 'none'}">
type="select" <InputField
label={$t('status')} name="user[licence][status]"
bind:value={localUser.licence.status} type="select"
options={licenceStatusOptions} label={$t('status')}
readonly={role_id === 0} bind:value={localUser.licence.status}
/> options={licenceStatusOptions}
<InputField readonly={readonlyUser}
name="user[licence][number]" />
type="text" <InputField
label={$t('licence_number')} name="user[licence][number]"
bind:value={localUser.licence.number} type="text"
placeholder={$t('placeholder.licence_number')} label={$t('licence_number')}
toUpperCase={true} bind:value={localUser.licence.number}
readonly={role_id === 0} placeholder={$t('placeholder.licence_number')}
/> toUpperCase={true}
<InputField readonly={readonlyUser}
name="user[licence][issued_date]" />
type="date" <InputField
label={$t('issued_date')} name="user[licence][issued_date]"
bind:value={localUser.licence.issued_date} type="date"
placeholder={$t('placeholder.issued_date')} label={$t('issued_date')}
readonly={role_id === 0} bind:value={localUser.licence.issued_date}
/> placeholder={$t('placeholder.issued_date')}
<InputField readonly={readonlyUser}
name="user[licence][expiration_date]" />
type="date" <InputField
label={$t('expiration_date')} name="user[licence][expiration_date]"
bind:value={localUser.licence.expiration_date} type="date"
placeholder={$t('placeholder.expiration_date')} label={$t('expiration_date')}
readonly={role_id === 0} bind:value={localUser.licence.expiration_date}
/> placeholder={$t('placeholder.expiration_date')}
<InputField readonly={readonlyUser}
name="user[licence][country]" />
label={$t('country')} <InputField
bind:value={localUser.licence.country} name="user[licence][country]"
placeholder={$t('placeholder.issuing_country')} label={$t('country')}
readonly={role_id === 0} bind:value={localUser.licence.country}
/> placeholder={$t('placeholder.issuing_country')}
<div class="licence-categories"> readonly={readonlyUser}
<h3>{$t('licence_categories')}</h3> />
<div class="checkbox-grid"> <div class="licence-categories">
{#each Object.entries(groupedCategories) as [, categories], groupIndex} <h3>{$t('licence_categories')}</h3>
{#if groupIndex > 0} <div class="checkbox-grid">
<div class="category-break"></div> {#each Object.entries(groupedCategories) as [, categories], groupIndex}
{/if} {#if groupIndex > 0}
{#each categories as category} <div class="category-break"></div>
<div class="checkbox-item"> {/if}
<div class="checkbox-label-container"> {#each categories as category}
<InputField <div class="checkbox-item">
type="checkbox" <div class="checkbox-label-container">
name="user[licence][categories][]" <InputField
value={JSON.stringify(category)} type="checkbox"
label={category.category} name="user[licence][categories][]"
checked={localUser.licence.categories != null && value={JSON.stringify(category)}
localUser.licence.categories.some( label={category.category}
(cat) => cat.category === category.category checked={localUser.licence.categories != null &&
)} localUser.licence.categories.some(
/> (cat) => cat.category === category.category
)}
/>
</div>
<span class="checkbox-description">
{$t(`licenceCategory.${category.category}`)}
</span>
</div> </div>
<span class="checkbox-description"> {/each}
{$t(`licenceCategory.${category.category}`)}
</span>
</div>
{/each} {/each}
{/each} </div>
</div> </div>
</div> </div>
</div> {/if}
<div class="tab-content" style="display: {activeTab === 'membership' ? 'block' : 'none'}"> <div class="tab-content" style="display: {activeTab === 'membership' ? 'block' : 'none'}">
<InputField <InputField
name="user[membership][status]" name="user[membership][status]"
@@ -413,7 +424,7 @@
label={$t('status')} label={$t('status')}
bind:value={localUser.membership.status} bind:value={localUser.membership.status}
options={membershipStatusOptions} options={membershipStatusOptions}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<InputField <InputField
name="user[membership][subscription_model][name]" name="user[membership][subscription_model][name]"
@@ -421,31 +432,33 @@
label={$t('subscription.subscription')} label={$t('subscription.subscription')}
bind:value={localUser.membership.subscription_model.name} bind:value={localUser.membership.subscription_model.name}
options={subscriptionModelOptions} options={subscriptionModelOptions}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<div class="subscription-info"> <div class="subscription-info">
<div class="subscription-column"> {#if hasPrivilige(editor, PERMISSIONS.Member)}
<p> <div class="subscription-column">
<strong>{$t('subscription.monthly_fee')}:</strong>
{selectedSubscriptionModel?.monthly_fee || '-'}
</p>
<p>
<strong>{$t('subscription.hourly_rate')}:</strong>
{selectedSubscriptionModel?.hourly_rate || '-'}
</p>
{#if selectedSubscriptionModel?.included_hours_per_year}
<p> <p>
<strong>{$t('subscription.included_hours_per_year')}:</strong> <strong>{$t('subscription.monthly_fee')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year} {selectedSubscriptionModel?.monthly_fee || '-'}
</p> </p>
{/if}
{#if selectedSubscriptionModel?.included_hours_per_month}
<p> <p>
<strong>{$t('subscription.included_hours_per_month')}:</strong> <strong>{$t('subscription.hourly_rate')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month} {selectedSubscriptionModel?.hourly_rate || '-'}
</p> </p>
{/if} {#if selectedSubscriptionModel?.included_hours_per_year}
</div> <p>
<strong>{$t('subscription.included_hours_per_year')}:</strong>
{selectedSubscriptionModel?.included_hours_per_year}
</p>
{/if}
{#if selectedSubscriptionModel?.included_hours_per_month}
<p>
<strong>{$t('subscription.included_hours_per_month')}:</strong>
{selectedSubscriptionModel?.included_hours_per_month}
</p>
{/if}
</div>
{/if}
<div class="subscription-column"> <div class="subscription-column">
<p> <p>
<strong>{$t('details')}:</strong> <strong>{$t('details')}:</strong>
@@ -465,7 +478,7 @@
label={$t('start')} label={$t('start')}
bind:value={localUser.membership.start_date} bind:value={localUser.membership.start_date}
placeholder={$t('placeholder.start_date')} placeholder={$t('placeholder.start_date')}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<InputField <InputField
name="user[membership][end_date]" name="user[membership][end_date]"
@@ -473,16 +486,18 @@
label={$t('end')} label={$t('end')}
bind:value={localUser.membership.end_date} bind:value={localUser.membership.end_date}
placeholder={$t('placeholder.end_date')} placeholder={$t('placeholder.end_date')}
readonly={role_id === 0} readonly={readonlyUser}
/>
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={localUser.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
readonly={role_id === 0}
/> />
{#if hasPrivilige(editor, PERMISSIONS.Member)}
<InputField
name="user[membership][parent_member_id]"
type="number"
label={$t('parent_member_id')}
bind:value={localUser.membership.parent_member_id}
placeholder={$t('placeholder.parent_member_id')}
readonly={readonlyUser}
/>
{/if}
</div> </div>
<div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}"> <div class="tab-content" style="display: {activeTab === 'bankaccount' ? 'block' : 'none'}">
<InputField <InputField
@@ -516,7 +531,7 @@
label={$t('mandate_reference')} label={$t('mandate_reference')}
bind:value={localUser.bank_account.mandate_reference} bind:value={localUser.bank_account.mandate_reference}
placeholder={$t('placeholder.mandate_reference')} placeholder={$t('placeholder.mandate_reference')}
readonly={role_id === 0} readonly={readonlyUser}
/> />
<InputField <InputField
name="user[bank_account][mandate_date_signed]" name="user[bank_account][mandate_date_signed]"

View File

@@ -3,12 +3,13 @@ export default {
1: 'Nicht verifiziert', 1: 'Nicht verifiziert',
2: 'Deaktiviert', 2: 'Deaktiviert',
3: 'Verifiziert', 3: 'Verifiziert',
4: 'Aktiv', 4: 'Systemzugang',
5: 'Passiv' 5: 'Passiv'
}, },
userRole: { userRole: {
0: 'Mitglied', 0: 'Sponsor',
1: 'Betrachter', 1: 'Mitglied',
2: 'Betrachter',
4: 'Bearbeiter', 4: 'Bearbeiter',
8: 'Administrator' 8: 'Administrator'
}, },
@@ -124,7 +125,8 @@ export default {
dateofbirth: 'Geburtstag', dateofbirth: 'Geburtstag',
email: 'Email', email: 'Email',
status: 'Status', status: 'Status',
role: 'Nutzerrolle' role: 'Nutzerrolle',
supporter: 'Sponsor'
}, },
subscription: { subscription: {
name: 'Modellname', name: 'Modellname',
@@ -155,6 +157,7 @@ export default {
delete: 'Löschen', delete: 'Löschen',
search: 'Suche:', search: 'Suche:',
name: 'Name', name: 'Name',
supporter: 'Sponsoren',
mandate_date_signed: 'Mandatserteilungsdatum', mandate_date_signed: 'Mandatserteilungsdatum',
licence_categories: 'Führerscheinklassen', licence_categories: 'Führerscheinklassen',
subscription_model: 'Mitgliedschatfsmodell', subscription_model: 'Mitgliedschatfsmodell',

View File

@@ -1,15 +1,210 @@
export default { export default {
userStatus: { userStatus: {
1: "Unverified", 1: 'Not Verified',
2: "Verified", 2: 'Deactivated',
3: "Active", 3: 'Verified',
4: "Passive", 4: 'System Access',
5: "Disabled", 5: 'Passive'
}, },
userRole: { userRole: {
0: "Member", 0: 'Sponsor',
1: "Viewer", 1: 'Member',
4: "Editor", 2: 'Viewer',
8: "Admin", 4: 'Editor',
}, 8: 'Administrator'
},
placeholder: {
password: 'Enter password...',
email: 'Enter email address...',
company: 'Enter company name...',
first_name: 'Enter first name...',
last_name: 'Enter last name...',
phone: 'Enter phone number...',
address: 'Enter street and house number...',
zip_code: 'Enter postal code...',
city: 'Enter city...',
bank_name: 'Enter bank name...',
parent_member_id: 'Enter parent member ID...',
bank_account_holder: 'Enter name...',
iban: 'Enter IBAN...',
bic: 'Enter BIC (for non-German accounts)...',
mandate_reference: 'Enter SEPA mandate reference...',
notes: 'Your notes about {name}...',
licence_number: 'On the drivers license under field 5',
issued_date: 'Issue date under field 4a',
expiration_date: 'Expiration date under field 4b',
issuing_country: 'Issuing country',
subscription_name: 'Subscription model name',
subscription_details: 'Describe the subscription model...',
subscription_conditions: 'Describe the usage conditions...',
search: 'Search...'
},
validation: {
required: 'Input required',
password: 'Password too short, at least 8 characters',
password_match: 'Passwords do not match!',
phone: 'Invalid format (+491738762387 or 0173850698)',
zip_code: 'Invalid postal code (Only German locations are allowed)',
bic: 'Invalid BIC',
iban: 'Invalid IBAN',
date: 'Please enter a date',
email: 'Invalid email address',
licence: 'Number too short (11 characters)'
},
server: {
general: 'General',
error: {
invalid_json: 'Invalid JSON data',
no_auth_token: 'Unauthorized, missing or invalid auth token',
jwt_parsing_error: 'Unauthorized, auth token could not be read',
unauthorized: 'You are not authorized to perform this action',
internal_server_error:
'Damn, error on our side, try again, then contact someone from the organization.',
not_possible: 'Operation not possible.',
not_found: 'Could not be found.',
in_use: 'Is in use',
undelivered_verification_mail:
'Registration successful, but the verification email could not be sent. Please contact the organization to verify your email address and activate your account.'
},
validation: {
invalid: 'Invalid',
invalid_user_id: 'Invalid user ID',
invalid_subscription_model: 'Model not found',
user_not_found: '{field} could not be found',
invalid_user_data: 'Invalid user data',
user_not_found_or_wrong_password: 'Does not exist or wrong password',
email_already_registered: 'A member has already been created with this email address.',
password_already_changed: 'The password has already been changed.',
alphanumunicode: 'Contains disallowed characters',
safe_content: 'I see what you did there! Do not cross this line!',
iban: 'Invalid. Format: DE07123412341234123412',
bic: 'Invalid. Format: BELADEBEXXX',
email: 'Invalid format',
number: 'Is not a number',
euDriversLicence: 'Is not a European drivers license',
lte: 'Is too large/new',
gt: 'Is too small/old',
required: 'Field is required',
image: 'This is not an image',
alphanum: 'Contains invalid characters',
user_disabled: 'User is disabled',
duplicate: 'Already exists...',
alphaunicode: 'Must consist only of letters',
too_soon: 'Too soon'
}
},
licenceCategory: {
AM: 'Mopeds and light four-wheeled vehicles (50cc, max 45 km/h)',
A1: 'Light motorcycles (125cc)',
A2: 'Medium-power motorcycles (max 35 kW)',
A: 'Motorcycles',
B: 'Motor vehicles ≤ 3500 kg, ≤ 8 seats',
C1: 'Medium-heavy vehicles - 7500 kg',
C: 'Heavy commercial vehicles > 3500 kg',
D1: 'Minibuses with 9-16 seats',
D: 'Buses > 8 seats',
BE: 'Vehicle class B with trailer',
C1E: 'Vehicle class C1 with trailer',
CE: 'Vehicle class C with trailer',
D1E: 'Vehicle class D1 with trailer',
DE: 'Vehicle class D with trailer',
L: 'Agricultural, forestry vehicles, forklifts max 40 km/h',
T: 'Agricultural, forestry vehicles, forklifts max 60 km/h'
},
users: 'Members',
user: {
login: 'User Login',
edit: 'Edit User',
create: 'Create User',
user: 'User',
management: 'Member Management',
id: 'Member ID',
first_name: 'First Name',
last_name: 'Last Name',
phone: 'Phone Number',
dateofbirth: 'Date of Birth',
email: 'Email',
status: 'Status',
role: 'User Role',
supporter: 'Sponsor'
},
subscription: {
name: 'Model Name',
edit: 'Edit Model',
create: 'Create Model',
subscription: 'Subscription Model',
subscriptions: 'Subscription Models',
conditions: 'Conditions',
monthly_fee: 'Monthly Fee',
hourly_rate: 'Hourly Rate',
included_hours_per_year: 'Included Hours Per Year',
included_hours_per_month: 'Included Hours Per Month'
},
loading: {
user_data: 'Loading user data',
subscription_data: 'Loading model data',
please_wait: 'Please wait...',
updating: 'Updating...'
},
dialog: {
user_deletion: 'Should the user {firstname} {lastname} really be deleted?',
subscription_deletion: 'Should the subscription model {name} really be deleted?'
},
cancel: 'Cancel',
confirm: 'Confirm',
actions: 'Actions',
edit: 'Edit',
delete: 'Delete',
search: 'Search:',
name: 'Name',
supporter: 'Sponsors',
mandate_date_signed: 'Mandate Signing Date',
licence_categories: 'Drivers License Categories',
subscription_model: 'Membership Model',
licence: 'Drivers License',
licence_number: 'Drivers License Number',
issued_date: 'Issue Date',
expiration_date: 'Expiration Date',
country: 'Country',
details: 'Details',
unknown: 'Unknown',
notes: 'Notes',
address: 'Street & House Number',
city: 'City',
zip_code: 'ZIP Code',
forgot_password: 'Forgot Password?',
password: 'Password',
confirm_password: 'Repeat Password',
password_changed: 'Password successfully changed.',
change_password: 'Change Password',
password_change_requested: 'Password change request sent... Please check your inbox.',
company: 'Company',
login: 'Login',
profile: 'Profile',
membership: 'Membership',
bankaccount: 'Bank Account',
status: 'Status',
start: 'Start',
end: 'End',
parent_member_id: 'Parent Member ID',
bank_account_holder: 'Account Holder',
bank_name: 'Bank Name',
iban: 'IBAN',
bic: 'BIC',
mandate_reference: 'SEPA Mandate',
payments: 'Payments',
add_new: 'New',
email_sent: 'Email has been sent...',
payment: {
id: 'Payment ID',
amount: 'Amount',
date: 'Date',
status: 'Status'
},
subscriptionStatus: {
pending: 'Pending',
completed: 'Completed',
failed: 'Failed',
cancelled: 'Cancelled'
}
}; };

View File

@@ -1,3 +1,12 @@
export const BASE_API_URI = import.meta.env.DEV export const BASE_API_URI = import.meta.env.DEV
? import.meta.env.VITE_BASE_API_URI_DEV ? import.meta.env.VITE_BASE_API_URI_DEV
: import.meta.env.VITE_BASE_API_URI_PROD; : import.meta.env.VITE_BASE_API_URI_PROD;
export const PERMISSIONS = {
Member: 1,
View: 2,
Update: 4,
Create: 4,
Delete: 4,
Super: 8
};

View File

@@ -200,3 +200,13 @@ export function refreshCookie(newToken, cookies) {
} }
} }
} }
/**
* checks the permission of the user
* @param {App.Locals['user']} user - The user object
* @param {number} required_permission - The required permission
* @returns {boolean} - True if the user has the required permission
*/
export function hasPrivilige(user, required_permission) {
return user.role_id >= required_permission;
}

View File

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

View File

@@ -2,8 +2,8 @@
// - Implement a load function to fetch a list of all users. // - Implement a load function to fetch a list of all users.
// - Create actions for updating user information (similar to the about/[id] route). // - Create actions for updating user information (similar to the about/[id] route).
import { BASE_API_URI } from '$lib/utils/constants'; import { BASE_API_URI, PERMISSIONS } from '$lib/utils/constants';
import { formatError, userDatesFromRFC3339 } from '$lib/utils/helpers'; import { formatError, hasPrivilige, userDatesFromRFC3339 } from '$lib/utils/helpers';
import { fail, redirect } from '@sveltejs/kit'; import { fail, redirect } from '@sveltejs/kit';
import { import {
formDataToObject, formDataToObject,
@@ -18,7 +18,7 @@ export async function load({ locals }) {
if (!locals.user) { if (!locals.user) {
throw redirect(302, `${base}/auth/login?next=${base}/auth/admin/users`); 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}`); throw redirect(302, `${base}/auth/about/${locals.user.id}`);
} }
} }

View File

@@ -6,7 +6,8 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { applyAction, enhance } from '$app/forms'; 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} */ /** @type {import('./$types').ActionData} */
export let form; export let form;
@@ -19,7 +20,7 @@
payments = [] payments = []
} = $page.data); } = $page.data);
let activeSection = 'users'; let activeSection = 'members';
/** @type{App.Locals['user'] | null} */ /** @type{App.Locals['user'] | null} */
let selectedUser = null; let selectedUser = null;
/** @type{App.Types['subscription'] | null} */ /** @type{App.Types['subscription'] | null} */
@@ -28,9 +29,21 @@
let showUserModal = false; let showUserModal = false;
let searchTerm = ''; 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 subject = 'Important Announcement';
const body = `Hello everyone,\n\nThis is an important message.`; const body = `Hello everyone,\n\nThis is an important message.`;
const bccEmails = filteredUsers 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'][]}*/ * @return {App.Locals['user'][]}*/
const getFilteredUsers = () => { const getFilteredUsers = (userSet) => {
if (!searchTerm.trim()) return users; if (!searchTerm.trim()) return userSet;
const term = searchTerm.trim().toLowerCase(); const term = searchTerm.trim().toLowerCase();
return users.filter((/** @type{App.Locals['user']}*/ user) => { return userSet.filter((/** @type{App.Locals['user']}*/ user) => {
const basicMatch = [ const basicMatch = [
user.first_name?.toLowerCase(), user.first_name?.toLowerCase(),
user.last_name?.toLowerCase(), user.last_name?.toLowerCase(),
@@ -124,12 +138,22 @@
<ul class="nav-list"> <ul class="nav-list">
<li> <li>
<button <button
class="nav-link {activeSection === 'users' ? 'active' : ''}" class="nav-link {activeSection === 'members' ? 'active' : ''}"
on:click={() => setActiveSection('users')} on:click={() => setActiveSection('members')}
> >
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
{$t('users')} {$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> </button>
</li> </li>
<li> <li>
@@ -168,7 +192,7 @@
{/each} {/each}
{/if} {/if}
{#if activeSection === 'users'} {#if activeSection === 'members'}
<div class="section-header"> <div class="section-header">
<h2>{$t('users')}</h2> <h2>{$t('users')}</h2>
<div class="title-container"> <div class="title-container">
@@ -183,7 +207,7 @@
<button <button
class="btn primary" class="btn primary"
aria-label="Mail Users" aria-label="Mail Users"
on:click={() => handleMailButtonClick()} on:click={() => handleMailButtonClick(filteredMembers)}
> >
<i class="fas fa-envelope"></i> <i class="fas fa-envelope"></i>
</button> </button>
@@ -196,7 +220,108 @@
</div> </div>
</div> </div>
<div class="accordion"> <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"> <details class="accordion-item">
<summary class="accordion-header"> <summary class="accordion-header">
{user.first_name} {user.first_name}
@@ -272,7 +397,7 @@
{:else if activeSection === 'subscriptions'} {:else if activeSection === 'subscriptions'}
<div class="section-header"> <div class="section-header">
<h2>{$t('subscription.subscriptions')}</h2> <h2>{$t('subscription.subscriptions')}</h2>
{#if user.role_id == 8} {#if hasPrivilige(user, PERMISSIONS.Super)}
<button class="btn primary" on:click={() => openEditSubscriptionModal(null)}> <button class="btn primary" on:click={() => openEditSubscriptionModal(null)}>
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{$t('add_new')} {$t('add_new')}
@@ -285,7 +410,7 @@
<summary class="accordion-header"> <summary class="accordion-header">
{subscription.name} {subscription.name}
<span class="nav-badge" <span class="nav-badge"
>{users.filter( >{members.filter(
(/** @type{App.Locals['user']}*/ user) => (/** @type{App.Locals['user']}*/ user) =>
user.membership?.subscription_model?.name === subscription.name user.membership?.subscription_model?.name === subscription.name
).length}</span ).length}</span
@@ -328,7 +453,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{#if user.role_id == 8} {#if hasPrivilige(user, PERMISSIONS.Super)}
<div class="button-group"> <div class="button-group">
<button <button
class="btn primary" class="btn primary"
@@ -337,7 +462,7 @@
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
{$t('edit')} {$t('edit')}
</button> </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 <form
method="POST" method="POST"
action="?/subscriptionDelete" action="?/subscriptionDelete"
@@ -415,7 +540,7 @@
<Modal on:close={close}> <Modal on:close={close}>
<UserEditForm <UserEditForm
{form} {form}
role_id={user.role_id} editor={user}
user={selectedUser} user={selectedUser}
{subscriptions} {subscriptions}
{licence_categories} {licence_categories}

View File

@@ -66,22 +66,24 @@ var Priviliges = struct {
Update int8 Update int8
Delete int8 Delete int8
}{ }{
View: 1, View: 2,
Update: 4, Update: 4,
Create: 4, Create: 4,
Delete: 4, Delete: 4,
} }
var Roles = struct { var Roles = struct {
Member int8 Supporter int8
Viewer int8 Member int8
Editor int8 Viewer int8
Admin int8 Editor int8
Admin int8
}{ }{
Member: 0, Supporter: 0,
Viewer: 1, Member: 1,
Editor: 4, Viewer: 2,
Admin: 8, Editor: 4,
Admin: 8,
} }
var MemberUpdateFields = map[string]bool{ var MemberUpdateFields = map[string]bool{