You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	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
This commit is contained in:
		
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								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 | ||||
|  | ||||
|   | ||||
| @@ -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-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-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-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; | ||||
| 	} | ||||
|     .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; | ||||
| 	} | ||||
|     /* width */ | ||||
|     .immich-scrollbar::-webkit-scrollbar { | ||||
|         width: 8px; | ||||
|     } | ||||
|  | ||||
| 	/* Track */ | ||||
| 	.immich-scrollbar::-webkit-scrollbar-track { | ||||
| 		background: #f1f1f1; | ||||
| 		border-radius: 16px; | ||||
| 	} | ||||
|     /* 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 */ | ||||
|     .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; | ||||
| 	} | ||||
|     /* Handle on hover */ | ||||
|     .immich-scrollbar::-webkit-scrollbar-thumb:hover { | ||||
|         background: #4250afad; | ||||
|         border-radius: 16px; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -1,122 +1,122 @@ | ||||
| <script lang="ts"> | ||||
| 	import { api } from '@api'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
|   import { api } from '@api'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|  | ||||
| 	let error: string; | ||||
| 	let success: string; | ||||
|   let error: string; | ||||
|   let success: string; | ||||
|  | ||||
| 	let password: string = ''; | ||||
| 	let confirmPassowrd: string = ''; | ||||
|   let password = ''; | ||||
|   let confirmPassowrd = ''; | ||||
|  | ||||
| 	let canCreateUser = false; | ||||
|   let canCreateUser = false; | ||||
|  | ||||
| 	$: { | ||||
| 		if (password !== confirmPassowrd && confirmPassowrd.length > 0) { | ||||
| 			error = 'Password does not match'; | ||||
| 			canCreateUser = false; | ||||
| 		} else { | ||||
| 			error = ''; | ||||
| 			canCreateUser = true; | ||||
| 		} | ||||
| 	} | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|   $: { | ||||
|     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 = ''; | ||||
|   async function registerUser(event: SubmitEvent) { | ||||
|     if (canCreateUser) { | ||||
|       error = ''; | ||||
|  | ||||
| 			const formElement = event.target as HTMLFormElement; | ||||
|       const formElement = event.target as HTMLFormElement; | ||||
|  | ||||
| 			const form = new FormData(formElement); | ||||
|       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 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) | ||||
| 			}); | ||||
|       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'; | ||||
|       if (status === 201) { | ||||
|         success = 'New user created'; | ||||
|  | ||||
| 				dispatch('user-created'); | ||||
| 				return; | ||||
| 			} else { | ||||
| 				error = 'Error create user account'; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|         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> | ||||
| <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> | ||||
|     <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="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="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="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> | ||||
|         <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 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> | ||||
|         {#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
									
								
								web/src/lib/components/forms/edit-user-form.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								web/src/lib/components/forms/edit-user-form.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -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> | ||||
| @@ -1,114 +1,181 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import { api, UserResponseDto } from '@api'; | ||||
|   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' | ||||
| 			}; | ||||
| 		} | ||||
|   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()) | ||||
| 			]); | ||||
|     try { | ||||
|  | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				props: { | ||||
| 					user: user, | ||||
| 					allUsers: allUsers | ||||
| 				} | ||||
| 			}; | ||||
| 		} catch (e) { | ||||
| 			return { | ||||
| 				status: 302, | ||||
| 				redirect: '/auth/login' | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
|       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 { 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'; | ||||
|   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: ImmichUser; | ||||
| 	export let allUsers: UserResponseDto[]; | ||||
|   let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; | ||||
|  | ||||
| 	let shouldShowCreateUserForm: boolean; | ||||
|   export let user: UserResponseDto; | ||||
|   export let allUsers: UserResponseDto[]; | ||||
|  | ||||
| 	const onButtonClicked = (buttonType: CustomEvent) => { | ||||
| 		selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection; | ||||
| 	}; | ||||
|   let editUser: UserResponseDto; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		selectedAction = AdminSideBarSelection.USER_MANAGEMENT; | ||||
| 	}); | ||||
|   let shouldShowEditUserForm = false; | ||||
|   let shouldShowCreateUserForm = false; | ||||
|   let shouldShowInfoPanel = false; | ||||
|  | ||||
| 	const onUserCreated = async () => { | ||||
| 		const { data } = await api.userApi.getAllUsers(false); | ||||
| 		allUsers = data; | ||||
|   const onButtonClicked = (buttonType: CustomEvent) => { | ||||
|     selectedAction = buttonType.detail['actionType'] as AdminSideBarSelection; | ||||
|   }; | ||||
|  | ||||
| 		shouldShowCreateUserForm = false; | ||||
| 	}; | ||||
|   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} | ||||
| 		/> | ||||
|     <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> | ||||
|         <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 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> | ||||
|   | ||||
| @@ -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 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(); | ||||
|   export const load: Load = async () => { | ||||
|     if (browser) { | ||||
|       try { | ||||
|         const {data: user} = await api.userApi.getMyUserInfo(); | ||||
|  | ||||
| 				return { | ||||
| 					status: 302, | ||||
| 					redirect: '/photos' | ||||
| 				}; | ||||
| 			} catch (e) {} | ||||
|         return { | ||||
|           status: 302, | ||||
|           redirect: '/photos' | ||||
|         }; | ||||
|       } catch (e) { | ||||
|       } | ||||
|  | ||||
| 			const { data } = await api.userApi.getUserCount(); | ||||
|       const {data} = await api.userApi.getUserCount(); | ||||
|  | ||||
| 			return { | ||||
| 				status: 200, | ||||
| 				props: { | ||||
| 					isAdminUserExist: data.userCount == 0 ? false : true | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
|       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> | ||||
|   | ||||
| @@ -1,119 +1,120 @@ | ||||
| <script context="module" lang="ts"> | ||||
| 	export const prerender = false; | ||||
|   export const prerender = false; | ||||
|  | ||||
| 	import type { Load } from '@sveltejs/kit'; | ||||
| 	import { AlbumResponseDto, api, UserResponseDto } from '@api'; | ||||
|   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' | ||||
| 			}; | ||||
| 		} | ||||
|   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()) | ||||
| 			]); | ||||
|     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' | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
|       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'; | ||||
|   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[]; | ||||
|   export let user: UserResponseDto; | ||||
|   export let sharedAlbums: AlbumResponseDto[]; | ||||
|  | ||||
| 	const createSharedAlbum = async () => { | ||||
| 		try { | ||||
| 			const { data: newAlbum } = await api.albumApi.createAlbum({ | ||||
| 				albumName: 'Untitled' | ||||
| 			}); | ||||
|   const createSharedAlbum = async () => { | ||||
|     try { | ||||
|       const {data: newAlbum} = await api.albumApi.createAlbum({ | ||||
|         albumName: 'Untitled' | ||||
|       }); | ||||
|  | ||||
| 			goto('/albums/' + newAlbum.id); | ||||
| 		} catch (e) { | ||||
| 			console.log('Error [createAlbum] ', e); | ||||
| 		} | ||||
| 	}; | ||||
|       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 /> | ||||
|     <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> | ||||
|     <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" | ||||
| 					> | ||||
|                 <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> | ||||
|                         <p>Create shared album</p> | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
| 			<div class="my-4"> | ||||
| 				<hr /> | ||||
| 			</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> | ||||
|             <!-- 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> | ||||
|             <!-- 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> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user