perf(auth): make password hashing non-blocking and batch CSV user import (#25804)

Co-authored-by: Tim Baek <tim@openwebui.com>
This commit is contained in:
G30 2026-06-29 03:45:39 -04:00 committed by GitHub
parent 18e6a10778
commit 6fdf9b4340
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 33 additions and 17 deletions

View file

@ -379,7 +379,7 @@ async def update_password(
validate_password(form_data.new_password)
except Exception as e:
raise HTTPException(400, detail=str(e))
hashed = get_password_hash(form_data.new_password)
hashed = await get_password_hash(form_data.new_password)
success = await Auths.update_user_password_by_id(user.id, hashed, db=db)
if success:
await publish_event(
@ -786,7 +786,7 @@ async def signup_handler(
# Insert with default role first to avoid TOCTOU race on first signup.
# If has_users() is checked before insert, concurrent requests during
# first-user registration can all see an empty table and each get admin.
hashed = get_password_hash(password)
hashed = await get_password_hash(password)
user = await Auths.insert_new_auth(
email=email.lower(),
@ -1040,7 +1040,7 @@ async def add_user(
except Exception as e:
raise HTTPException(400, detail=str(e))
hashed = get_password_hash(form_data.password)
hashed = await get_password_hash(form_data.password)
user = await Auths.insert_new_auth(
form_data.email.lower(),
hashed,

View file

@ -642,7 +642,7 @@ async def update_user_by_id(
except Exception as e:
raise HTTPException(400, detail=str(e))
hashed = get_password_hash(form_data.password)
hashed = await get_password_hash(form_data.password)
await Auths.update_user_password_by_id(user_id, hashed, db=db)
# Build update dict from only the provided fields

View file

@ -158,9 +158,11 @@ def get_license_data(app, key):
bearer_security = HTTPBearer(auto_error=False)
def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt"""
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
async def get_password_hash(password: str) -> str:
"""Hash a password using bcrypt in a thread pool (non-blocking)."""
import asyncio
return (await asyncio.to_thread(bcrypt.hashpw, password.encode('utf-8'), bcrypt.gensalt())).decode('utf-8')
def validate_password(password: str) -> bool:
@ -486,7 +488,7 @@ async def create_admin_user(email: str, password: str, name: str = 'Admin'):
log.info(f'Creating admin account from environment variables: {email}')
try:
hashed = get_password_hash(password)
hashed = await get_password_hash(password)
user = await Auths.insert_new_auth(
email=email.lower(),
password=hashed,

View file

@ -1871,7 +1871,7 @@ class OAuthManager:
user = await Auths.insert_new_auth(
email=email,
password=get_password_hash(str(uuid.uuid4())), # Random password, not used
password=await get_password_hash(str(uuid.uuid4())), # Random password, not used
name=name,
profile_image_url=picture_url,
role=await self.get_user_role(None, user_data),

View file

@ -72,7 +72,7 @@
const csv = e.target.result;
const rows = csv.split('\n');
let userCount = 0;
const validRows = [];
for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim());
@ -83,7 +83,21 @@
columns.length === 4 &&
['admin', 'user', 'pending'].includes(columns[3].toLowerCase())
) {
const res = await addUser(
validRows.push({ idx, columns });
} else {
toast.error(`Row ${idx + 1}: invalid format.`);
}
}
}
let userCount = 0;
const BATCH_SIZE = 10;
for (let i = 0; i < validRows.length; i += BATCH_SIZE) {
const batch = validRows.slice(i, i + BATCH_SIZE);
const results = await Promise.allSettled(
batch.map(({ idx, columns }) =>
addUser(
localStorage.token,
columns[0],
columns[1],
@ -93,13 +107,13 @@
).catch((error) => {
toast.error(`Row ${idx + 1}: ${error}`);
return null;
});
})
)
);
if (res) {
userCount = userCount + 1;
}
} else {
toast.error(`Row ${idx + 1}: invalid format.`);
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
userCount++;
}
}
}