refac: account details

This commit is contained in:
Timothy Jaeryang Baek 2025-08-21 02:39:25 +04:00
parent 4451f86eb0
commit 86011e40be
6 changed files with 171 additions and 36 deletions

View file

@ -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")

View file

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

View file

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

View file

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

View file

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

View file

@ -14,16 +14,23 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Textarea from '$lib/components/common/Textarea.svelte'; import Textarea from '$lib/components/common/Textarea.svelte';
import { getUserById } from '$lib/apis/users';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let saveHandler: Function; export let saveHandler: Function;
export let saveSettings: Function; export let saveSettings: Function;
let loaded = false;
let profileImageUrl = ''; let profileImageUrl = '';
let name = ''; let name = '';
let bio = ''; let bio = '';
let _gender = '';
let gender = '';
let dateOfBirth = '';
let webhookUrl = ''; let webhookUrl = '';
let showAPIKeys = false; let showAPIKeys = false;
@ -49,11 +56,15 @@
}); });
} }
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( const updatedUser = await updateUserProfile(localStorage.token, {
(error) => { name: name,
toast.error(`${error}`); 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) { if (updatedUser) {
// Get Session User Info // Get Session User Info
@ -78,14 +89,30 @@
}; };
onMount(async () => { onMount(async () => {
name = $user?.name; const user = await getSessionUser(localStorage.token).catch((error) => {
profileImageUrl = $user?.profile_image_url; 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 ?? ''; webhookUrl = $settings?.notifications?.webhook_url ?? '';
APIKey = await getAPIKey(localStorage.token).catch((error) => { APIKey = await getAPIKey(localStorage.token).catch((error) => {
console.log(error); console.log(error);
return ''; return '';
}); });
loaded = true;
}); });
</script> </script>
@ -164,7 +191,7 @@
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> --> <!-- <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="flex flex-col self-start group">
<div class="self-center flex"> <div class="self-center flex">
<button <button
@ -177,7 +204,7 @@
<img <img
src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)} src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)}
alt="profile" 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"> <div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition">
@ -254,12 +281,61 @@
<div class="flex-1"> <div class="flex-1">
<Textarea <Textarea
className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden" className="w-full text-sm dark:text-gray-300 bg-transparent outline-hidden"
minSize={60}
bind:value={bio} bind:value={bio}
minSize={100}
placeholder={$i18n.t('Share your background and interests')} placeholder={$i18n.t('Share your background and interests')}
/> />
</div> </div>
</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> </div>
</div> </div>