mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
refac: account details
This commit is contained in:
parent
4451f86eb0
commit
86011e40be
6 changed files with 171 additions and 36 deletions
|
|
@ -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")
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
profile_image_url: profileImageUrl,
|
||||||
|
bio: bio ? bio : null,
|
||||||
|
gender: gender ? gender : null,
|
||||||
|
date_of_birth: dateOfBirth ? dateOfBirth : null
|
||||||
|
}).catch((error) => {
|
||||||
toast.error(`${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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue