Răsfoiți Sursa

refac: account details

Timothy Jaeryang Baek 1 lună în urmă
părinte
comite
86011e40be

+ 32 - 0
backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py

@@ -0,0 +1,32 @@
+"""update user table
+
+Revision ID: 3af16a1c9fb6
+Revises: 018012973d35
+Create Date: 2025-08-21 02:07:18.078283
+
+"""
+
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision: str = "3af16a1c9fb6"
+down_revision: Union[str, None] = "018012973d35"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    op.add_column("user", sa.Column("username", sa.String(length=50), nullable=True))
+    op.add_column("user", sa.Column("bio", sa.Text(), nullable=True))
+    op.add_column("user", sa.Column("gender", sa.Text(), nullable=True))
+    op.add_column("user", sa.Column("date_of_birth", sa.Date(), nullable=True))
+
+
+def downgrade() -> None:
+    op.drop_column("user", "username")
+    op.drop_column("user", "bio")
+    op.drop_column("user", "gender")
+    op.drop_column("user", "date_of_birth")

+ 0 - 5
backend/open_webui/models/auths.py

@@ -73,11 +73,6 @@ class ProfileImageUrlForm(BaseModel):
     profile_image_url: str
 
 
-class UpdateProfileForm(BaseModel):
-    profile_image_url: str
-    name: str
-
-
 class UpdatePasswordForm(BaseModel):
     password: str
     new_password: str

+ 37 - 12
backend/open_webui/models/users.py

@@ -11,9 +11,10 @@ from open_webui.utils.misc import throttle
 
 
 from pydantic import BaseModel, ConfigDict
-from sqlalchemy import BigInteger, Column, String, Text
+from sqlalchemy import BigInteger, Column, String, Text, Date
 from sqlalchemy import or_
 
+import datetime
 
 ####################
 # User DB Schema
@@ -25,20 +26,28 @@ class User(Base):
 
     id = Column(String, primary_key=True)
     name = Column(String)
+
     email = Column(String)
+    username = Column(String(50), nullable=True)
+
     role = Column(String)
     profile_image_url = Column(Text)
 
-    last_active_at = Column(BigInteger)
-    updated_at = Column(BigInteger)
-    created_at = Column(BigInteger)
+    bio = Column(Text, nullable=True)
+    gender = Column(Text, nullable=True)
+    date_of_birth = Column(Date, nullable=True)
 
-    api_key = Column(String, nullable=True, unique=True)
-    settings = Column(JSONField, nullable=True)
     info = Column(JSONField, nullable=True)
+    settings = Column(JSONField, nullable=True)
 
+    api_key = Column(String, nullable=True, unique=True)
     oauth_sub = Column(Text, unique=True)
 
+    last_active_at = Column(BigInteger)
+
+    updated_at = Column(BigInteger)
+    created_at = Column(BigInteger)
+
 
 class UserSettings(BaseModel):
     ui: Optional[dict] = {}
@@ -49,20 +58,27 @@ class UserSettings(BaseModel):
 class UserModel(BaseModel):
     id: str
     name: str
+
     email: str
+    username: Optional[str] = None
+
     role: str = "pending"
     profile_image_url: str
 
-    last_active_at: int  # timestamp in epoch
-    updated_at: int  # timestamp in epoch
-    created_at: int  # timestamp in epoch
+    bio: Optional[str] = None
+    gender: Optional[str] = None
+    date_of_birth: Optional[datetime.date] = None
 
-    api_key: Optional[str] = None
-    settings: Optional[UserSettings] = None
     info: Optional[dict] = None
+    settings: Optional[UserSettings] = None
 
+    api_key: Optional[str] = None
     oauth_sub: Optional[str] = None
 
+    last_active_at: int  # timestamp in epoch
+    updated_at: int  # timestamp in epoch
+    created_at: int  # timestamp in epoch
+
     model_config = ConfigDict(from_attributes=True)
 
 
@@ -71,6 +87,14 @@ class UserModel(BaseModel):
 ####################
 
 
+class UpdateProfileForm(BaseModel):
+    profile_image_url: str
+    name: str
+    bio: Optional[str] = None
+    gender: Optional[str] = None
+    date_of_birth: Optional[datetime.date] = None
+
+
 class UserListResponse(BaseModel):
     users: list[UserModel]
     total: int
@@ -349,7 +373,8 @@ class UsersTable:
                 user = db.query(User).filter_by(id=id).first()
                 return UserModel.model_validate(user)
                 # return UserModel(**user.dict())
-        except Exception:
+        except Exception as e:
+            print(e)
             return None
 
     def update_user_settings_by_id(self, id: str, updated: dict) -> Optional[UserModel]:

+ 12 - 4
backend/open_webui/routers/auths.py

@@ -15,10 +15,9 @@ from open_webui.models.auths import (
     SigninResponse,
     SignupForm,
     UpdatePasswordForm,
-    UpdateProfileForm,
     UserResponse,
 )
-from open_webui.models.users import Users
+from open_webui.models.users import Users, UpdateProfileForm
 from open_webui.models.groups import Groups
 
 from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
@@ -73,7 +72,13 @@ class SessionUserResponse(Token, UserResponse):
     permissions: Optional[dict] = None
 
 
-@router.get("/", response_model=SessionUserResponse)
+class SessionUserInfoResponse(SessionUserResponse):
+    bio: Optional[str] = None
+    gender: Optional[str] = None
+    date_of_birth: Optional[datetime.date] = None
+
+
+@router.get("/", response_model=SessionUserInfoResponse)
 async def get_session_user(
     request: Request, response: Response, user=Depends(get_current_user)
 ):
