forked from open-webui/open-webui
feat: csv user import frontend
This commit is contained in:
parent
e5703a588f
commit
bd3b5f1edb
4 changed files with 188 additions and 54 deletions
|
@ -1,12 +1,14 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request, UploadFile, File
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
from apps.web.models.auths import (
|
from apps.web.models.auths import (
|
||||||
SigninForm,
|
SigninForm,
|
||||||
|
@ -212,7 +214,7 @@ async def signup(request: Request, form_data: SignupForm):
|
||||||
|
|
||||||
|
|
||||||
@router.post("/add", response_model=SigninResponse)
|
@router.post("/add", response_model=SigninResponse)
|
||||||
async def signup(form_data: AddUserForm, user=Depends(get_admin_user)):
|
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
if not validate_email_format(form_data.email.lower()):
|
if not validate_email_format(form_data.email.lower()):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
@ -251,6 +253,76 @@ async def signup(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/add/import", response_model=SigninResponse)
|
||||||
|
async def add_user_csv_import(
|
||||||
|
file: UploadFile = File(...), user=Depends(get_admin_user)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Check if the file is a CSV file
|
||||||
|
if file.filename.endswith(".csv"):
|
||||||
|
# Read the contents of the CSV file
|
||||||
|
contents = await file.read()
|
||||||
|
|
||||||
|
# Decode the contents from bytes to string
|
||||||
|
decoded_content = contents.decode("utf-8")
|
||||||
|
|
||||||
|
# Split the CSV content into lines
|
||||||
|
csv_lines = decoded_content.split("\n")
|
||||||
|
|
||||||
|
# Parse CSV
|
||||||
|
csv_data = []
|
||||||
|
csv_reader = csv.reader(csv_lines)
|
||||||
|
for row in csv_reader:
|
||||||
|
csv_data.append(row)
|
||||||
|
|
||||||
|
# Print the CSV data
|
||||||
|
for row in csv_data:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
return {"message": "CSV file uploaded successfully."}
|
||||||
|
else:
|
||||||
|
raise "File must be a CSV."
|
||||||
|
except Exception as err:
|
||||||
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
# if not validate_email_format(form_data.email.lower()):
|
||||||
|
# raise HTTPException(
|
||||||
|
# status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if Users.get_user_by_email(form_data.email.lower()):
|
||||||
|
# raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||||
|
|
||||||
|
# try:
|
||||||
|
|
||||||
|
# print(form_data)
|
||||||
|
# hashed = get_password_hash(form_data.password)
|
||||||
|
# user = Auths.insert_new_auth(
|
||||||
|
# form_data.email.lower(),
|
||||||
|
# hashed,
|
||||||
|
# form_data.name,
|
||||||
|
# form_data.profile_image_url,
|
||||||
|
# form_data.role,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if user:
|
||||||
|
# token = create_token(data={"id": user.id})
|
||||||
|
# return {
|
||||||
|
# "token": token,
|
||||||
|
# "token_type": "Bearer",
|
||||||
|
# "id": user.id,
|
||||||
|
# "email": user.email,
|
||||||
|
# "name": user.name,
|
||||||
|
# "role": user.role,
|
||||||
|
# "profile_image_url": user.profile_image_url,
|
||||||
|
# }
|
||||||
|
# else:
|
||||||
|
# raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
|
||||||
|
# except Exception as err:
|
||||||
|
# raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# ToggleSignUp
|
# ToggleSignUp
|
||||||
############################
|
############################
|
||||||
|
|
1
backend/static/user-import.csv
Normal file
1
backend/static/user-import.csv
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Name,Email,Password,Role
|
|
|
@ -5,12 +5,16 @@
|
||||||
import { addUser } from '$lib/apis/auths';
|
import { addUser } from '$lib/apis/auths';
|
||||||
|
|
||||||
import Modal from '../common/Modal.svelte';
|
import Modal from '../common/Modal.svelte';
|
||||||
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
|
|
||||||
|
let tab = '';
|
||||||
|
let inputFiles;
|
||||||
|
|
||||||
let _user = {
|
let _user = {
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
@ -76,69 +80,126 @@
|
||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" ">
|
<div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
|
||||||
<div class="flex flex-col w-full">
|
<button
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
tab = '';
|
||||||
|
}}>Form</button
|
||||||
|
>
|
||||||
|
|
||||||
<div class="flex-1">
|
<button
|
||||||
<select
|
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
|
||||||
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
type="button"
|
||||||
bind:value={_user.role}
|
on:click={() => {
|
||||||
placeholder={$i18n.t('Enter Your Role')}
|
tab = 'import';
|
||||||
required
|
}}>CSV Import</button
|
||||||
>
|
>
|
||||||
<option value="pending"> pending </option>
|
</div>
|
||||||
<option value="user"> user </option>
|
<div class="px-1">
|
||||||
<option value="admin"> admin </option>
|
{#if tab === ''}
|
||||||
</select>
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<select
|
||||||
|
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
|
bind:value={_user.role}
|
||||||
|
placeholder={$i18n.t('Enter Your Role')}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="pending"> pending </option>
|
||||||
|
<option value="user"> user </option>
|
||||||
|
<option value="admin"> admin </option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={_user.name}
|
bind:value={_user.name}
|
||||||
placeholder={$i18n.t('Enter Your Full Name')}
|
placeholder={$i18n.t('Enter Your Full Name')}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" dark:border-gray-800 my-3 w-full" />
|
<hr class=" dark:border-gray-800 my-3 w-full" />
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
type="email"
|
type="email"
|
||||||
bind:value={_user.email}
|
bind:value={_user.email}
|
||||||
placeholder={$i18n.t('Enter Your Email')}
|
placeholder={$i18n.t('Enter Your Email')}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full mt-2">
|
<div class="flex flex-col w-full mt-2">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
|
||||||
type="password"
|
type="password"
|
||||||
bind:value={_user.password}
|
bind:value={_user.password}
|
||||||
placeholder={$i18n.t('Enter Your Password')}
|
placeholder={$i18n.t('Enter Your Password')}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else if tab === 'import'}
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 w-full">
|
||||||
|
<input
|
||||||
|
id="upload-user-csv-input"
|
||||||
|
hidden
|
||||||
|
bind:files={inputFiles}
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
document.getElementById('upload-user-csv-input')?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if inputFiles}
|
||||||
|
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||||
|
{:else}
|
||||||
|
{$i18n.t('Click here to select a csv file.')}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs text-gray-500">
|
||||||
|
ⓘ {$i18n.t(
|
||||||
|
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
class="underline dark:text-gray-200"
|
||||||
|
href="{WEBUI_BASE_URL}/static/user-import.csv"
|
||||||
|
>
|
||||||
|
Click here to download user import template file.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
if (isCtrlPressed && event.key === '.') {
|
if (isCtrlPressed && event.key === '.') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('openSettings');
|
console.log('openSettings');
|
||||||
document.getElementById('open-settings-button')?.click();
|
showSettings.set(!$showSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Ctrl + / is pressed
|
// Check if Ctrl + / is pressed
|
||||||
|
|
Loading…
Reference in a new issue