소스 검색

Edit user on the web (#458)

* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

* Implemented edit user and password reset
Alex 2 년 전
부모
커밋
4b9187928c

+ 3 - 0
Makefile

@@ -1,6 +1,9 @@
 dev:
 	rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
 
+dev-new:
+	rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
+
 dev-update:
 	rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 

+ 56 - 55
web/src/app.css

@@ -6,85 +6,86 @@
 @tailwind utilities;
 
 :root {
-	font-family: 'Work Sans', sans-serif;
-	/* --immich-icon-button-hover-color: #d3d3d3; */
+    font-family: 'Work Sans', sans-serif;
+    /* --immich-icon-button-hover-color: #d3d3d3; */
 }
 
 html {
-	height: 100%;
-	width: 100%;
+    height: 100%;
+    width: 100%;
 }
 
 html::-webkit-scrollbar {
-	width: 8px;
+    width: 8px;
 }
 
 /* Track */
 html::-webkit-scrollbar-track {
-	background: #f1f1f1;
-	border-radius: 16px;
+    background: #f1f1f1;
+    border-radius: 16px;
 }
 
 /* Handle */
 html::-webkit-scrollbar-thumb {
-	background: rgba(85, 86, 87, 0.408);
-	border-radius: 16px;
+    background: rgba(85, 86, 87, 0.408);
+    border-radius: 16px;
 }
 
 /* Handle on hover */
 html::-webkit-scrollbar-thumb:hover {
-	background: #4250afad;
-	border-radius: 16px;
+    background: #4250afad;
+    border-radius: 16px;
 }
 
 body {
-	/* min-height: 100vh; */
-	margin: 0;
-	background-color: #f6f8fe;
-	color: #5f6368;
+    /* min-height: 100vh; */
+    margin: 0;
+    background-color: #f6f8fe;
+    color: #5f6368;
 }
 
 input:focus-visible {
-	outline-offset: 0px !important;
-	outline: none !important;
+    outline-offset: 0px !important;
+    outline: none !important;
 }
+
 @layer utilities {
-	.immich-form-input {
-		@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm;
-	}
-
-	.immich-form-label {
-		@apply font-medium text-sm text-gray-500;
-	}
-
-	.immich-btn-primary {
-		@apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium;
-	}
-
-	.immich-text-button {
-		@apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
-	}
-
-	/* width */
-	.immich-scrollbar::-webkit-scrollbar {
-		width: 8px;
-	}
-
-	/* Track */
-	.immich-scrollbar::-webkit-scrollbar-track {
-		background: #f1f1f1;
-		border-radius: 16px;
-	}
-
-	/* Handle */
-	.immich-scrollbar::-webkit-scrollbar-thumb {
-		background: rgba(85, 86, 87, 0.408);
-		border-radius: 16px;
-	}
-
-	/* Handle on hover */
-	.immich-scrollbar::-webkit-scrollbar-thumb:hover {
-		background: #4250afad;
-		border-radius: 16px;
-	}
+    .immich-form-input {
+        @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ;
+    }
+
+    .immich-form-label {
+        @apply font-medium text-sm text-gray-500;
+    }
+
+    .immich-btn-primary {
+        @apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium;
+    }
+
+    .immich-text-button {
+        @apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
+    }
+
+    /* width */
+    .immich-scrollbar::-webkit-scrollbar {
+        width: 8px;
+    }
+
+    /* Track */
+    .immich-scrollbar::-webkit-scrollbar-track {
+        background: #f1f1f1;
+        border-radius: 16px;
+    }
+
+    /* Handle */
+    .immich-scrollbar::-webkit-scrollbar-thumb {
+        background: rgba(85, 86, 87, 0.408);
+        border-radius: 16px;
+    }
+
+    /* Handle on hover */
+    .immich-scrollbar::-webkit-scrollbar-thumb:hover {
+        background: #4250afad;
+        border-radius: 16px;
+    }
 }

+ 4 - 1
web/src/lib/components/admin-page/user-management.svelte

@@ -31,6 +31,9 @@
 				<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
 				<td class="text-sm px-4 w-1/4 text-ellipsis"
 					><button
+						on:click={() => {
+							dispatch('edit-user', { user });
+						}}
 						class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
 						><PencilOutline size="20" /></button
 					></td
@@ -40,4 +43,4 @@
 	</tbody>
 </table>
 
-<button on:click={() => dispatch('createUser')} class="immich-btn-primary">Create user</button>
+<button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>

+ 118 - 118
web/src/lib/components/forms/create-user-form.svelte

@@ -1,122 +1,122 @@
 <script lang="ts">
-	import { api } from '@api';
-	import { createEventDispatcher } from 'svelte';
-
-	let error: string;
-	let success: string;
-
-	let password: string = '';
-	let confirmPassowrd: string = '';
-
-	let canCreateUser = false;
-
-	$: {
-		if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
-			error = 'Password does not match';
-			canCreateUser = false;
-		} else {
-			error = '';
-			canCreateUser = true;
-		}
-	}
-	const dispatch = createEventDispatcher();
-
-	async function registerUser(event: SubmitEvent) {
-		console.log('registerUser');
-		if (canCreateUser) {
-			error = '';
-
-			const formElement = event.target as HTMLFormElement;
-
-			const form = new FormData(formElement);
-
-			const email = form.get('email');
-			const password = form.get('password');
-			const firstName = form.get('firstName');
-			const lastName = form.get('lastName');
-
-			const { status } = await api.userApi.createUser({
-				email: String(email),
-				password: String(password),
-				firstName: String(firstName),
-				lastName: String(lastName)
-			});
-
-			if (status === 201) {
-				success = 'New user created';
-
-				dispatch('user-created');
-				return;
-			} else {
-				error = 'Error create user account';
-			}
-		}
-	}
+  import { api } from '@api';
+  import { createEventDispatcher } from 'svelte';
+
+  let error: string;
+  let success: string;
+
+  let password = '';
+  let confirmPassowrd = '';
+
+  let canCreateUser = false;
+
+  $: {
+    if (password !== confirmPassowrd && confirmPassowrd.length > 0) {
+      error = 'Password does not match';
+      canCreateUser = false;
+    } else {
+      error = '';
+      canCreateUser = true;
+    }
+  }
+  const dispatch = createEventDispatcher();
+
+  async function registerUser(event: SubmitEvent) {
+    if (canCreateUser) {
+      error = '';
+
+      const formElement = event.target as HTMLFormElement;
+
+      const form = new FormData(formElement);
+
+      const email = form.get('email');
+      const password = form.get('password');
+      const firstName = form.get('firstName');
+      const lastName = form.get('lastName');
+
+      const {status} = await api.userApi.createUser({
+        email: String(email),
+        password: String(password),
+        firstName: String(firstName),
+        lastName: String(lastName)
+      });
+
+      if (status === 201) {
+        success = 'New user created';
+
+        dispatch('user-created');
+        return;
+      } else {
+        error = 'Error create user account';
+      }
+    }
+  }
 </script>
 
