add admin settings
All checks were successful
delpoy / build-and-deploy (push) Successful in 40s

This commit is contained in:
bytedream 2023-11-30 19:15:00 +01:00
parent 44454f445f
commit 235dfe3094
12 changed files with 176 additions and 9 deletions

View File

@ -5,6 +5,8 @@ export class Permissions {
static readonly UserWrite = 16; static readonly UserWrite = 16;
static readonly ReportRead = 32; static readonly ReportRead = 32;
static readonly ReportWrite = 64; static readonly ReportWrite = 64;
static readonly SettingsRead = 128;
static readonly SettingsWrite = 256;
readonly value: number; readonly value: number;
@ -33,7 +35,9 @@ export class Permissions {
Permissions.UserRead, Permissions.UserRead,
Permissions.UserWrite, Permissions.UserWrite,
Permissions.ReportRead, Permissions.ReportRead,
Permissions.ReportWrite Permissions.ReportWrite,
Permissions.SettingsRead,
Permissions.SettingsWrite
]; ];
} }
@ -55,6 +59,12 @@ export class Permissions {
reportWrite(): boolean { reportWrite(): boolean {
return (this.value & Permissions.ReportWrite) != 0; return (this.value & Permissions.ReportWrite) != 0;
} }
settingsRead(): boolean {
return (this.value & Permissions.SettingsRead) != 0;
}
settingsWrite(): boolean {
return (this.value & Permissions.SettingsWrite) != 0;
}
asArray(): number[] { asArray(): number[] {
const array = []; const array = [];

View File

@ -103,8 +103,23 @@ export class Admin extends Model {
} }
} }
@Table({ modelName: 'settings', underscored: true })
export class Settings extends Model {
@Column({ type: DataTypes.STRING, allowNull: false, unique: true })
declare key: string;
@Column({
type: DataTypes.STRING,
allowNull: false,
get(this: Settings): any {
const value = this.getDataValue('value');
return value != null ? JSON.parse(value) : null;
}
})
declare value: string;
}
export const sequelize = new Sequelize(building ? 'sqlite::memory:' : env.DATABASE_URI, { export const sequelize = new Sequelize(building ? 'sqlite::memory:' : env.DATABASE_URI, {
// only log sql queries in dev mode // only log sql queries in dev mode
logging: dev ? console.log : false, logging: dev ? console.log : false,
models: [User, Report, Admin] models: [User, Report, Admin, Settings]
}); });

View File

@ -16,6 +16,7 @@ export const load: LayoutServerLoad = async ({ route, cookies }) => {
? await Report.count({ where: { draft: false, status: ['none', 'review'] } }) ? await Report.count({ where: { draft: false, status: ['none', 'review'] } })
: null, : null,
adminCount: session?.permissions.adminRead() ? await Admin.count() : null, adminCount: session?.permissions.adminRead() ? await Admin.count() : null,
settingsRead: session?.permissions.settingsRead(),
self: session self: session
? JSON.parse(JSON.stringify(await Admin.findOne({ where: { id: session.userId } }))) ? JSON.parse(JSON.stringify(await Admin.findOne({ where: { id: session.userId } })))
: null : null

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { ArrowLeftOnRectangle, Flag, UserGroup, Users } from 'svelte-heros-v2'; import { ArrowLeftOnRectangle, Cog6Tooth, Flag, UserGroup, Users } from 'svelte-heros-v2';
import { buttonTriggeredRequest } from '$lib/components/utils'; import { buttonTriggeredRequest } from '$lib/components/utils';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { LayoutData } from './$types'; import type { LayoutData } from './$types';
@ -44,6 +44,13 @@
name: 'Website Admins', name: 'Website Admins',
badge: $adminCount, badge: $adminCount,
enabled: data.adminCount != null enabled: data.adminCount != null
},
{
path: `${env.PUBLIC_BASE_PATH}/admin/settings`,
icon: Cog6Tooth,
name: 'Website Einstellungen',
badge: null,
enabled: data.settingsRead
} }
]; ];
</script> </script>
@ -57,7 +64,9 @@
<a href={tab.path}> <a href={tab.path}>
<svelte:component this={tab.icon} /> <svelte:component this={tab.icon} />
<span class="mr-1" class:underline={$page.url.pathname === tab.path}>{tab.name}</span> <span class="mr-1" class:underline={$page.url.pathname === tab.path}>{tab.name}</span>
{#if tab.badge != null}
<div class="badge">{tab.badge}</div> <div class="badge">{tab.badge}</div>
{/if}
</a> </a>
</li> </li>
{/if} {/if}
@ -74,7 +83,7 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="h-full w-full overflow-y-scroll overflow-x-hidden"> <div class="h-full w-full -mt-12 pt-12 overflow-y-scroll overflow-x-hidden">
<slot /> <slot />
</div> </div>
{:else} {:else}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { Flag, UserGroup, Users } from 'svelte-heros-v2'; import { Cog6Tooth, Flag, UserGroup, Users } from 'svelte-heros-v2';
export let data: PageData; export let data: PageData;
@ -23,6 +23,12 @@
icon: Users, icon: Users,
name: 'Website Admins', name: 'Website Admins',
enabled: data.adminCount != null enabled: data.adminCount != null
},
{
path: `${env.PUBLIC_BASE_PATH}/admin/settings`,
icon: Cog6Tooth,
name: 'Website Einstellungen',
enabled: data.settingsRead
} }
]; ];
</script> </script>

View File