@@ -121,6 +126,9 @@ async def get_session_user(
         "name": user.name,
         "role": user.role,
         "profile_image_url": user.profile_image_url,
+        "bio": user.bio,
+        "gender": user.gender,
+        "date_of_birth": user.date_of_birth,
         "permissions": user_permissions,
     }
 
@@ -137,7 +145,7 @@ async def update_profile(
     if session_user:
         user = Users.update_user_by_id(
             session_user.id,
-            {"profile_image_url": form_data.profile_image_url, "name": form_data.name},
+            form_data.model_dump(),
         )
         if user:
             return user

+ 2 - 3
src/lib/apis/auths/index.ts

@@ -393,7 +393,7 @@ export const addUser = async (
 	return res;
 };
 
-export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
+export const updateUserProfile = async (token: string, profile: object) => {
 	let error = null;
 
 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/profile`, {
@@ -403,8 +403,7 @@ export const updateUserProfile = async (token: string, name: string, profileImag
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		body: JSON.stringify({
-			name: name,
-			profile_image_url: profileImageUrl
+			...profile
 		})
 	})
 		.then(async (res) => {

+ 86 - 10
src/lib/components/chat/Settings/Account.svelte

@@ -14,16 +14,23 @@
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
 	import Textarea from '$lib/components/common/Textarea.svelte';
+	import { getUserById } from '$lib/apis/users';
 
 	const i18n = getContext('i18n');
 
 	export let saveHandler: Function;
 	export let saveSettings: Function;
 
+	let loaded = false;
+
 	let profileImageUrl = '';
 	let name = '';
 	let bio = '';
 
+	let _gender = '';
+	let gender = '';
+	let dateOfBirth = '';
+
 	let webhookUrl = '';
 	let showAPIKeys = false;
 
@@ -49,11 +56,15 @@
 			});
 		}
 
-		const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
-			(error) => {
-				toast.error(`${error}`);
-			}
-		);
+		const updatedUser = await updateUserProfile(localStorage.token, {
+			name: name,
+			profile_image_url: profileImageUrl,
+			bio: bio ? bio : null,
+			gender: gender ? gender : null,
+			date_of_birth: dateOfBirth ? dateOfBirth : null
+		}).catch((error) => {
+			toast.error(`${error}`);
+		});
 
 		if (updatedUser) {
 			// Get Session User Info
@@ -78,14 +89,30 @@
 	};
 
 	onMount(async () => {
-		name = $user?.name;
-		profileImageUrl = $user?.profile_image_url;
+		const user = await getSessionUser(localStorage.token).catch((error) => {
+			toast.error(`${error}`);
+			return null;
+		});
+
+		if (user) {
+			name = user?.name ?? '';
+			profileImageUrl = user?.profile_image_url ?? '';
+			bio = user?.bio ?? '';
+
+			_gender = user?.gender ?? '';
+			gender = _gender;
+
+			dateOfBirth = user?.date_of_birth ?? '';
+		}
+
 		webhookUrl = $settings?.notifications?.webhook_url ?? '';
 
 		APIKey = await getAPIKey(localStorage.token).catch((error) => {
 			console.log(error);
 			return '';
 		});
+
+		loaded = true;
 	});
 </script>
 
@@ -164,7 +191,7 @@
 
 			<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
 
-			<div class="flex space-x-5 mt-4">
+			<div class="flex space-x-5 my-4">
 				<div class="flex flex-col self-start group">
 					<div class="self-center flex">
 						<button
@@ -177,7 +204,7 @@
 							<img
 								src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
 								alt="profile"
-								class=" rounded-full size-14 md:size-20 object-cover"
+								class=" rounded-full size-14 md:size-18 object-cover"
 							/>
 
 							<div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
@@ -254,12 +281,61 @@
 							<div class="flex-1">
 								<Textarea
 									className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
+									minSize={60}
 									bind:value={bio}
-									minSize={100}
 									placeholder={$i18n.t('Share your background and interests')}
 								/>
 							</div>
 						</div>
+
+						<div class="flex flex-col w-full mt-2">
+							<div class=" mb-1 text-xs font-medium">{$i18n.t('Gender')}</div>
+
+							<div class="flex-1">
+								<select
+									class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
+									bind:value={_gender}
+									on:change={(e) => {
+										console.log(_gender);
+
+										if (_gender === 'custom') {
+											// Handle custom gender input
+											gender = '';
+										} else {
+											gender = _gender;
+										}
+									}}
+								>
+									<option value="" selected>{$i18n.t('Prefer not to say')}</option>
+									<option value="male">{$i18n.t('Male')}</option>
+									<option value="female">{$i18n.t('Female')}</option>
+									<option value="custom">{$i18n.t('Custom')}</option>
+								</select>
+							</div>
+
+							{#if _gender === 'custom'}
+								<input
+									class="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden mt-1"
+									type="text"
+									required
+									placeholder={$i18n.t('Enter your gender')}
+									bind:value={gender}
+								/>
+							{/if}
+						</div>
+
+						<div class="flex flex-col w-full mt-2">
+							<div class=" mb-1 text-xs font-medium">{$i18n.t('Birth Date')}</div>
+
+							<div class="flex-1">
+								<input
+									class="w-full text-sm dark:text-gray-300 dark:placeholder:text-gray-300 bg-transparent outline-hidden"
+									type="date"
+									bind:value={dateOfBirth}
+									required
+								/>
+							</div>
+						</div>
 					</div>
 				</div>
 			</div>