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
 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 - 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 { 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 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>
-
-						<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>
-						</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>
-
-						<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>
-						</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">

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

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