@ -16,7 +16,9 @@
'User Read': Permissions.UserRead, 'User Read': Permissions.UserRead,
'User Write': Permissions.UserWrite, 'User Write': Permissions.UserWrite,
'Report Read': Permissions.ReportRead, 'Report Read': Permissions.ReportRead,
'Report Write': Permissions.ReportWrite 'Report Write': Permissions.ReportWrite,
'Settings Read': Permissions.SettingsRead,
'Settings Write': Permissions.SettingsWrite
}; };
let newAdminUsername: string; let newAdminUsername: string;

View File

@ -0,0 +1,30 @@
import type { PageServerLoad } from './$types';
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/public';
import { Settings } from '$lib/server/database';
export const load: PageServerLoad = async ({ parent, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.SettingsRead] }) == null) {
throw redirect(302, `${env.PUBLIC_BASE_PATH}/admin`);
}
const { self } = await parent();
const settings = (await Settings.findAll()).reduce(
(prev, curr) => {
return { ...prev, [curr.key]: curr.value };
},
{} as { [key: string]: any }
);
return {
settings: {
register: {
enabled: settings['register.enabled'] ?? true
}
},
self: self
};
};

View File

@ -0,0 +1,42 @@
<script lang="ts">
import type { PageData } from './$types';
import { env } from '$env/dynamic/public';
export let data: PageData;
let settings = structuredClone(data.settings);
async function change() {
await fetch(`${env.PUBLIC_BASE_PATH}/admin/settings`, {
method: 'POST',
body: JSON.stringify({
register: {
enabled: returnIfNoDup(settings.register.enabled, data.settings.register.enabled)
}
} as PageData['settings'])
});
data.settings = settings;
settings = structuredClone(data.settings);
}
function returnIfNoDup<T>(value: T, original: T): T | undefined {
return value != original ? value : undefined;
}
</script>
<div class="h-full flex flex-col items-center justify-between">
<div class="grid grid-cols-3 w-full [&>*]:mx-8">
<div>
<div class="divider">Anmeldung</div>
<label class="label cursor-pointer">
<span class="label-text">Aktiviert</span>
<input type="checkbox" class="toggle" bind:checked={settings.register.enabled} />
</label>
</div>
</div>
<div class="mb-6">
<button
class="btn btn-success mt-auto"
class:btn-disabled={JSON.stringify(data.settings) === JSON.stringify(settings)}
on:click={change}>Speichern</button
>
</div>
</div>

View File

@ -0,0 +1,30 @@
import type { PageData } from './$types';
import type { RequestHandler } from '@sveltejs/kit';
import { getSession } from '$lib/server/session';
import { Permissions } from '$lib/permissions';
import { Settings } from '$lib/server/database';
export const POST = (async ({ request, cookies }) => {
if (getSession(cookies, { permissions: [Permissions.SettingsWrite] }) == null) {
return new Response(null, {
status: 401
});
}
const settings: PageData['settings'] = await request.json();
for (const [key, value] of Object.entries(settings.register)) {
const setting = await Settings.findOne({ where: { key: `register.${key}` } });
if (setting) {
setting.value = JSON.stringify(value);
await setting.save();
} else {
await Settings.create({
key: `register.${key}`,
value: JSON.stringify(value)
});
}
}
return new Response();
}) satisfies RequestHandler;

View File

@ -0,0 +1,8 @@
import type { PageServerLoad } from './$types';
import { Settings } from '$lib/server/database';
export const load: PageServerLoad = async () => {
return {
enabled: (await Settings.findOne({ where: { key: 'register.enabled' } }))?.value ?? true
};
};

View File

@ -2,9 +2,12 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import RegistrationComplete from './RegistrationComplete.svelte'; import RegistrationComplete from './RegistrationComplete.svelte';
import Register from './Register.svelte'; import Register from './Register.svelte';
import type { PageData } from './$types';
let registered = false; let registered = false;
let username: string | null = null; let username: string | null = null;
export let data: PageData;
</script> </script>
<svelte:head> <svelte:head>
@ -13,9 +16,16 @@
<!--the tooltip when not all fields are correctly filled won't completely show if the overflow is hidden--> <!--the tooltip when not all fields are correctly filled won't completely show if the overflow is hidden-->
<div <div
class="grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 my-12 bg-base-100 shadow-lg h-min" class="relative grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 my-12 bg-base-100 shadow-lg h-min"
class:overflow-hidden={registered} class:overflow-hidden={registered}
> >
{#if !data.enabled}
<div
class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 backdrop-blur-sm z-50 rounded-xl flex justify-center md:items-center pt-20 md:pt-0"
>
<h1 class="text-2xl sm:text-3xl md:text-5xl text-white">Anmeldung geschlossen</h1>
</div>
{/if}
{#if !registered} {#if !registered}
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
<Register <Register

View File

@ -1,8 +1,12 @@
import { ApiError, getJavaUuid, getNoAuthUuid, UserNotFoundError } from '$lib/server/minecraft'; import { ApiError, getJavaUuid, getNoAuthUuid, UserNotFoundError } from '$lib/server/minecraft';
import { error, type RequestHandler } from '@sveltejs/kit'; import { error, type RequestHandler } from '@sveltejs/kit';
import { User } from '$lib/server/database'; import { Settings, User } from '$lib/server/database';
export const POST = (async ({ request }) => { export const POST = (async ({ request }) => {
if ((await Settings.findOne({ where: { key: 'register.enabled' } }))?.value === false) {
throw error(400, 'Anmeldung geschlossen');
}
const data = await request.formData(); const data = await request.formData();
const firstname = data.get('firstname') as string | null; const firstname = data.get('firstname') as string | null;
const lastname = data.get('lastname') as string | null; const lastname = data.get('lastname') as string | null;