Compare commits
10 Commits
803dcd19cb
...
8620e9b0cd
| Author | SHA1 | Date |
|---|---|---|
|
|
8620e9b0cd | |
|
|
b1787cda4e | |
|
|
0cd32a0276 | |
|
|
1049b04968 | |
|
|
a46c302be8 | |
|
|
23752001e7 | |
|
|
98570a1e5f | |
|
|
1f72e586f2 | |
|
|
9093ddaeb5 | |
|
|
b2e6f059a6 |
|
|
@ -2,14 +2,32 @@
|
||||||
"USERADMIN": {
|
"USERADMIN": {
|
||||||
"size": 8,
|
"size": 8,
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"VIEW": 0,
|
"VIEW": {
|
||||||
"ADD": 1,
|
"position": 0,
|
||||||
"DELETE": 2,
|
"name": "Anzeigen"
|
||||||
"EDIT": 3,
|
},
|
||||||
"EDIT_PASSWORD": 4
|
"ADD": {
|
||||||
|
"position": 1,
|
||||||
|
"name": "Anlegen"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"position": 2,
|
||||||
|
"name": "Entfernen"
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"position": 3,
|
||||||
|
"name": "Bearbeiten"
|
||||||
|
},
|
||||||
|
"ADMIN": {
|
||||||
|
"position": 4,
|
||||||
|
"name": "Administration"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"meta_permissions": {
|
"meta_permissions": {
|
||||||
"MANAGE": ["VIEW", "ADD", "DELETE", "EDIT"]
|
"MANAGE": {
|
||||||
|
"permissions": ["VIEW", "ADD", "DELETE", "EDIT"],
|
||||||
|
"name": "Verwalten"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,34 @@
|
||||||
|
|
||||||
type GroupPermissionsDef = Record<string, number>
|
interface GroupPermissionDetail {
|
||||||
|
position: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupMetaPermissionDetail {
|
||||||
|
permissions: Array<string>
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupPermissionsDef = Record<string, GroupPermissionDetail>
|
||||||
|
type GroupMetaPermissionsDef = Record<string, GroupMetaPermissionDetail>
|
||||||
|
|
||||||
interface GroupDef {
|
interface GroupDef {
|
||||||
size: number
|
size: number
|
||||||
permissions: GroupPermissionsDef
|
permissions: GroupPermissionsDef
|
||||||
meta_permissions?: Record<string, Array<string>>
|
meta_permissions?: GroupMetaPermissionsDef
|
||||||
}
|
}
|
||||||
|
|
||||||
type PermissionDef = Record<string, GroupDef>
|
type PermissionDef = Record<string, GroupDef>
|
||||||
|
|
||||||
|
interface PermissionDescription {
|
||||||
|
key: string
|
||||||
|
value: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const _display_names = new Map<number, string>()
|
||||||
|
|
||||||
|
|
||||||
import raw from "./permissions.config.json"
|
import raw from "./permissions.config.json"
|
||||||
const config = raw as const
|
const config = raw as const
|
||||||
|
|
@ -23,7 +41,7 @@ const __validate_config = (config: unknown): config is PermissionDef => {
|
||||||
for (const [group, definition] of Object.entries(config)) {
|
for (const [group, definition] of Object.entries(config)) {
|
||||||
if (typeof definition !== "object")
|
if (typeof definition !== "object")
|
||||||
throw error(`definition for ${group} is not an object (is ${typeof definition})`)
|
throw error(`definition for ${group} is not an object (is ${typeof definition})`)
|
||||||
if (definition === null)
|
if (definition == null)
|
||||||
throw error(`definition for ${group} is null`)
|
throw error(`definition for ${group} is null`)
|
||||||
|
|
||||||
if (typeof definition.size !== "number")
|
if (typeof definition.size !== "number")
|
||||||
|
|
@ -31,10 +49,14 @@ const __validate_config = (config: unknown): config is PermissionDef => {
|
||||||
|
|
||||||
if (typeof definition.permissions !== "object")
|
if (typeof definition.permissions !== "object")
|
||||||
throw error(`definition of permissions for group ${group} is not an object`)
|
throw error(`definition of permissions for group ${group} is not an object`)
|
||||||
for (const [name, position] of Object.entries(definition.permissions)) {
|
|
||||||
if (typeof position !== "number")
|
for (const [name, detail] of Object.entries(definition.permissions)) {
|
||||||
throw error(`position of ${name} in group ${group} is not a number (is ${typeof position})`)
|
if (typeof detail !== "object" || detail == null)
|
||||||
if (position >= definition.size)
|
throw error(`definition for permission ${name} has to be an object`)
|
||||||
|
|
||||||
|
if (typeof detail.position !== "number")
|
||||||
|
throw error(`position of ${name} in group ${group} is not a number (is ${typeof detail.position})`)
|
||||||
|
if (detail.position >= definition.size)
|
||||||
throw error(`position ${position} of permission ${name} in group ${group} is out of bounds (size is ${definition.size})`)
|
throw error(`position ${position} of permission ${name} in group ${group} is out of bounds (size is ${definition.size})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,9 +68,11 @@ const __validate_config = (config: unknown): config is PermissionDef => {
|
||||||
for (const [name, parts] of Object.entries(definition.meta_permissions)) {
|
for (const [name, parts] of Object.entries(definition.meta_permissions)) {
|
||||||
if (Object.keys(definition.permissions).includes(name))
|
if (Object.keys(definition.permissions).includes(name))
|
||||||
throw error(`meta permission ${name} uses the same name as the permission`)
|
throw error(`meta permission ${name} uses the same name as the permission`)
|
||||||
if (typeof parts !== "object" && !(parts instanceof Array) || parts === null)
|
if (typeof parts !== "object" || parts == null)
|
||||||
throw error(`definition of meta permission ${name} in group ${group} is not an array`)
|
throw error(`definition of meta permission ${name} in group ${group} is not an object`)
|
||||||
for (const partial of (parts as Array<any>)) {
|
if (typeof parts.permissions !== "object" || !(parts.permissions instanceof Array))
|
||||||
|
throw error(`definition of meta permission ${name} has to include a key "permissions" with a value of type Array<string>`)
|
||||||
|
for (const partial of (parts.permissions as Array<any>)) {
|
||||||
if (!permissions.includes(partial))
|
if (!permissions.includes(partial))
|
||||||
throw error(`permission ${partial} of definition of meta permission ${name} in group ${group} is not a permission of this group`)
|
throw error(`permission ${partial} of definition of meta permission ${name} in group ${group} is not a permission of this group`)
|
||||||
}
|
}
|
||||||
|
|
@ -67,21 +91,19 @@ function toPermissionObj(config: PermissionDef): Record<PermissionGroups, Record
|
||||||
|
|
||||||
for (const [name, definition] of Object.entries(config)) {
|
for (const [name, definition] of Object.entries(config)) {
|
||||||
obj[name] = {}
|
obj[name] = {}
|
||||||
obj[name]["ALL"] = 0;
|
for (const [permission, detail] of Object.entries(definition.permissions)) {
|
||||||
for (const [permission, value] of Object.entries(definition.permissions)) {
|
const mask = 1 << (curr_pos + detail.position)
|
||||||
if (permission == "ALL") {
|
obj[name][permission] = mask
|
||||||
throw new Error(`permission must not be called ALL in group ${name}`)
|
_display_names.set(mask, detail.name)
|
||||||
}
|
}
|
||||||
obj[name][permission] = 1 << (curr_pos + value)
|
for (const [meta, detail] of Object.entries(definition?.meta_permissions ?? {})) {
|
||||||
obj[name]["ALL"] |= obj[name][permission]
|
|
||||||
}
|
|
||||||
for (const [meta, permissions] of Object.entries(definition?.meta_permissions ?? {})) {
|
|
||||||
let mask = 0;
|
let mask = 0;
|
||||||
for (const permission in permissions) {
|
for (const permission of detail.permissions) {
|
||||||
mask |= 1 << definition.permissions[permission]
|
mask |= 1 << definition.permissions[permission].position
|
||||||
}
|
}
|
||||||
|
|
||||||
obj[name][meta] = mask << curr_pos
|
obj[name][meta] = mask << curr_pos
|
||||||
|
_display_names.set(mask, detail.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
curr_pos += definition.size
|
curr_pos += definition.size
|
||||||
|
|
@ -91,10 +113,41 @@ function toPermissionObj(config: PermissionDef): Record<PermissionGroups, Record
|
||||||
|
|
||||||
__validate_config(config)
|
__validate_config(config)
|
||||||
|
|
||||||
|
const _deconstruct = (permission: number): Array<number> => {
|
||||||
|
|
||||||
|
const parts: Array<number> = []
|
||||||
|
let mask = 1
|
||||||
|
while (permission >= mask) {
|
||||||
|
const perm = permission & mask
|
||||||
|
if (perm > 0) parts.push(perm)
|
||||||
|
|
||||||
|
mask <<= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
const _to_display_name = (permission: number): string => {
|
||||||
|
const name = _display_names.get(permission)
|
||||||
|
if (name != undefined) return name
|
||||||
|
|
||||||
|
const names = _deconstruct(permission).map((value) => _display_names.get(value)).filter((value) => value != undefined)
|
||||||
|
|
||||||
|
return names.join(" | ")
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...toPermissionObj(config),
|
...toPermissionObj(config),
|
||||||
|
|
||||||
|
ALL: (permission_group: Record<string, number>): number => Object.values(permission_group).reduce((pv: number, cv: number) => pv | cv),
|
||||||
|
|
||||||
has: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) == permissions,
|
has: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) == permissions,
|
||||||
any: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) > 0,
|
any: (user_permissions: number, permissions: number): boolean => (user_permissions & permissions) > 0,
|
||||||
|
|
||||||
|
iterate: (permission_group: Record<string, number>): Array<PermissionDescription> => Object.entries(permission_group).map(([key, value]) => ({ key: key, value: value, name: _to_display_name(value)})),
|
||||||
|
is_meta: (permission: number) => permission != 0 && ((permission & (permission - 1)) != 0),
|
||||||
|
|
||||||
|
deconstruct: _deconstruct,
|
||||||
|
display_name: _to_display_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -463,12 +463,13 @@ export function do_users_exist(): any {
|
||||||
return (answer as any)?.[USER_DATABASE_EMPTY.slice(7, -1)];
|
return (answer as any)?.[USER_DATABASE_EMPTY.slice(7, -1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUser(data: {id: number, gender?: string, name?: string, address?: string, username?: string }) {
|
export function updateUser(data: {id: number, gender?: string, name?: string, address?: string, username?: string, permissions?: number }) {
|
||||||
let changed: Array<string> = []
|
let changed: Array<string> = []
|
||||||
if (data.gender) changed.push("gender=$gender")
|
if (data.gender) changed.push("gender=$gender")
|
||||||
if (data.name) changed.push("name=$name")
|
if (data.name) changed.push("name=$name")
|
||||||
if (data.address) changed.push("address=$address")
|
if (data.address) changed.push("address=$address")
|
||||||
if (data.username) changed.push("username=$username")
|
if (data.username) changed.push("username=$username")
|
||||||
|
if (data.permissions) changed.push("permissions=$permissions")
|
||||||
|
|
||||||
const update_query = "UPDATE users SET " + changed.join(", ") + " WHERE id=$id;"
|
const update_query = "UPDATE users SET " + changed.join(", ") + " WHERE id=$id;"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Permissions from "$lib/permissions"
|
||||||
|
|
||||||
export const load: LayoutServerLoad = ({ locals }) => {
|
export const load: LayoutServerLoad = ({ locals }) => {
|
||||||
return {
|
return {
|
||||||
loggedInAs: locals.user?.username,
|
loggedInAs: locals.user?.toUserEntry(),
|
||||||
isAdmin: Permissions.any(locals.user?.permissions ?? 0, Permissions.USERADMIN.ALL)
|
isAdmin: Permissions.any(locals.user?.permissions ?? 0, Permissions.ALL(Permissions.USERADMIN))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<li class="separator"></li>
|
<li class="separator"></li>
|
||||||
{#if data?.loggedInAs != null}
|
{#if data?.loggedInAs != null}
|
||||||
<li>Eingeloggt als: {data.loggedInAs}</li>
|
<li>Eingeloggt als: {data.loggedInAs.username}</li>
|
||||||
<li><a href="/user">Account</a></li>
|
<li><a href="/user">Account</a></li>
|
||||||
{/if}
|
{/if}
|
||||||
{#if data?.isAdmin === true}
|
{#if data?.isAdmin === true}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { fail, redirect } from "@sveltejs/kit"
|
||||||
import Permissions from "$lib/permissions"
|
import Permissions from "$lib/permissions"
|
||||||
import { toInt } from "$lib/util"
|
import { toInt } from "$lib/util"
|
||||||
|
|
||||||
|
import Logs from "$lib/server/log"
|
||||||
import SessionStore from "$lib/server/session_store"
|
import SessionStore from "$lib/server/session_store"
|
||||||
import { get_user_entry_by_id, updateUser } from "$lib/server/database"
|
import { get_user_entry_by_id, updateUser } from "$lib/server/database"
|
||||||
import { change_password } from "$lib/server/auth"
|
import { change_password } from "$lib/server/auth"
|
||||||
|
|
@ -19,9 +20,9 @@ export const load: PageServerLoad = ({ locals, url }) => {
|
||||||
|
|
||||||
let user: UserEntry|null = locals.user.toUserEntry()
|
let user: UserEntry|null = locals.user.toUserEntry()
|
||||||
|
|
||||||
if (url.searchParams.has("user")) {
|
if (locals.user.id != (toInt(url.searchParams.get("user") ?? locals.user.id.toFixed(0)))) {
|
||||||
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) {
|
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) {
|
||||||
//return fail(403, { message: "Insufficient Permissions" })
|
return fail(403, { message: "Insufficient Permissions" })
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = toInt(url.searchParams.get("user") ?? "")
|
let user_id = toInt(url.searchParams.get("user") ?? "")
|
||||||
|
|
@ -35,6 +36,10 @@ export const load: PageServerLoad = ({ locals, url }) => {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return fail(404, { message: `User ${user_id} not found` })
|
return fail(404, { message: `User ${user_id} not found` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)) {
|
||||||
|
user.permissions = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -62,30 +67,41 @@ export const actions = {
|
||||||
const password1 = data.get("password1") as string|null
|
const password1 = data.get("password1") as string|null
|
||||||
const password2 = data.get("password2") as string|null
|
const password2 = data.get("password2") as string|null
|
||||||
|
|
||||||
if (isNaN(id) || name == null || gender == null || address == null || username == null) {
|
const ua_permissions = (data.getAll("USERADMIN") as string[]).map((value) => toInt(value))
|
||||||
|
|
||||||
|
if (isNaN(id) || name == null || gender == null || address == null || username == null || ua_permissions.some((permission) => isNaN(permission))) {
|
||||||
return fail(400, { message: "invalid request" })
|
return fail(400, { message: "invalid request" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (locals.user.id != id
|
if (locals.user.id != id
|
||||||
&& (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT)
|
&& (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT)
|
||||||
|| ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT_PASSWORD)))) {
|
|| ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)))) {
|
||||||
return fail(403, { message: "Unauthorized action" })
|
return fail(403, { message: "Unauthorized action" })
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((password1 != null || password2 != null)) {
|
if (password1 != null && password2 != null && password1.length > 0 && password2.length > 0) {
|
||||||
if (password1 != password2) {
|
if (password1 != password2) {
|
||||||
return fail(400, { message: "Passwörter müssen übereinstimmen" })
|
return fail(400, { message: "Passwörter müssen übereinstimmen" })
|
||||||
}
|
}
|
||||||
const result = change_password(id, password1!)
|
const result = change_password(id, password1)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return fail(500, { message: "Database failure"})
|
return fail(500, { message: "Database failure"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated_user = updateUser({id, name, gender, address, username})
|
let permissions = null
|
||||||
|
if (ua_permissions.length > 0) {
|
||||||
|
permissions = ua_permissions.reduce((pv, cv) => pv | cv)
|
||||||
|
|
||||||
|
if (locals.user.id == id && locals.user.permissions != permissions) {
|
||||||
|
return fail(403, { message: "Cannot modify permissions for oneself" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated_user = updateUser({id, name, gender, address, username, permissions})
|
||||||
SessionStore.reload_user_data(updated_user ?? locals.user)
|
SessionStore.reload_user_data(updated_user ?? locals.user)
|
||||||
|
|
||||||
return {}
|
return { message: "Erfolgreich gespeichert" }
|
||||||
|
|
||||||
}
|
}
|
||||||
} satisfies Actions
|
} satisfies Actions
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
import type { PageProps } from "./$types"
|
import type { PageProps } from "./$types"
|
||||||
import { enhance } from "$app/forms"
|
import { enhance } from "$app/forms"
|
||||||
|
import { page } from "$app/state"
|
||||||
|
|
||||||
|
import Permissions from "$lib/permissions"
|
||||||
|
|
||||||
const { data, form }: PageProps = $props()
|
const { data, form }: PageProps = $props()
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form method="POST" id="form_edit" action="?/edit" use:enhance>
|
<form method="POST" id="form_edit" action={`?/edit&${page.url.searchParams.toString()}`} use:enhance={() => {
|
||||||
|
return async ({update}) => { update({ reset: false }) }
|
||||||
|
}}>
|
||||||
<input type="hidden" name="id" value={data.user.id} />
|
<input type="hidden" name="id" value={data.user.id} />
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
|
@ -32,8 +37,8 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Geschlecht</td>
|
<td>Geschlecht</td>
|
||||||
<td><select name="gender">
|
<td><select name="gender">
|
||||||
<option>M</option>
|
<option selected={ data.user?.gender === "M" }>M</option>
|
||||||
<option>W</option></select>
|
<option selected={ data.user?.gender === "W" }>W</option></select>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -65,6 +70,81 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{#if data.user?.id == data.loggedInAs.id || Permissions.has(data.loggedInAs.permissions ?? 0, Permissions.USERADMIN.ADMIN)}
|
||||||
|
{@const disabled = data.user?.id == data.loggedInAs.id}
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col class="leader2" />
|
||||||
|
<col class="form2" />
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
|
<thead>
|
||||||
|
<tr><th colspan="2">Berechtigungen</th></tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Benutzerverwaltung</td>
|
||||||
|
<td>
|
||||||
|
<div class="permission-selector">
|
||||||
|
<input type=hidden name="USERADMIN" value="0" disabled={disabled} />
|
||||||
|
{#each Permissions.iterate(Permissions.USERADMIN) as permission}
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="USERADMIN"
|
||||||
|
value={permission.value}
|
||||||
|
checked={Permissions.has(data.user.permissions, permission.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
data-bits={Permissions.deconstruct(permission.value).join(" ")}
|
||||||
|
onclick={(event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
const container = target.parentElement?.parentElement ?? target
|
||||||
|
|
||||||
|
const bits = target.dataset?.bits?.split(" ")
|
||||||
|
if (!bits) return
|
||||||
|
|
||||||
|
if (bits.length > 1) {
|
||||||
|
for (const bit of bits) {
|
||||||
|
const affected = container.querySelectorAll(`input[data-bits="${bit}"]`) ?? []
|
||||||
|
for (const box of affected) {
|
||||||
|
(box as HTMLInputElement).checked = target.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bit = bits[0]
|
||||||
|
|
||||||
|
const affected = container.querySelectorAll(`input[data-bits~="${bit}"]`) ?? []
|
||||||
|
for (const _box of affected) {
|
||||||
|
const box = _box as HTMLInputElement
|
||||||
|
|
||||||
|
const groupBits = box.dataset.bits?.split(" ") ?? [];
|
||||||
|
const children = groupBits.map(
|
||||||
|
b => container.querySelector(`input[data-bits="${b}"]`) as HTMLInputElement
|
||||||
|
);
|
||||||
|
|
||||||
|
const allChecked = children.every(cb => cb.checked);
|
||||||
|
const anyChecked = children.some(cb => cb.checked);
|
||||||
|
|
||||||
|
box.indeterminate = (box.indeterminate && anyChecked) || (box.checked && !allChecked && anyChecked)
|
||||||
|
box.checked = allChecked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}} />
|
||||||
|
{permission.name}
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
<button type="submit">Speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,15 +172,32 @@ td {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
}
|
}
|
||||||
.form3 {
|
.form3 {
|
||||||
width: 37.5%
|
width: 37.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
.permission-selector {
|
||||||
|
width: 100%;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
.permission-selector > label {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: solid 1px black;
|
border-bottom: solid 1px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label:has(input[type="checkbox"][disabled]) {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
margin-left: 85%;
|
margin-left: 85%;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue