Browse Source

feat: csv user import frontend

Timothy J. Baek 1 year ago
parent
commit
bd3b5f1edb

+ 74 - 2
backend/apps/web/routers/auths.py

@@ -1,12 +1,14 @@
 import logging
 
-from fastapi import Request
+from fastapi import Request, UploadFile, File
 from fastapi import Depends, HTTPException, status
 
 from fastapi import APIRouter
 from pydantic import BaseModel
 import re
 import uuid
+import csv
+
 
 from apps.web.models.auths import (
     SigninForm,
@@ -212,7 +214,7 @@ async def signup(request: Request, form_data: SignupForm):
 
 
 @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()):
         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))
 
 
+@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
 ############################

+ 1 - 0
backend/static/user-import.csv

@@ -0,0 +1 @@
+Name,Email,Password,Role

+ 120 - 59
src/lib/components/admin/AddUserModal.svelte

@@ -5,12 +5,16 @@
 	import { addUser } from '$lib/apis/auths';
 
 	import Modal from '../common/Modal.svelte';
+	import { WEBUI_BASE_URL } from '$lib/constants';
 
 	const i18n = getContext('i18n');
 	const dispatch = createEventDispatcher();
 
 	export let show = false;
 
+	let tab = '';
+	let inputFiles;
+
 	let _user = {
 		name: '',
 		email: '',
@@ -76,69 +80,126 @@
 						submitHandler();
 					}}
 				>
-					<div class=" ">
-						<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 class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
+						<button
+							class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
+							type="button"
+							on:click={() => {
+								tab = '';
+							}}>Form</button
+						>
+
+						<button
+							class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
+							type="button"
+							on:click={() => {
+								tab = 'import';
+							}}>CSV Import</button
+						>
+					</div>
+					<div class="px-1">
+						{#if tab === ''}
+							<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 class="flex flex-col w-full mt-2">
-							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
-
-							<div class="flex-1">
-								<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"
-									type="text"
-									bind:value={_user.name}
-									placeholder={$i18n.t('Enter Your Full Name')}
-									autocomplete="off"
-									required
-								/>
+
+							<div class="flex flex-col w-full mt-2">
+								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
+
+								<div class="flex-1">
+									<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"
+										type="text"
+										bind:value={_user.name}
+										placeholder={$i18n.t('Enter Your Full Name')}
+										autocomplete="off"
+										required
+									/>
+								</div>
 							</div>
-						</div>
-
-						<hr class=" dark:border-gray-800 my-3 w-full" />
-
-						<div class="flex flex-col w-full">
-							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
-
-							<div class="flex-1">
-								<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"
-									type="email"
-									bind:value={_user.email}
-									placeholder={$i18n.t('Enter Your Email')}
-									autocomplete="off"
-									required
-								/>
+
+							<hr class=" dark:border-gray-800 my-3 w-full" />
+
+							<div class="flex flex-col w-full">
+								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
+
+								<div class="flex-1">
+									<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"
+										type="email"
+										bind:value={_user.email}
+										placeholder={$i18n.t('Enter Your Email')}
+										autocomplete="off"
+										required
+									/>
+								</div>
+							</div>
+
+							<div class="flex flex-col w-full mt-2">
+								<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
+
+								<div class="flex-1">
+									<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"
+										type="password"
+										bind:value={_user.password}
+										placeholder={$i18n.t('Enter Your Password')}
+										autocomplete="off"
+									/>
+								</div>
 							</div>
-						</div>
-
-						<div class="flex flex-col w-full mt-2">
-							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
-
-							<div class="flex-1">
-								<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"
-									type="password"
-									bind:value={_user.password}
-									placeholder={$i18n.t('Enter Your Password')}
-									autocomplete="off"
-								/>
+						{: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>
-						</div>
+						{/if}
 					</div>
 
 					<div class="flex justify-end pt-3 text-sm font-medium">

+ 1 - 1
src/routes/(app)/+layout.svelte

@@ -154,7 +154,7 @@
 				if (isCtrlPressed && event.key === '.') {
 					event.preventDefault();
 					console.log('openSettings');
-					document.getElementById('open-settings-button')?.click();
+					showSettings.set(!$showSettings);
 				}
 
 				// Check if Ctrl + / is pressed