Selaa lähdekoodia

Merge pull request #315 from ollama-webui/account-settings

feat: account settings
Timothy Jaeryang Baek 1 vuosi sitten
vanhempi
commit
87f5e72eeb

+ 15 - 1
backend/apps/web/models/auths.py

@@ -64,6 +64,11 @@ class SigninForm(BaseModel):
     password: str
 
 
+class UpdatePasswordForm(BaseModel):
+    password: str
+    new_password: str
+
+
 class SignupForm(BaseModel):
     name: str
     email: str
@@ -109,7 +114,16 @@ class AuthsTable:
         except:
             return None
 
-    def delete_auth_by_id(self, id: str) -> Optional[UserModel]:
+    def update_user_password_by_id(self, id: str, new_password: str) -> bool:
+        try:
+            query = Auth.update(password=new_password).where(Auth.id == id)
+            result = query.execute()
+
+            return True if result == 1 else False
+        except:
+            return False
+
+    def delete_auth_by_id(self, id: str) -> bool:
         try:
             # Delete User
             result = Users.delete_user_by_id(id)

+ 23 - 0
backend/apps/web/routers/auths.py

@@ -11,6 +11,7 @@ import uuid
 from apps.web.models.auths import (
     SigninForm,
     SignupForm,
+    UpdatePasswordForm,
     UserResponse,
     SigninResponse,
     Auths,
@@ -53,6 +54,28 @@ async def get_session_user(cred=Depends(bearer_scheme)):
         )
 
 
+############################
+# Update Password
+############################
+
+
+@router.post("/update/password", response_model=bool)
+async def update_password(form_data: UpdatePasswordForm, cred=Depends(bearer_scheme)):
+    token = cred.credentials
+    session_user = Users.get_user_by_token(token)
+
+    if session_user:
+        user = Auths.authenticate_user(session_user.email, form_data.password)
+
+        if user:
+            hashed = get_password_hash(form_data.new_password)
+            return Auths.update_user_password_by_id(user.id, hashed)
+        else:
+            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD)
+    else:
+        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
+
+
 ############################
 # SignIn
 ############################

+ 3 - 0
backend/constants.py

@@ -21,6 +21,9 @@ class ERROR_MESSAGES(str, Enum):
         "Your session has expired or the token is invalid. Please sign in again."
     )
     INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
+    INVALID_PASSWORD = (
+        "The password provided is incorrect. Please check for typos and try again."
+    )
     UNAUTHORIZED = "401 Unauthorized"
     ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
     ACTION_PROHIBITED = (

+ 31 - 0
src/lib/apis/auths/index.ts

@@ -88,3 +88,34 @@ export const userSignUp = async (name: string, email: string, password: string)
 
 	return res;
 };
+
+export const updateUserPassword = async (token: string, password: string, newPassword: string) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/password`, {
+		method: 'POST',
+		headers: {
+			'Content-Type': 'application/json',
+			...(token && { authorization: `Bearer ${token}` })
+		},
+		body: JSON.stringify({
+			password: password,
+			new_password: newPassword
+		})
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.catch((err) => {
+			console.log(err);
+			error = err.detail;
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+};

+ 118 - 0
src/lib/components/chat/SettingsModal.svelte

@@ -18,6 +18,7 @@
 
 	import Advanced from './Settings/Advanced.svelte';
 	import Modal from '../common/Modal.svelte';
+	import { updateUserPassword } from '$lib/apis/auths';
 
 	export let show = false;
 
@@ -118,6 +119,11 @@
 	let authType = 'Basic';
 	let authContent = '';
 
+	// Account
+	let currentPassword = '';
+	let newPassword = '';
+	let newPasswordConfirm = '';
+
 	// About
 	let ollamaVersion = '';
 
@@ -595,6 +601,31 @@
 		return models;
 	};
 
+	const updatePasswordHandler = async () => {
+		if (newPassword === newPasswordConfirm) {
+			const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch(
+				(error) => {
+					toast.error(error);
+					return null;
+				}
+			);
+
+			if (res) {
+				toast.success('Successfully updated.');
+			}
+
+			currentPassword = '';
+			newPassword = '';
+			newPasswordConfirm = '';
+		} else {
+			toast.error(
+				`The passwords you entered don't quite match. Please double-check and try again.`
+			);
+			newPassword = '';
+			newPasswordConfirm = '';
+		}
+	};
+
 	onMount(async () => {
 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
 		console.log(settings);
@@ -845,6 +876,32 @@
 					</button>
 				{/if}
 
+				<button
+					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
+					'account'
+						? 'bg-gray-200 dark:bg-gray-700'
+						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
+					on:click={() => {
+						selectedTab = 'account';
+					}}
+				>
+					<div class=" self-center mr-2">
+						<svg
+							xmlns="http://www.w3.org/2000/svg"
+							viewBox="0 0 16 16"
+							fill="currentColor"
+							class="w-4 h-4"
+						>
+							<path
+								fill-rule="evenodd"
+								d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
+								clip-rule="evenodd"
+							/>
+						</svg>
+					</div>
+					<div class=" self-center">Account</div>
+				</button>
+
 				<button
 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
 					'about'
@@ -1817,6 +1874,67 @@
 							</button>
 						</div>
 					</form>
+				{:else if selectedTab === 'account'}
+					<form
+						class="flex flex-col h-full text-sm"
+						on:submit|preventDefault={() => {
+							updatePasswordHandler();
+						}}
+					>
+						<div class=" mb-2.5 font-medium">Change Password</div>
+
+						<div class=" space-y-1.5">
+							<div class="flex flex-col w-full">
+								<div class=" mb-1 text-xs text-gray-500">Current Password</div>
+
+								<div class="flex-1">
+									<input
+										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+										type="password"
+										bind:value={currentPassword}
+										autocomplete="current-password"
+										required
+									/>
+								</div>
+							</div>
+
+							<div class="flex flex-col w-full">
+								<div class=" mb-1 text-xs text-gray-500">New Password</div>
+
+								<div class="flex-1">
+									<input
+										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+										type="password"
+										bind:value={newPassword}
+										autocomplete="new-password"
+										required
+									/>
+								</div>
+							</div>
+
+							<div class="flex flex-col w-full">
+								<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
+
+								<div class="flex-1">
+									<input
+										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
+										type="password"
+										bind:value={newPasswordConfirm}
+										autocomplete="off"
+										required
+									/>
+								</div>
+							</div>
+						</div>
+
+						<div class="mt-3 flex justify-end">
+							<button
+								class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
+							>
+								Update password
+							</button>
+						</div>
+					</form>
 				{:else if selectedTab === 'about'}
 					<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
 						<div class=" space-y-3">