Implemented permissions and user administration (#2)
Reviewed-on: https://git.maschek.info/Patrick/Stundenaufzeichnung/pulls/2
This commit is contained in:
parent
d0af8e9b2e
commit
f801f82faa
|
|
@ -5,6 +5,7 @@ export interface UserEntry {
|
|||
address: string;
|
||||
username: string;
|
||||
password: string;
|
||||
permissions: number;
|
||||
created: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"USERADMIN": {
|
||||
"size": 8,
|
||||
"permissions": {
|
||||
"VIEW": {
|
||||
"position": 0,
|
||||
"name": "Anzeigen"
|
||||
},
|
||||
"ADD": {
|
||||
"position": 1,
|
||||
"name": "Anlegen"
|
||||
},
|
||||
"DELETE": {
|
||||
"position": 2,
|
||||
"name": "Entfernen"
|
||||
},
|
||||
"EDIT": {
|
||||
"position": 3,
|
||||
"name": "Bearbeiten"
|
||||
},
|
||||
"ADMIN": {
|
||||
"position": 4,
|
||||
"name": "Administration"
|
||||
}
|
||||
},
|
||||
"meta_permissions": {
|
||||
"MANAGE": {
|
||||
"permissions": ["VIEW", "ADD", "DELETE", "EDIT"],
|
||||
"name": "Verwalten"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
|
||||
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 {
|
||||
size: number
|
||||
permissions: GroupPermissionsDef
|
||||
meta_permissions?: GroupMetaPermissionsDef
|
||||
}
|
||||
|
||||
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"
|
||||
const config = raw as const
|
||||
|
||||
const __validate_config = (config: unknown): config is PermissionDef => {
|
||||
const error = (message: string) => new Error("Failed to parse permissions.config.json: " + message)
|
||||
|
||||
if (typeof config !== "object" || config === null) throw error("configuration is not an object or null.")
|
||||
|
||||
for (const [group, definition] of Object.entries(config)) {
|
||||
if (typeof definition !== "object")
|
||||
throw error(`definition for ${group} is not an object (is ${typeof definition})`)
|
||||
if (definition == null)
|
||||
throw error(`definition for ${group} is null`)
|
||||
|
||||
if (typeof definition.size !== "number")
|
||||
throw error(`type of size in group ${group} is not number (is ${typeof definition.size})`)
|
||||
|
||||
if (typeof definition.permissions !== "object")
|
||||
throw error(`definition of permissions for group ${group} is not an object`)
|
||||
|
||||
for (const [name, detail] of Object.entries(definition.permissions)) {
|
||||
if (typeof detail !== "object" || detail == null)
|
||||
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})`)
|
||||
}
|
||||
|
||||
if (definition.meta_permissions !== undefined) {
|
||||
const permissions = Object.keys(definition.permissions)
|
||||
|
||||
if (typeof definition.meta_permissions !== "object")
|
||||
throw error(`meta_permissions of group ${group} is not an object (is ${typeof definition.meta_permissions})`)
|
||||
for (const [name, parts] of Object.entries(definition.meta_permissions)) {
|
||||
if (Object.keys(definition.permissions).includes(name))
|
||||
throw error(`meta permission ${name} uses the same name as the permission`)
|
||||
if (typeof parts !== "object" || parts == null)
|
||||
throw error(`definition of meta permission ${name} in group ${group} is not an object`)
|
||||
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))
|
||||
throw error(`permission ${partial} of definition of meta permission ${name} in group ${group} is not a permission of this group`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type PermissionGroups = keyof typeof config
|
||||
|
||||
function toPermissionObj(config: PermissionDef): Record<PermissionGroups, Record<string, number>> {
|
||||
let curr_pos = 0
|
||||
const obj: Record<string, Record<string, number>> = {}
|
||||
|
||||
for (const [name, definition] of Object.entries(config)) {
|
||||
obj[name] = {}
|
||||
for (const [permission, detail] of Object.entries(definition.permissions)) {
|
||||
const mask = 1 << (curr_pos + detail.position)
|
||||
obj[name][permission] = mask
|
||||
_display_names.set(mask, detail.name)
|
||||
}
|
||||
for (const [meta, detail] of Object.entries(definition?.meta_permissions ?? {})) {
|
||||
let mask = 0;
|
||||
for (const permission of detail.permissions) {
|
||||
mask |= 1 << definition.permissions[permission].position
|
||||
}
|
||||
|
||||
obj[name][meta] = mask << curr_pos
|
||||
_display_names.set(mask, detail.name)
|
||||
}
|
||||
|
||||
curr_pos += definition.size
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
__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 {
|
||||
...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,
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import Bun from 'bun';
|
||||
|
||||
import { User, get_user_by_name } from "$lib/server/database";
|
||||
import { User, get_user_by_name, update_user_password } from "$lib/server/database";
|
||||
|
||||
export async function authorize_password(username: string, password: string): Promise<User | null> {
|
||||
|
||||
|
|
@ -12,4 +12,8 @@ export async function authorize_password(username: string, password: string): Pr
|
|||
return res ? user : null;
|
||||
}
|
||||
|
||||
export async function change_password(user_id: number, new_password: string): Promise<boolean> {
|
||||
const hash = await Bun.password.hash(new_password, { algorithm: "bcrypt", cost: 11});
|
||||
|
||||
return update_user_password(user_id, hash)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { Database, SQLiteError } from "bun:sqlite";
|
|||
import type { UserEntry, RecordEntry, EstimatesEntry } from "$lib/db_types";
|
||||
import { calculateDuration, parseDate, toInt, isTimeValidHHMM } from "$lib/util";
|
||||
|
||||
import Logs from "$lib/server/log"
|
||||
|
||||
const DATABASES_PATH: string = (process.env.APP_USER_DATA_PATH ?? ".") + "/databases/";
|
||||
const USER_DATABASE_PATH: string = DATABASES_PATH + "users.sqlite";
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ const USER_DATABASE_SETUP: string[] = [
|
|||
address TEXT,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
permissions INTEGER DEFAULT 0,
|
||||
created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);`,
|
||||
|
||||
|
|
@ -47,8 +50,11 @@ const USER_DATABASE_SETUP: string[] = [
|
|||
const USER_DATABASE_ADD_USER: string =
|
||||
"INSERT INTO users (name, gender, address, username, password) VALUES ($name, $gender, $address, $username, $password);";
|
||||
|
||||
const USER_DATABASE_GET_USER: string =
|
||||
"SELECT * FROM users;";
|
||||
const USER_DATABASE_GET_ALL_USER: string =
|
||||
"SELECT id, username, name FROM users;";
|
||||
|
||||
const USER_DATABASE_GET_USER_BY_ID: string =
|
||||
"SELECT * FROM users WHERE id = $id;"
|
||||
|
||||
const USER_DATABASE_GET_USER_BY_NAME: string =
|
||||
"SELECT * FROM users WHERE username = $username;"
|
||||
|
|
@ -56,6 +62,9 @@ const USER_DATABASE_GET_USER_BY_NAME: string =
|
|||
const USER_DATABASE_EMPTY: string =
|
||||
"SELECT EXISTS (SELECT 1 FROM users);"
|
||||
|
||||
const USER_DATABASE_UPDATE_PASSWORD: string =
|
||||
"UPDATE users SET password=$password WHERE id=$id;"
|
||||
|
||||
/*const USER_DATABASE_ADD_ACCESS_TOKEN: string =
|
||||
"INSERT INTO access_tokens (user_id, token, expiry_date) VALUES ($user_id, $token, $expiry_date);"
|
||||
|
||||
|
|
@ -82,7 +91,7 @@ const ENTRY_DATABASE_SETUP: string[] = [
|
|||
record_id INTEGER NOT NULL,
|
||||
date VARCHAR(10),
|
||||
start VARCHAR(5),
|
||||
end VARCHAR(5),
|
||||
end V<F52>ARCHAR(5),
|
||||
comment TEXT,
|
||||
modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY(record_id) REFERENCES records(id)
|
||||
|
|
@ -179,7 +188,8 @@ export class User {
|
|||
name: string;
|
||||
address: string;
|
||||
username: string;
|
||||
password: string
|
||||
password: string;
|
||||
permissions: number;
|
||||
created: string;
|
||||
|
||||
private _database: Database;
|
||||
|
|
@ -191,10 +201,24 @@ export class User {
|
|||
this.address = user.address;
|
||||
this.username = user.username;
|
||||
this.password = user.password;
|
||||
this.permissions = user.permissions;
|
||||
this.created = user.created;
|
||||
this._database = db;
|
||||
}
|
||||
|
||||
toUserEntry(): UserEntry {
|
||||
return {
|
||||
id: this.id,
|
||||
gender: this.gender,
|
||||
name: this.name,
|
||||
address: this.address,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
permissions: this.permissions,
|
||||
created: this.created,
|
||||
}
|
||||
}
|
||||
|
||||
get_months(): { year: string, month: string }[] {
|
||||
const query = this._database.query(ENTRY_DATABASE_GET_MONTHS);
|
||||
const res = query.all();
|
||||
|
|
@ -378,51 +402,30 @@ export async function create_user(user: { name: string, gender: string, address:
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function _get_user(): UserEntry | null {
|
||||
export function get_all_user(): { id: number, username: string, name: string }[] {
|
||||
|
||||
try {
|
||||
const statement = user_database.prepare(USER_DATABASE_GET_USER);
|
||||
const result = statement.get() as UserEntry;
|
||||
|
||||
return result;
|
||||
|
||||
const query = user_database.prepare(USER_DATABASE_GET_ALL_USER)
|
||||
const user = query.all() as { id: number, username: string, name: string }[]
|
||||
|
||||
return user
|
||||
} catch (e) {
|
||||
if (e instanceof SQLiteError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function get_user(id: number): User | null {
|
||||
|
||||
const user = _get_user();
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const db_name = get_user_db_name(user);
|
||||
export function get_user_entry_by_id(id: number): UserEntry | null {
|
||||
|
||||
try {
|
||||
const query = user_database.query(USER_DATABASE_GET_USER_BY_ID)
|
||||
const user = query.get({ id: id }) as UserEntry | null;
|
||||
|
||||
fs.mkdir(DATABASES_PATH, { recursive: true });
|
||||
|
||||
let userdb = new Database(db_name, { create: true, strict: true });
|
||||
|
||||
if (!is_db_initialized(userdb)) {
|
||||
setup_db(userdb, ENTRY_DATABASE_SETUP);
|
||||
}
|
||||
|
||||
return new User(user, userdb);
|
||||
return user
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
|
||||
return null;
|
||||
Logs.db.error(`Encountered exception when retrievieng user ${id} from database: ${e.message}`)
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function get_user_by_name(username: string): User | null {
|
||||
|
|
@ -459,3 +462,26 @@ export function do_users_exist(): any {
|
|||
// sqlite trims the first "SELECT " and ";" from the query string
|
||||
return (answer as any)?.[USER_DATABASE_EMPTY.slice(7, -1)];
|
||||
}
|
||||
|
||||
export function updateUser(data: {id: number, gender?: string, name?: string, address?: string, username?: string, permissions?: number }) {
|
||||
let changed: Array<string> = []
|
||||
if (data.gender) changed.push("gender=$gender")
|
||||
if (data.name) changed.push("name=$name")
|
||||
if (data.address) changed.push("address=$address")
|
||||
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 query = user_database.prepare(update_query)
|
||||
const result = query.run(data)
|
||||
|
||||
return get_user_by_name(data?.name ?? "") // GET USER BY lastRowId from result
|
||||
}
|
||||
|
||||
export function update_user_password(user_id: number, password: string) {
|
||||
const query = user_database.prepare(USER_DATABASE_UPDATE_PASSWORD)
|
||||
const result = query.run({ password: password, id: user_id })
|
||||
|
||||
return result.changes > 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,13 @@ function logout_user_session(token: string): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
function reload_user_data(user: User) {
|
||||
const session_info = active_users.get(user.id)
|
||||
if (!session_info) return
|
||||
|
||||
session_info.user = user
|
||||
}
|
||||
|
||||
async function __clean_session_store() {
|
||||
|
||||
let cleaned_user_sessions = 0
|
||||
|
|
@ -135,6 +142,7 @@ export default class SessionStore {
|
|||
static issue_access_token_for_user = issue_access_token_for_user;
|
||||
static get_user_by_access_token = get_user_by_access_token;
|
||||
static logout_user_session = logout_user_session;
|
||||
static reload_user_data = reload_user_data;
|
||||
}
|
||||
|
||||
setInterval(__clean_session_store, 15*60*1000);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import type { LayoutServerLoad } from "./$types"
|
||||
|
||||
import Permissions from "$lib/permissions"
|
||||
|
||||
export const load: LayoutServerLoad = ({ locals }) => {
|
||||
return {
|
||||
loggedInAs: locals.user?.toUserEntry(),
|
||||
isAdmin: Permissions.any(locals.user?.permissions ?? 0, Permissions.ALL(Permissions.USERADMIN))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,34 @@
|
|||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
import type { LayoutProps } from './$types'
|
||||
|
||||
let { data, children }: LayoutProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet nav(classlist: string)}
|
||||
<div class={classlist}>
|
||||
<h1>Navigation</h1>
|
||||
<ul>
|
||||
<li><a href="/">Stundenliste</a></li>
|
||||
<li><a href="/schaetzung">Stundenschätzung</a></li>
|
||||
<li><a href="/dokumente">Dokumente</a></li>
|
||||
|
||||
<li class="separator"></li>
|
||||
<li>Benutzerverwaltung</li>
|
||||
<li>
|
||||
<form method="POST" action="/login?/logout">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class={classlist}>
|
||||
<h1>Navigation</h1>
|
||||
<ul>
|
||||
<li><a href="/">Stundenliste</a></li>
|
||||
<li><a href="/schaetzung">Stundenschätzung</a></li>
|
||||
<li><a href="/dokumente">Dokumente</a></li>
|
||||
|
||||
<li class="separator"></li>
|
||||
{#if data?.loggedInAs != null}
|
||||
<li>Eingeloggt als: {data.loggedInAs.username}</li>
|
||||
<li><a href="/user">Account</a></li>
|
||||
{/if}
|
||||
{#if data?.isAdmin === true}
|
||||
<li><a href="/useradmin">Benutzerverwaltung</a></li>
|
||||
{/if}
|
||||
{#if data?.loggedInAs != null}
|
||||
<li>
|
||||
<form method="POST" action="/login?/logout">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="navcontainer">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import type { PageServerLoad, Actions } from "./$types"
|
||||
|
||||
import type { UserEntry } from "$lib/db_types"
|
||||
|
||||
import { fail, redirect } from "@sveltejs/kit"
|
||||
|
||||
import Permissions from "$lib/permissions"
|
||||
import { toInt } from "$lib/util"
|
||||
|
||||
import Logs from "$lib/server/log"
|
||||
import SessionStore from "$lib/server/session_store"
|
||||
import { get_user_entry_by_id, updateUser } from "$lib/server/database"
|
||||
import { change_password } from "$lib/server/auth"
|
||||
|
||||
export const load: PageServerLoad = ({ locals, url }) => {
|
||||
|
||||
if (locals.user == null) {
|
||||
return fail(401, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
let user: UserEntry|null = locals.user.toUserEntry()
|
||||
|
||||
if (locals.user.id != (toInt(url.searchParams.get("user") ?? locals.user.id.toFixed(0)))) {
|
||||
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) {
|
||||
return fail(403, { message: "Insufficient Permissions" })
|
||||
}
|
||||
|
||||
let user_id = toInt(url.searchParams.get("user") ?? "")
|
||||
|
||||
if (isNaN(user_id)) {
|
||||
return fail(400, { message: `Invalid user id: ${url.searchParams.get("user")}`})
|
||||
}
|
||||
|
||||
user = get_user_entry_by_id(user_id)
|
||||
|
||||
if (user == null) {
|
||||
return fail(404, { message: `User ${user_id} not found` })
|
||||
}
|
||||
|
||||
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)) {
|
||||
user.permissions = 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user: user
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const actions = {
|
||||
edit: async ({locals, request}) => {
|
||||
|
||||
if (locals.user == null) {
|
||||
Logs.route.warn("An unauthorized user tried to edit an user")
|
||||
return fail(401, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
const data = await request.formData();
|
||||
|
||||
const id = toInt((data.get("id") as string|null) ?? "NaN")
|
||||
const name = data.get("name") as string|null
|
||||
const gender = data.get("gender") as string|null
|
||||
const address = data.get("address") as string|null
|
||||
|
||||
const username = data.get("username") as string|null
|
||||
const password1 = data.get("password1") as string|null
|
||||
const password2 = data.get("password2") as string|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" })
|
||||
}
|
||||
|
||||
if (locals.user.id != id
|
||||
&& (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.EDIT)
|
||||
|| ((password1 != null || password2 != null) && !Permissions.has(locals.user.permissions, Permissions.USERADMIN.ADMIN)))) {
|
||||
return fail(403, { message: "Unauthorized action" })
|
||||
}
|
||||
|
||||
if (password1 != null && password2 != null && password1.length > 0 && password2.length > 0) {
|
||||
if (password1 != password2) {
|
||||
return fail(400, { message: "Passwörter müssen übereinstimmen" })
|
||||
}
|
||||
const result = change_password(id, password1)
|
||||
if (!result) {
|
||||
return fail(500, { message: "Database failure"})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return { message: "Erfolgreich gespeichert" }
|
||||
|
||||
}
|
||||
} satisfies Actions
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageProps } from "./$types"
|
||||
import { enhance } from "$app/forms"
|
||||
import { page } from "$app/state"
|
||||
|
||||
import Permissions from "$lib/permissions"
|
||||
|
||||
const { data, form }: PageProps = $props()
|
||||
|
||||
</script>
|
||||
|
||||
<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} />
|
||||
|
||||
<div class="root">
|
||||
|
||||
<h1>Benutzer</h1>
|
||||
|
||||
<p>{form?.message}</p>
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="leader2" />
|
||||
<col class="form2" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th colspan="2">Persönliche Daten</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td><input type="text" name="name" value={data.user.name} /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Geschlecht</td>
|
||||
<td><select name="gender">
|
||||
<option selected={ data.user?.gender === "M" }>M</option>
|
||||
<option selected={ data.user?.gender === "W" }>W</option></select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Addresse</td>
|
||||
<td><input type="text" name="address" value={data.user.address} /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="leader3" />
|
||||
<col class="form3" />
|
||||
<col class="form3" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th colspan="3">Benutzer</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Benutzername</td>
|
||||
<td colspan="2"><input type="text" name="username" value={data.user.username} /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Passwort ändern</td>
|
||||
<td><input type="password" name="password1" /></td>
|
||||
<td><input type="password" name="password2" placeholder="Wiederholen"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<style>
|
||||
|
||||
.root {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.form2 {
|
||||
width: 75%;
|
||||
}
|
||||
.form3 {
|
||||
width: 37.5%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
border: none;
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
|
||||
label:has(input[type="checkbox"][disabled]) {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 15%;
|
||||
margin-left: 85%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import type { PageServerLoad, Actions } from "./$types"
|
||||
|
||||
import { fail } from "@sveltejs/kit"
|
||||
|
||||
import Permissions from "$lib/permissions";
|
||||
|
||||
import { get_all_user } from "$lib/server/database"
|
||||
|
||||
export const load: PageServerLoad = ({ locals }) => {
|
||||
if (locals.user == null) {
|
||||
return fail(403, { message: "Unauthorized user" })
|
||||
}
|
||||
|
||||
if (!Permissions.has(locals.user.permissions, Permissions.USERADMIN.VIEW)) {
|
||||
//return fail(403, { message: "No permission" })
|
||||
}
|
||||
|
||||
const user = get_all_user()
|
||||
|
||||
return {
|
||||
user: user
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
} satisfies Actions
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { PageProps } from "./$types"
|
||||
import { enhance } from "$app/forms";
|
||||
|
||||
const { data }: PageProps = $props();
|
||||
|
||||
console.log(data)
|
||||
|
||||
</script>
|
||||
|
||||
<form method="GET" id="form_manage_user" action="user"></form>
|
||||
|
||||
<div>
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
|
||||
<select form="form_manage_user" name="user" size="10" required>
|
||||
{#each data.user as user (user.id)}
|
||||
<option value={user.id}>{user.username} | {user.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button type="submit" form="form_manage_user">Edit</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
position: absolute;
|
||||
top: 33%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
Loading…
Reference in New Issue