-<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
-	<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
-		<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
-		<h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
-		<p class="text-sm border rounded-md p-4 font-mono text-gray-600">
-			Please provide your user with the password, they will have to change it on their first sign
-			in.
-		</p>
-	</div>
-
-	<form on:submit|preventDefault={registerUser} autocomplete="off">
-		<div class="m-4 flex flex-col gap-2">
-			<label class="immich-form-label" for="email">Email</label>
-			<input class="immich-form-input" id="email" name="email" type="email" required />
-		</div>
-
-		<div class="m-4 flex flex-col gap-2">
-			<label class="immich-form-label" for="password">Password</label>
-			<input
-				class="immich-form-input"
-				id="password"
-				name="password"
-				type="password"
-				required
-				bind:value={password}
-			/>
-		</div>
-
-		<div class="m-4 flex flex-col gap-2">
-			<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
-			<input
-				class="immich-form-input"
-				id="confirmPassword"
-				name="password"
-				type="password"
-				required
-				bind:value={confirmPassowrd}
-			/>
-		</div>
-
-		<div class="m-4 flex flex-col gap-2">
-			<label class="immich-form-label" for="firstName">First Name</label>
-			<input class="immich-form-input" id="firstName" name="firstName" type="text" required />
-		</div>
-
-		<div class="m-4 flex flex-col gap-2">
-			<label class="immich-form-label" for="lastName">Last Name</label>
-			<input class="immich-form-input" id="lastName" name="lastName" type="text" required />
-		</div>
-
-		{#if error}
-			<p class="text-red-400 ml-4 text-sm">{error}</p>
-		{/if}
-
-		{#if success}
-			<p class="text-immich-primary ml-4 text-sm">{success}</p>
-		{/if}
-		<div class="flex w-full">
-			<button
-				type="submit"
-				class="m-4 p-2 bg-immich-primary hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full"
-				>Create</button
-			>
-		</div>
-	</form>
+<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
+    <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
+        <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>
+        <h1 class="text-2xl text-immich-primary font-medium">Create new user</h1>
+        <p class="text-sm border rounded-md p-4 font-mono text-gray-600">
+            Please provide your user with the password, they will have to change it on their first sign
+            in.
+        </p>
+    </div>
+
+    <form on:submit|preventDefault={registerUser} autocomplete="off">
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="email">Email</label>
+            <input class="immich-form-input" id="email" name="email" type="email" required/>
+        </div>
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="password">Password</label>
+            <input
+                    class="immich-form-input"
+                    id="password"
+                    name="password"
+                    type="password"
+                    required
+                    bind:value={password}
+            />
+        </div>
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="confirmPassword">Confirm Password</label>
+            <input
+                    class="immich-form-input"
+                    id="confirmPassword"
+                    name="password"
+                    type="password"
+                    required
+                    bind:value={confirmPassowrd}
+            />
+        </div>
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="firstName">First Name</label>
+            <input class="immich-form-input" id="firstName" name="firstName" type="text" required/>
+        </div>
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="lastName">Last Name</label>
+            <input class="immich-form-input" id="lastName" name="lastName" type="text" required/>
+        </div>
+
+        {#if error}
+            <p class="text-red-400 ml-4 text-sm">{error}</p>
+        {/if}
+
+        {#if success}
+            <p class="text-immich-primary ml-4 text-sm">{success}</p>
+        {/if}
+        <div class="flex w-full">
+            <button
+                    type="submit"
+                    class="m-4 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
+            >Create
+            </button
+            >
+        </div>
+    </form>
 </div>

+ 103 - 0
web/src/lib/components/forms/edit-user-form.svelte

@@ -0,0 +1,103 @@
+<script lang="ts">
+  import { api, UserResponseDto } from '@api';
+  import { createEventDispatcher } from 'svelte';
+  import AccountEditOutline from 'svelte-material-icons/AccountEditOutline.svelte';
+
+  export let user: UserResponseDto;
+
+  let error: string;
+  let success: string;
+
+  const dispatch = createEventDispatcher();
+
+  // eslint-disable-next-line no-undef
+  const editUser = async (event: SubmitEvent) => {
+
+    const formElement = event.target as HTMLFormElement;
+    const form = new FormData(formElement);
+
+    const firstName = form.get('firstName');
+    const lastName = form.get('lastName');
+
+
+    const {status} = await api.userApi.updateUser({
+      id: user.id,
+      firstName: firstName.toString(),
+      lastName: lastName.toString()
+    }).catch(e => console.log("Error updating user ", e));
+
+    if (status == 200) {
+      dispatch('edit-success');
+    }
+  }
+
+  const resetPassword = async () => {
+    const defaultPassword = 'password'
+
+    const {status} = await api.userApi.updateUser({
+      id: user.id,
+      password: defaultPassword,
+      shouldChangePassword: true,
+
+    }).catch(e => console.log("Error updating user ", e));
+
+    if (status == 200) {
+      dispatch('reset-password-success');
+    }
+  }
+</script>
+
+<div class="border bg-white p-4 shadow-sm w-[500px] rounded-3xl py-8">
+    <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
+        <!--        <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo"/>-->
+        <AccountEditOutline size="4em" color="#4250affe"/>
+        <h1 class="text-2xl text-immich-primary font-medium">Edit user</h1>
+    </div>
+
+    <form on:submit|preventDefault={editUser} autocomplete="off">
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="email">Email
+                (cannot change)</label>
+            <input class="immich-form-input disabled:bg-gray-200 hover:cursor-not-allowed"
+                   id="email" name="email"
+                   type="email" disabled
+                   bind:value={user.email}/>
+        </div>
+
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="firstName">First Name</label>
+            <input class="immich-form-input" id="firstName" name="firstName" type="text" required
+                   bind:value={user.firstName}/>
+        </div>
+
+        <div class="m-4 flex flex-col gap-2">
+            <label class="immich-form-label" for="lastName">Last Name</label>
+            <input class="immich-form-input" id="lastName" name="lastName" type="text" required
+                   bind:value={user.lastName}/>
+        </div>
+
+
+        {#if error}
+            <p class="text-red-400 ml-4 text-sm">{error}</p>
+        {/if}
+
+        {#if success}
+            <p class="text-immich-primary ml-4 text-sm">{success}</p>
+        {/if}
+        <div class="flex w-full px-4 gap-4 mt-8">
+            <button on:click={resetPassword}
+                    class="flex-1 transition-colors bg-[#F9DEDC] hover:bg-red-50 text-[#410E0B] px-6 py-3 rounded-full w-full font-medium"
+
+            >Reset password
+            </button
+            >
+            <button
+                    type="submit"
+                    class="flex-1 transition-colors bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
+            >Confirm
+            </button
+            >
+        </div>
+    </form>
+</div>

+ 166 - 99
web/src/routes/admin/index.svelte

@@ -1,114 +1,181 @@
 <script context="module" lang="ts">
-	import type { Load } from '@sveltejs/kit';
-	import { api, UserResponseDto } from '@api';
-
-	export const load: Load = async ({ fetch, session }) => {
-		if (!browser && !session.user) {
-			return {
-				status: 302,
-				redirect: '/auth/login'
-			};
-		}
-
-		try {
-			const [user, allUsers] = await Promise.all([
-				fetch('/data/user/get-my-user-info').then((r) => r.json()),
-				fetch('/data/user/get-all-users?isAll=false').then((r) => r.json())
-			]);
-
-			return {
-				status: 200,
-				props: {
-					user: user,
-					allUsers: allUsers
-				}
-			};
-		} catch (e) {
-			return {
-				status: 302,
-				redirect: '/auth/login'
-			};
-		}
-	};
+  import type { Load } from '@sveltejs/kit';
+  import { api, UserResponseDto } from '@api';
+  import { browser } from '$app/env';
+
+  export const load: Load = async ({fetch, session}) => {
+    if (!browser && !session.user) {
+      return {
+        status: 302,
+        redirect: '/auth/login'
+      };
+    }
+
+    try {
+
+
+      const user: UserResponseDto = await fetch('/data/user/get-my-user-info').then((r) => r.json());
+      const allUsers: UserResponseDto[] = await fetch<UserResponseDto[]>('/data/user/get-all-users?isAll=false').then((r) => r.json());
+
+      if (!user.isAdmin) {
+        return {
+          status: 302,
+          redirect: '/photos'
+        };
+      }
+
+      return {
+        status: 200,
+        props: {
+          user: user,
+          allUsers: allUsers
+        }
+      };
+    } catch (e) {
+      return {
+        status: 302,
+        redirect: '/auth/login'
+      };
+    }
+  };
 </script>
 
 <script lang="ts">
-	import { onMount } from 'svelte';
-
-	import type { ImmichUser } from '$lib/models/immich-user';
-	import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
-	import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
-	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
-	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
-	import UserManagement from '$lib/components/admin-page/user-management.svelte';
-	import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-	import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
-	import StatusBox from '$lib/components/shared-components/status-box.svelte';
-	import { browser } from '$app/env';
-
-	let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
-
-	export let user: ImmichUser;
-	export let allUsers: UserResponseDto[];
-
-	let shouldShowCreateUserForm: boolean;
-
-	const onButtonClicked = (buttonType: CustomEvent) => {
-		selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
-	};
-
-	onMount(() => {
-		selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
-	});
-
-	const onUserCreated = async () => {
-		const { data } = await api.userApi.getAllUsers(false);
-		allUsers = data;
-
-		shouldShowCreateUserForm = false;
-	};
+  import { onMount } from 'svelte';
+
+  import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection';
+  import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte';
+  import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
+  import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
+  import UserManagement from '$lib/components/admin-page/user-management.svelte';
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
+  import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
+  import StatusBox from '$lib/components/shared-components/status-box.svelte';
+
+
+  let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT;
+
+  export let user: UserResponseDto;
+  export let allUsers: UserResponseDto[];
+
+  let editUser: UserResponseDto;
+
+  let shouldShowEditUserForm = false;
+  let shouldShowCreateUserForm = false;
+  let shouldShowInfoPanel = false;
+
+  const onButtonClicked = (buttonType: CustomEvent) => {
+    selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection;
+  };
+
+  onMount(() => {
+    selectedAction = AdminSideBarSelection.USER_MANAGEMENT;
+  });
+
+  const onUserCreated = async () => {
+    const {data} = await api.userApi.getAllUsers(false);
+    allUsers = data;
+    shouldShowCreateUserForm = false;
+  };
+
+  const editUserHandler = async (event: CustomEvent) => {
+    const {user} = event.detail;
+    editUser = user;
+    shouldShowEditUserForm = true;
+  };
+
+  const onEditUserSuccess = async () => {
+    const {data} = await api.userApi.getAllUsers(false);
+    allUsers = data;
+    shouldShowEditUserForm = false;
+  }
+
+  const onEditPasswordSuccess = async () => {
+    const {data} = await api.userApi.getAllUsers(false);
+    allUsers = data;
+    shouldShowEditUserForm = false;
+    shouldShowInfoPanel = true;
+  }
 </script>
 
 <svelte:head>
-	<title>Administration - Immich</title>
+    <title>Administration - Immich</title>
 </svelte:head>
 
-<NavigationBar {user} />
+<NavigationBar {user}/>
 
 {#if shouldShowCreateUserForm}
-	<FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
-		<div>
-			<CreateUserForm on:user-created={onUserCreated} />
-		</div>
-	</FullScreenModal>
+    <FullScreenModal on:clickOutside={() => (shouldShowCreateUserForm = false)}>
+        <CreateUserForm on:user-created={onUserCreated}/>
+    </FullScreenModal>
+{/if}
+
+{#if shouldShowEditUserForm}
+    <FullScreenModal on:clickOutside={() => (shouldShowEditUserForm = false)}>
+        <EditUserForm user={editUser} on:edit-success={onEditUserSuccess}
+                      on:reset-password-success={onEditPasswordSuccess}/>
+    </FullScreenModal>
 {/if}
 
+{#if shouldShowInfoPanel}
+    <FullScreenModal on:clickOutside={() => (shouldShowInfoPanel = false)}>
+
+        <div class="border bg-white shadow-sm w-[500px] rounded-3xl p-8 text-sm">
+            <h1 class="font-bold text-immich-primary text-lg mb-4">Password reset success</h1>
+
+            <p>
+                The user's password has been reset to the default <code
+                    class="font-bold bg-gray-200 px-2 py-1 rounded-md text-immich-primary">password</code>
+                <br>
+                Please inform the user, and they will need to change the password at the next log-on.
+            </p>
+
+            <div class="flex w-full">
+                <button
+                        on:click={() => shouldShowInfoPanel = false}
+                        class="mt-6 bg-immich-primary hover:bg-immich-primary/75 px-6 py-3 text-white rounded-full shadow-md w-full font-medium"
+                >Done
+                </button
+                >
+            </div>
+        </div>
+    </FullScreenModal>
+{/if}
+
+
 <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
-	<section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
-		<SideBarButton
-			title="User"
-			logo={AccountMultipleOutline}
-			actionType={AdminSideBarSelection.USER_MANAGEMENT}
-			isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
-			on:selected={onButtonClicked}
-		/>
-
-		<div class="mb-6 mt-auto">
-			<StatusBox />
-		</div>
-	</section>
-	<section class="overflow-y-auto relative">
-		<div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg">
-			<h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1>
-			<hr />
-		</div>
-
-		<section id="setting-content" class="relative pt-[85px] flex place-content-center">
-			<section class="w-[800px] pt-4">
-				{#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
-					<UserManagement {allUsers} on:createUser={() => (shouldShowCreateUserForm = true)} />
-				{/if}
-			</section>
-		</section>
-	</section>
+    <section id="admin-sidebar" class="pt-8 pr-6 flex flex-col">
+        <SideBarButton
+                title="User"
+                logo={AccountMultipleOutline}
+                actionType={AdminSideBarSelection.USER_MANAGEMENT}
+                isSelected={selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
+                on:selected={onButtonClicked}
+        />
+
+        <div class="mb-6 mt-auto">
+            <StatusBox/>
+        </div>
+    </section>
+    <section class="overflow-y-auto relative">
+        <div id="setting-title" class="pt-10 fixed w-full z-50 bg-immich-bg">
+            <h1 class="text-lg ml-8 mb-4 text-immich-primary font-medium">{selectedAction}</h1>
+            <hr/>
+        </div>
+
+
+        <section id="setting-content" class="relative pt-[85px] flex place-content-center">
+            <section class="w-[800px] pt-4">
+                {#if selectedAction === AdminSideBarSelection.USER_MANAGEMENT}
+                    <UserManagement
+                            {allUsers}
+                            on:create-user={() => (shouldShowCreateUserForm = true)}
+                            on:edit-user={editUserHandler}
+                    />
+                {/if}
+            </section>
+        </section>
+    </section>
+
 </section>

+ 45 - 43
web/src/routes/index.svelte

@@ -1,56 +1,58 @@
 <script context="module" lang="ts">
-	export const prerender = false;
-	import type { Load } from '@sveltejs/kit';
-	import { api } from '@api';
-
-	export const load: Load = async () => {
-		if (browser) {
-			try {
-				const { data: user } = await api.userApi.getMyUserInfo();
-
-				return {
-					status: 302,
-					redirect: '/photos'
-				};
-			} catch (e) {}
-
-			const { data } = await api.userApi.getUserCount();
-
-			return {
-				status: 200,
-				props: {
-					isAdminUserExist: data.userCount == 0 ? false : true
-				}
-			};
-		}
-	};
+  export const prerender = false;
+  import type { Load } from '@sveltejs/kit';
+  import { api } from '@api';
+  import { browser } from '$app/env';
+
+  export const load: Load = async () => {
+    if (browser) {
+      try {
+        const {data: user} = await api.userApi.getMyUserInfo();
+
+        return {
+          status: 302,
+          redirect: '/photos'
+        };
+      } catch (e) {
+      }
+
+      const {data} = await api.userApi.getUserCount();
+
+      return {
+        status: 200,
+        props: {
+          isAdminUserExist: data.userCount != 0
+        }
+      };
+    }
+  };
 </script>
 
 <script lang="ts">
-	import { goto } from '$app/navigation';
-	import { browser } from '$app/env';
+  import { goto } from '$app/navigation';
 
-	export let isAdminUserExist: boolean;
+  export let isAdminUserExist: boolean;
 
-	async function onGettingStartedClicked() {
-		isAdminUserExist ? goto('/auth/login') : goto('/auth/register');
-	}
+  async function onGettingStartedClicked() {
+    isAdminUserExist ? await goto('/auth/login') : await goto('/auth/register');
+  }
 </script>
 
 <svelte:head>
-	<title>Welcome 🎉 - Immich</title>
-	<meta name="description" content="Immich Web Interface" />
+    <title>Welcome 🎉 - Immich</title>
+    <meta name="description" content="Immich Web Interface"/>
 </svelte:head>
 
 <section class="h-screen w-screen flex place-items-center place-content-center">
-	<div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
-		<div class="flex place-items-center place-content-center ">
-			<img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo" />
-		</div>
-		<h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
-		<button
-			class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
-			on:click={onGettingStartedClicked}>Getting Started</button
-		>
-	</div>
+    <div class="flex flex-col place-items-center gap-8 text-center max-w-[350px]">
+        <div class="flex place-items-center place-content-center ">
+            <img class="text-center" src="immich-logo.svg" height="200" width="200" alt="immich-logo"/>
+        </div>
+        <h1 class="text-4xl text-immich-primary font-bold font-immich-title">Welcome to IMMICH Web</h1>
+        <button
+                class="border px-4 py-2 rounded-md bg-immich-primary hover:bg-immich-primary/75 text-white font-bold w-[200px]"
+                on:click={onGettingStartedClicked}>Getting Started
+        </button
+        >
+    </div>
 </section>

+ 104 - 103
web/src/routes/sharing/index.svelte

@@ -1,119 +1,120 @@
 <script context="module" lang="ts">
-	export const prerender = false;
-
-	import type { Load } from '@sveltejs/kit';
-	import { AlbumResponseDto, api, UserResponseDto } from '@api';
-
-	export const load: Load = async ({ fetch, session }) => {
-		if (!browser && !session.user) {
-			return {
-				status: 302,
-				redirect: '/auth/login'
-			};
-		}
-
-		try {
-			const [user, sharedAlbums] = await Promise.all([
-				fetch('/data/user/get-my-user-info').then((r) => r.json()),
-				fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
-			]);
-
-			return {
-				status: 200,
-				props: {
-					user: user,
-					sharedAlbums: sharedAlbums
-				}
-			};
-		} catch (e) {
-			return {
-				status: 302,
-				redirect: '/auth/login'
-			};
-		}
-	};
+  export const prerender = false;
+
+  import type { Load } from '@sveltejs/kit';
+  import { AlbumResponseDto, api, UserResponseDto } from '@api';
+  import { browser } from '$app/env';
+
+  export const load: Load = async ({fetch, session}) => {
+    if (!browser && !session.user) {
+      return {
+        status: 302,
+        redirect: '/auth/login'
+      };
+    }
+
+    try {
+      const [user, sharedAlbums] = await Promise.all([
+        fetch('/data/user/get-my-user-info').then((r) => r.json()),
+        fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
+      ]);
+
+      return {
+        status: 200,
+        props: {
+          user: user,
+          sharedAlbums: sharedAlbums
+        }
+      };
+    } catch (e) {
+      return {
+        status: 302,
+        redirect: '/auth/login'
+      };
+    }
+  };
 </script>
 
 <script lang="ts">
-	import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
-	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
-	import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
-	import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
-	import { goto } from '$app/navigation';
-	import { browser } from '$app/env';
-
-	export let user: UserResponseDto;
-	export let sharedAlbums: AlbumResponseDto[];
-
-	const createSharedAlbum = async () => {
-		try {
-			const { data: newAlbum } = await api.albumApi.createAlbum({
-				albumName: 'Untitled'
-			});
-
-			goto('/albums/' + newAlbum.id);
-		} catch (e) {
-			console.log('Error [createAlbum] ', e);
-		}
-	};
+  import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
+  import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
+  import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
+  import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
+  import { goto } from '$app/navigation';
+
+  export let user: UserResponseDto;
+  export let sharedAlbums: AlbumResponseDto[];
+
+  const createSharedAlbum = async () => {
+    try {
+      const {data: newAlbum} = await api.albumApi.createAlbum({
+        albumName: 'Untitled'
+      });
+
+      goto('/albums/' + newAlbum.id);
+    } catch (e) {
+      console.log('Error [createAlbum] ', e);
+    }
+  };
 </script>
 
 <svelte:head>
-	<title>Albums - Immich</title>
+    <title>Albums - Immich</title>
 </svelte:head>
 
 <section>
-	<NavigationBar {user} on:uploadClicked={() => {}} />
+    <NavigationBar {user} on:uploadClicked={() => {}}/>
 </section>
 
 <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
-	<SideBar />
-
-	<section class="overflow-y-auto relative">
-		<section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
-			<!-- Main Section -->
-			<div class="px-4 flex justify-between place-items-center">
-				<div>
-					<p class="font-medium">Sharing</p>
-				</div>
-
-				<div>
-					<button
-						on:click={createSharedAlbum}
-						class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
-					>
+    <SideBar/>
+
+    <section class="overflow-y-auto relative">
+        <section id="album-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
+            <!-- Main Section -->
+            <div class="px-4 flex justify-between place-items-center">
+                <div>
+                    <p class="font-medium">Sharing</p>
+                </div>
+
+                <div>
+                    <button
+                            on:click={createSharedAlbum}
+                            class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700"
+                    >
 						<span>
-							<PlusBoxOutline size="18" />
+							<PlusBoxOutline size="18"/>
 						</span>
-						<p>Create shared album</p>
-					</button>
-				</div>
-			</div>
-
-			<div class="my-4">
-				<hr />
-			</div>
-
-			<!-- Share Album List -->
-			<div class="w-full flex flex-col place-items-center">
-				{#each sharedAlbums as album}
-					<a sveltekit:prefetch href={`albums/${album.id}`}>
-						<SharedAlbumListTile {album} {user} /></a
-					>
-				{/each}
-			</div>
-
-			<!-- Empty List -->
-			{#if sharedAlbums.length === 0}
-				<div
-					class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
-				>
-					<img src="/empty-2.svg" alt="Empty shared album" width="500" />
-					<p class="text-center text-immich-text-gray-500">
-						Create a shared album to share photos and videos with people in your network
-					</p>
-				</div>
-			{/if}
-		</section>
-	</section>
+                        <p>Create shared album</p>
+                    </button>
+                </div>
+            </div>
+
+            <div class="my-4">
+                <hr/>
+            </div>
+
+            <!-- Share Album List -->
+            <div class="w-full flex flex-col place-items-center">
+                {#each sharedAlbums as album}
+                    <a sveltekit:prefetch href={`albums/${album.id}`}>
+                        <SharedAlbumListTile {album} {user}/>
+                    </a
+                    >
+                {/each}
+            </div>
+
+            <!-- Empty List -->
+            {#if sharedAlbums.length === 0}
+                <div
+                        class="border p-5 w-[50%] m-auto mt-10 bg-gray-50 rounded-3xl flex flex-col place-content-center place-items-center"
+                >
+                    <img src="/empty-2.svg" alt="Empty shared album" width="500"/>
+                    <p class="text-center text-immich-text-gray-500">
+                        Create a shared album to share photos and videos with people in your network
+                    </p>
+                </div>
+            {/if}
+        </section>
+    </section>
 </section>