diff --git a/deployment/helmchart/CHANGELOG.md b/deployment/helmchart/CHANGELOG.md
index 72b6183c333a7fa50fbac5a798ee8b47d81f3a48..5a24ce3d4b53eeb40853634c17b9157dfdb41757 100644
--- a/deployment/helmchart/CHANGELOG.md
+++ b/deployment/helmchart/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Changelog
 
+## [1.2.0]
+
+### Features
+
+* Batch user creation by pasting CSV in the dashboard
+* When an admin's dashboard access is changed to "User", their app access now
+  defaults to "user"
+
 ## [1.1.0]
 
 ### Bug fixes
diff --git a/deployment/helmchart/Chart.yaml b/deployment/helmchart/Chart.yaml
index a0ba170b15a3eec697155252a508cc75d872dbfc..393b079fd0540552e9b4f30ccc9f2d059c1c1450 100644
--- a/deployment/helmchart/Chart.yaml
+++ b/deployment/helmchart/Chart.yaml
@@ -1,7 +1,7 @@
 annotations:
   category: Dashboard
 apiVersion: v2
-appVersion: 0.2.6
+appVersion: 0.2.8
 dependencies:
   - name: common
     # https://artifacthub.io/packages/helm/bitnami/common
@@ -23,4 +23,4 @@ name: stackspin-dashboard
 sources:
   - https://open.greenhost.net/stackspin/dashboard/
   - https://open.greenhost.net/stackspin/dashboard-backend/
-version: 1.1.0
+version: 1.2.0
diff --git a/deployment/helmchart/values.yaml b/deployment/helmchart/values.yaml
index 01c48fb9eade293417c5fa024368d143620a7ed8..35afd3be476b2b4428e846a5427765bc58a9c645 100644
--- a/deployment/helmchart/values.yaml
+++ b/deployment/helmchart/values.yaml
@@ -68,7 +68,7 @@ dashboard:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard/dashboard
-    tag: 0-2-7
+    tag: 0-2-8
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
     ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
@@ -235,7 +235,7 @@ backend:
   image:
     registry: open.greenhost.net:4567
     repository: stackspin/dashboard-backend/dashboard-backend
-    tag: 0-2-8
+    tag: 0-2-9
     ## Optionally specify an array of imagePullSecrets.
     ## Secrets must be manually created in the namespace.
     ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
diff --git a/src/common/util/show-toast.tsx b/src/common/util/show-toast.tsx
index 351a98558eb032a9bf179686eed10c6b6bfee46e..0ab0f3c3c700c0538d76733c0dc7690c877fae9d 100644
--- a/src/common/util/show-toast.tsx
+++ b/src/common/util/show-toast.tsx
@@ -9,7 +9,7 @@ export enum ToastType {
   Error = 'error',
 }
 
-export const showToast = (text: string, type?: ToastType) => {
+export const showToast = (text: string, type?: ToastType, duration?: number) => {
   switch (type) {
     case ToastType.Error:
       toast.custom(
@@ -47,7 +47,7 @@ export const showToast = (text: string, type?: ToastType) => {
             </div>
           </Transition>
         ),
-        { position: 'top-right' },
+        { position: 'top-right', duration },
       );
       break;
     default:
@@ -86,7 +86,7 @@ export const showToast = (text: string, type?: ToastType) => {
             </div>
           </Transition>
         ),
-        { position: 'top-right' },
+        { position: 'top-right', duration },
       );
   }
 };
diff --git a/src/components/Form/Select/Select.tsx b/src/components/Form/Select/Select.tsx
index aad51fa9d97789c95635086d21ff6655aabccda1..06a5322f710e625ab06c5cbe4c2b13429fe1e8b9 100644
--- a/src/components/Form/Select/Select.tsx
+++ b/src/components/Form/Select/Select.tsx
@@ -26,7 +26,7 @@ export const Select = ({ control, name, label, options, disabled = false }: Sele
         value={field.value ? field.value : ''} // input value
         name={name} // send down the input name
         ref={field.ref} // send input ref, so we can focus on input when error appear
-        className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
+        className="block shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
         disabled={disabled}
       >
         {options?.map((option) => (
diff --git a/src/components/Form/TextArea/index.ts b/src/components/Form/TextArea/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9f93c0de6dba7400e560e165cef83cdf9ea6c5cd
--- /dev/null
+++ b/src/components/Form/TextArea/index.ts
@@ -0,0 +1 @@
+export { TextArea } from './textarea';
diff --git a/src/components/Form/TextArea/textarea.tsx b/src/components/Form/TextArea/textarea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2dfa1882202ee1ab7617f8b7775729b5d6a20e31
--- /dev/null
+++ b/src/components/Form/TextArea/textarea.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useController } from 'react-hook-form';
+
+/* eslint-disable react/react-in-jsx-scope */
+export const TextArea = ({ control, name, label, required, ...props }: TextAreaProps) => {
+  const {
+    field,
+    // fieldState: { invalid, isTouched, isDirty },
+    // formState: { touchedFields, dirtyFields },
+  } = useController({
+    name,
+    control,
+    rules: { required },
+    defaultValue: '',
+  });
+
+  return (
+    <>
+      {label && (
+        <label htmlFor={name} className="block text-sm font-medium text-gray-700 mb-1">
+          {label}
+        </label>
+      )}
+      <textarea
+        id={name}
+        onChange={field.onChange} // send value to hook form
+        onBlur={field.onBlur} // notify when input is touched/blur
+        value={field.value ? field.value.toString() : ''} // input value
+        name={name} // send down the input name
+        ref={field.ref} // send input ref, so we can focus on input when error appear
+        autoComplete="given-name"
+        className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
+        {...props}
+      />
+    </>
+  );
+};
+
+type TextAreaProps = {
+  control: any;
+  name: string;
+  label?: string;
+} & React.HTMLProps<HTMLTextAreaElement>;
diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts
index 5276586281294c676e305d9e61c3f560a0574392..19728f23f4aafb767d10d04769a5f4e5147613d2 100644
--- a/src/components/Form/index.ts
+++ b/src/components/Form/index.ts
@@ -2,3 +2,4 @@ export { Input } from './Input';
 export { Select } from './Select';
 export { Switch } from './Switch';
 export { CodeEditor } from './CodeEditor';
+export { TextArea } from './TextArea';
diff --git a/src/components/Modal/Modal/Modal.tsx b/src/components/Modal/Modal/Modal.tsx
index 048b315d43da0bc0c525c1afd55cd363fa3c0dd7..e61aaee56622b8773885a9fb40af927c4e47c471 100644
--- a/src/components/Modal/Modal/Modal.tsx
+++ b/src/components/Modal/Modal/Modal.tsx
@@ -12,6 +12,7 @@ export const Modal: React.FC<ModalProps> = ({
   useCancelButton = false,
   isLoading = false,
   leftActions = <></>,
+  saveButtonDisabled = false,
 }) => {
   const cancelButtonRef = useRef(null);
   const saveButtonRef = useRef(null);
@@ -86,9 +87,12 @@ export const Modal: React.FC<ModalProps> = ({
                   <div className="ml-auto sm:flex sm:flex-row-reverse">
                     <button
                       type="button"
-                      className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+                      className={`w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm ${
+                        saveButtonDisabled ? 'opacity-50' : ''
+                      }`}
                       onClick={onSave}
                       ref={saveButtonRef}
+                      disabled={saveButtonDisabled}
                     >
                       Save Changes
                     </button>
diff --git a/src/components/Modal/Modal/types.ts b/src/components/Modal/Modal/types.ts
index afd720cbb24229bf015d625b2c1ac9621bd3e5eb..e679e696d4687ed1d1d2898721fcc82de22dedd3 100644
--- a/src/components/Modal/Modal/types.ts
+++ b/src/components/Modal/Modal/types.ts
@@ -8,4 +8,5 @@ export type ModalProps = {
   useCancelButton?: boolean;
   isLoading?: boolean;
   leftActions?: React.ReactNode;
+  saveButtonDisabled?: boolean;
 };
diff --git a/src/components/Modal/StepsModal/StepsModal.tsx b/src/components/Modal/StepsModal/StepsModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7e2135b4beb930e27233d919b7911a1a00586095
--- /dev/null
+++ b/src/components/Modal/StepsModal/StepsModal.tsx
@@ -0,0 +1,143 @@
+import React, { Fragment, useRef } from 'react';
+import { Dialog, Transition } from '@headlessui/react';
+import { XIcon } from '@heroicons/react/solid';
+import { StepsModalProps } from './types';
+
+export const StepsModal: React.FC<StepsModalProps> = ({
+  open,
+  onClose,
+  onSave,
+  onNext,
+  onPrevious,
+  children,
+  title = '',
+  useCancelButton = false,
+  isLoading = false,
+  leftActions = <></>,
+  showSaveButton = false,
+  showPreviousButton = false,
+  saveButtonDisabled = false,
+}) => {
+  const cancelButtonRef = useRef(null);
+  const saveButtonRef = useRef(null);
+  const nextButtonRef = useRef(null);
+  const previousButtonRef = useRef(null);
+
+  return (
+    <Transition.Root show={open} as={Fragment}>
+      <Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef} onClose={() => {}}>
+        <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
+          <Transition.Child
+            as={Fragment}
+            enter="ease-out duration-300"
+            enterFrom="opacity-0"
+            enterTo="opacity-100"
+            leave="ease-in duration-200"
+            leaveFrom="opacity-100"
+            leaveTo="opacity-0"
+          >
+            <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
+          </Transition.Child>
+
+          {/* This element is to trick the browser into centering the modal contents. */}
+          <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
+            &#8203;
+          </span>
+          <Transition.Child
+            as={Fragment}
+            enter="ease-out duration-300"
+            enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+            enterTo="opacity-100 translate-y-0 sm:scale-100"
+            leave="ease-in duration-200"
+            leaveFrom="opacity-100 translate-y-0 sm:scale-100"
+            leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+          >
+            <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full relative">
+              {isLoading && (
+                <Dialog.Overlay className="inset-0 bg-gray-400 bg-opacity-75 transition-opacity absolute flex justify-center items-center">
+                  <svg
+                    className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
+                    xmlns="http://www.w3.org/2000/svg"
+                    fill="none"
+                    viewBox="0 0 24 24"
+                  >
+                    <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
+                    <path
+                      className="opacity-75"
+                      fill="currentColor"
+                      d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+                    />
+                  </svg>
+                </Dialog.Overlay>
+              )}
+
+              {!useCancelButton && (
+                <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:items-center sm:justify-between">
+                  <div>{title}</div>
+                  <button
+                    type="button"
+                    className="w-full inline-flex justify-center rounded-md border border-gray-200 p-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
+                    onClick={onClose}
+                    ref={cancelButtonRef}
+                  >
+                    <XIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
+                  </button>
+                </div>
+              )}
+
+              <div className="bg-white px-4 p-6">{children}</div>
+              <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex">
+                {leftActions}
+                <div className="ml-auto sm:flex sm:flex-row-reverse">
+                  {showSaveButton && onSave && (
+                    <button
+                      type="button"
+                      className={`w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm ${
+                        saveButtonDisabled ? 'opacity-50' : ''
+                      }`}
+                      onClick={onSave}
+                      ref={saveButtonRef}
+                      disabled={saveButtonDisabled}
+                    >
+                      Save Changes
+                    </button>
+                  )}
+                  {!showSaveButton && (
+                    <button
+                      type="button"
+                      className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+                      onClick={onNext}
+                      ref={nextButtonRef}
+                    >
+                      Next
+                    </button>
+                  )}
+                  {showPreviousButton && (
+                    <button
+                      type="button"
+                      className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+                      onClick={onPrevious}
+                      ref={previousButtonRef}
+                    >
+                      Previous
+                    </button>
+                  )}
+                  {useCancelButton && (
+                    <button
+                      type="button"
+                      className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-200 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
+                      onClick={onClose}
+                      ref={cancelButtonRef}
+                    >
+                      Cancel
+                    </button>
+                  )}
+                </div>
+              </div>
+            </div>
+          </Transition.Child>
+        </div>
+      </Dialog>
+    </Transition.Root>
+  );
+};
diff --git a/src/components/Modal/StepsModal/index.ts b/src/components/Modal/StepsModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c9bae5e58db0b8b6e910dd541b1b2942a8a63a4
--- /dev/null
+++ b/src/components/Modal/StepsModal/index.ts
@@ -0,0 +1 @@
+export { StepsModal } from './StepsModal';
diff --git a/src/components/Modal/StepsModal/types.ts b/src/components/Modal/StepsModal/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0b5bbd1f5d9c952e1d1e5d73822f719b26900cb
--- /dev/null
+++ b/src/components/Modal/StepsModal/types.ts
@@ -0,0 +1,16 @@
+import React from 'react';
+
+export type StepsModalProps = {
+  open: boolean;
+  onClose: () => void;
+  onNext: () => void;
+  onPrevious: () => void;
+  title?: string;
+  onSave?: () => void;
+  useCancelButton?: boolean;
+  isLoading?: boolean;
+  leftActions?: React.ReactNode;
+  showSaveButton?: boolean;
+  showPreviousButton?: boolean;
+  saveButtonDisabled?: boolean;
+};
diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts
index 4ef3e384fe3273b562aa57b7a3eaa58eead8fa2d..e5800df0cd61eb051c8456da27e6abece8183ee3 100644
--- a/src/components/Modal/index.ts
+++ b/src/components/Modal/index.ts
@@ -1,2 +1,3 @@
 export { ConfirmationModal } from './ConfirmationModal';
 export { Modal } from './Modal';
+export { StepsModal } from './StepsModal';
diff --git a/src/components/ProgressSteps/ProgressSteps.tsx b/src/components/ProgressSteps/ProgressSteps.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a24d67506ec09a94aab0a82112509174071f1c9
--- /dev/null
+++ b/src/components/ProgressSteps/ProgressSteps.tsx
@@ -0,0 +1,107 @@
+import _ from 'lodash';
+import React from 'react';
+import { ProgressStepsProps, ProgressStepStatus } from './types';
+
+export const ProgressSteps: React.FC<ProgressStepsProps> = ({ steps, onNext, onPrevious, onStepClick, children }) => {
+  const handleNext = () => {
+    if (onNext) {
+      onNext();
+    }
+  };
+  const handlePrevious = () => {
+    if (onPrevious) {
+      onPrevious();
+    }
+  };
+
+  const showNextPage = () => {
+    if (onNext) {
+      return _.some(steps, { status: ProgressStepStatus.Upcoming });
+    }
+    return false;
+  };
+
+  const showPreviousPage = () => {
+    if (onPrevious) {
+      return _.some(steps, { status: ProgressStepStatus.Complete });
+    }
+    return false;
+  };
+
+  const handleStepClick = (stepId: string) => {
+    if (onStepClick) {
+      onStepClick(stepId);
+    }
+  };
+
+  return (
+    <>
+      <nav aria-label="Progress">
+        {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
+        <ol role="list" className="space-y-4 md:flex md:space-y-0 md:space-x-8 mb-4">
+          {steps.map((step) => (
+            <li key={step.name} className="md:flex-1" onClick={() => handleStepClick(step.id)}>
+              {step.status === ProgressStepStatus.Complete ? (
+                <a
+                  href={step.href}
+                  className="group pl-4 py-2 flex flex-col border-l-4 border-primary-500 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                >
+                  <span className="text-xs text-primary-600 font-semibold tracking-wide uppercase group-hover:text-primary-800">
+                    {step.id}
+                  </span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              ) : step.status === ProgressStepStatus.Current ? (
+                <a
+                  href={step.href}
+                  className="pl-4 py-2 flex flex-col border-l-4 border-primary-500 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                  aria-current="step"
+                >
+                  <span className="text-xs text-primary-600 font-semibold tracking-wide uppercase">{step.id}</span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              ) : (
+                <a
+                  href={step.href}
+                  className="group pl-4 py-2 flex flex-col border-l-4 border-gray-200 hover:border-gray-300 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                >
+                  <span className="text-xs text-gray-500 font-semibold tracking-wide uppercase group-hover:text-gray-700">
+                    {step.id}
+                  </span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              )}
+            </li>
+          ))}
+        </ol>
+      </nav>
+
+      {children}
+
+      {(showNextPage() || showPreviousPage()) && (
+        <div className="pt-4 sm sm:flex">
+          <div className="ml-auto sm:flex sm:flex-row-reverse">
+            {showNextPage() && (
+              <button
+                type="button"
+                className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+                onClick={handleNext}
+              >
+                Next
+              </button>
+            )}
+            {showPreviousPage() && (
+              <button
+                type="button"
+                className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
+                onClick={handlePrevious}
+              >
+                Previous
+              </button>
+            )}
+          </div>
+        </div>
+      )}
+    </>
+  );
+};
diff --git a/src/components/ProgressSteps/index.ts b/src/components/ProgressSteps/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2bc8859dd118f5ff103c3a7f53093e5cc27697c2
--- /dev/null
+++ b/src/components/ProgressSteps/index.ts
@@ -0,0 +1 @@
+export { ProgressSteps } from './ProgressSteps';
diff --git a/src/components/ProgressSteps/types.ts b/src/components/ProgressSteps/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3eed8b3bc6572077b711366efc879964ae90f4c3
--- /dev/null
+++ b/src/components/ProgressSteps/types.ts
@@ -0,0 +1,20 @@
+export type ProgressStepsProps = {
+  steps: ProgressStepInfo[];
+  onNext?: () => void;
+  onPrevious?: () => void;
+  onStepClick?: (stepId: string) => void;
+};
+
+export interface ProgressStepInfo {
+  id: string;
+  name: string;
+  status: ProgressStepStatus;
+  component?: React.ReactNode;
+  href?: string;
+}
+
+export enum ProgressStepStatus {
+  Complete = 0,
+  Current = 1,
+  Upcoming = 2,
+}
diff --git a/src/components/UserModal/UserModal.tsx b/src/components/UserModal/UserModal.tsx
index 0993b94fbeb3473526f26b9a83e05ebdf635c6fe..404db4b13b73463405966b934365789223787c92 100644
--- a/src/components/UserModal/UserModal.tsx
+++ b/src/components/UserModal/UserModal.tsx
@@ -68,6 +68,8 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps)
     setAdminRoleSelected(isAdminDashboardRoleSelected);
     if (isAdminDashboardRoleSelected) {
       fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
+    } else {
+      fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [dashboardRole]);
diff --git a/src/components/UserModal/consts.ts b/src/components/UserModal/consts.ts
index 38827fbfa94a07913543ac40759e7c626fd6155c..b0b0deaeaa38070dcdb50f6d53b7ee831732b827 100644
--- a/src/components/UserModal/consts.ts
+++ b/src/components/UserModal/consts.ts
@@ -31,26 +31,35 @@ export const appAccessList = [
   },
 ];
 
-const initialAppRoles = [
+export const allAppAccessList = [
+  {
+    name: 'dashboard',
+    image: '/assets/logo-small.svg',
+    label: 'Dashboard',
+  },
+  ...appAccessList,
+];
+
+export const initialAppRoles = [
   {
     name: 'dashboard',
     role: UserRole.User,
   },
   {
     name: 'wekan',
-    role: UserRole.NoAccess,
+    role: UserRole.User,
   },
   {
     name: 'wordpress',
-    role: UserRole.NoAccess,
+    role: UserRole.User,
   },
   {
     name: 'nextcloud',
-    role: UserRole.NoAccess,
+    role: UserRole.User,
   },
   {
     name: 'zulip',
-    role: UserRole.NoAccess,
+    role: UserRole.User,
   },
 ];
 
diff --git a/src/components/index.ts b/src/components/index.ts
index 565e17d3790a54e2f7c8a2757fa444d443539cfd..9a2f607cfeb960268b04186d98cfc92dcd0c9abc 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -3,5 +3,6 @@ export { Header } from './Header';
 export { Table } from './Table';
 export { Banner } from './Banner';
 export { Tabs } from './Tabs';
-export { Modal, ConfirmationModal } from './Modal';
+export { Modal, ConfirmationModal, StepsModal } from './Modal';
 export { UserModal } from './UserModal';
+export { ProgressSteps } from './ProgressSteps';
diff --git a/src/modules/users/Users.tsx b/src/modules/users/Users.tsx
index c40cc794947c6b87834fb1e42cf2bae6b7993b96..41cfafbf3444a685267465d88df1dd9938bee7dc 100644
--- a/src/modules/users/Users.tsx
+++ b/src/modules/users/Users.tsx
@@ -1,6 +1,6 @@
 /* eslint-disable react-hooks/exhaustive-deps */
 import React, { useState, useCallback, useEffect, useMemo } from 'react';
-import { SearchIcon, PlusIcon } from '@heroicons/react/solid';
+import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
 import { CogIcon, TrashIcon } from '@heroicons/react/outline';
 import { useUsers } from 'src/services/users';
 import { Table } from 'src/components';
@@ -8,10 +8,12 @@ import { debounce } from 'lodash';
 import { useAuth } from 'src/services/auth';
 
 import { UserModal } from '../../components/UserModal';
+import { MultipleUsersModal } from './components';
 
 export const Users: React.FC = () => {
   const [selectedRowsIds, setSelectedRowsIds] = useState({});
   const [configureModal, setConfigureModal] = useState(false);
+  const [multipleUsersModal, setMultipleUsersModal] = useState(false);
   const [userId, setUserId] = useState(null);
   const [search, setSearch] = useState('');
   const { users, loadUsers, userTableLoading } = useUsers();
@@ -39,8 +41,11 @@ export const Users: React.FC = () => {
     setUserId(id);
     setConfigureModal(true);
   };
+
   const configureModalClose = () => setConfigureModal(false);
 
+  const multipleUsersModalClose = () => setMultipleUsersModal(false);
+
   const columns: any = React.useMemo(
     () => [
       {
@@ -101,11 +106,19 @@ export const Users: React.FC = () => {
               <button
                 onClick={() => configureModalOpen(null)}
                 type="button"
-                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
+                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
               >
                 <PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
                 Add new user
               </button>
+              <button
+                onClick={() => setMultipleUsersModal(true)}
+                type="button"
+                className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
+              >
+                <ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
+                Add new users
+              </button>
             </div>
           )}
         </div>
@@ -166,6 +179,7 @@ export const Users: React.FC = () => {
         {configureModal && (
           <UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} />
         )}
+        {multipleUsersModal && <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} />}
       </div>
     </div>
   );
diff --git a/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..74f60c692ec53fc6347ea35a1ced1db40e22b59d
--- /dev/null
+++ b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
@@ -0,0 +1,248 @@
+import React, { useEffect, useState } from 'react';
+import _ from 'lodash';
+import { useFieldArray, useForm, useWatch } from 'react-hook-form';
+
+import { Banner, StepsModal, ProgressSteps } from 'src/components';
+import { Select, TextArea } from 'src/components/Form';
+import { MultipleUsersData, UserRole, useUsers } from 'src/services/users';
+import { allAppAccessList } from 'src/components/UserModal/consts';
+import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types';
+import { initialMultipleUsersForm, MultipleUsersModalProps } from './types';
+
+export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => {
+  const [steps, setSteps] = useState<ProgressStepInfo[]>([]);
+  const [isAdminRoleSelected, setAdminRoleSelected] = useState(false);
+  const { createUsers, userModalLoading } = useUsers();
+
+  const { control, handleSubmit } = useForm<MultipleUsersData>({
+    defaultValues: initialMultipleUsersForm,
+  });
+
+  const { fields, update } = useFieldArray({
+    control,
+    name: 'appRoles',
+  });
+
+  const dashboardRole = useWatch({
+    control,
+    name: 'appRoles.0.role',
+  });
+
+  const csvDataWatch = useWatch({
+    control,
+    name: 'csvUserData',
+  });
+
+  useEffect(() => {
+    const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
+    setAdminRoleSelected(isAdminDashboardRoleSelected);
+    if (isAdminDashboardRoleSelected) {
+      fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
+    } else {
+      fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [dashboardRole]);
+
+  const renderUsersCsvDataInput = () => {
+    return (
+      <div>
+        <div className="mt-8">
+          <h3 className="text-lg leading-6 font-medium text-gray-900">CSV data</h3>
+        </div>
+        <div className="mt-6">
+          <TextArea
+            control={control}
+            name="csvUserData"
+            placeholder={`Please paste users in CSV format: email, name\nuser1@example.com,User One\nuser2@example.com,User Two`}
+            rows={15}
+            required
+          />
+        </div>
+      </div>
+    );
+  };
+
+  const renderAppAccess = () => {
+    return (
+      <div>
+        <div className="mt-8">
+          <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
+        </div>
+
+        {isAdminRoleSelected && (
+          <div className="sm:col-span-6">
+            <Banner title="Admin users automatically have admin-level access to all apps." titleSm="Admin user" />
+          </div>
+        )}
+
+        <div>
+          <div className="flow-root mt-6">
+            <ul className="-my-5 divide-y divide-gray-200">
+              {fields
+                .filter((field) => field.name === 'dashboard')
+                .map((item, index) => (
+                  <li className="py-4" key={item.name}>
+                    <div className="flex items-center space-x-4">
+                      <div className="flex-shrink-0 flex-1 flex items-center">
+                        <img
+                          className="h-10 w-10 rounded-md overflow-hidden"
+                          src={_.find(allAppAccessList, ['name', item.name!])?.image}
+                          alt={item.name ?? 'Image'}
+                        />
+                        <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
+                          {_.find(allAppAccessList, ['name', item.name!])?.label}
+                        </h3>
+                      </div>
+                      <div className="sm:col-span-2">
+                        <Select
+                          key={item.id}
+                          control={control}
+                          name={`appRoles.${index}.role`}
+                          options={[
+                            { value: UserRole.User, name: 'User' },
+                            { value: UserRole.Admin, name: 'Admin' },
+                          ]}
+                        />
+                      </div>
+                    </div>
+                  </li>
+                ))}
+              {!isAdminRoleSelected &&
+                fields.map((item, index) => {
+                  if (item.name === 'dashboard') {
+                    return null;
+                  }
+
+                  return (
+                    <li className="py-4" key={item.name}>
+                      <div className="flex items-center space-x-4">
+                        <div className="flex-shrink-0 flex-1 flex items-center">
+                          <img
+                            className="h-10 w-10 rounded-md overflow-hidden"
+                            src={_.find(allAppAccessList, ['name', item.name!])?.image}
+                            alt={item.name ?? 'Image'}
+                          />
+                          <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
+                            {_.find(allAppAccessList, ['name', item.name!])?.label}
+                          </h3>
+                        </div>
+                        <div className="sm:col-span-2">
+                          <Select
+                            key={item.id}
+                            control={control}
+                            name={`appRoles.${index}.role`}
+                            disabled={isAdminRoleSelected}
+                            options={[
+                              { value: UserRole.NoAccess, name: 'No Access' },
+                              { value: UserRole.User, name: 'User' },
+                              { value: UserRole.Admin, name: 'Admin' },
+                            ]}
+                          />
+                        </div>
+                      </div>
+                    </li>
+                  );
+                })}
+            </ul>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  useEffect(() => {
+    setSteps([
+      {
+        id: 'Step 1',
+        name: 'Enter CSV user data',
+        status: ProgressStepStatus.Current,
+      },
+      {
+        id: 'Step 2',
+        name: 'Define app access roles',
+        status: ProgressStepStatus.Upcoming,
+      },
+    ]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open]);
+
+  const handleSave = async () => {
+    try {
+      await handleSubmit((data) => createUsers(data))();
+    } catch (e: any) {
+      // Continue
+    }
+
+    onClose();
+  };
+
+  const handleClose = () => {
+    onClose();
+  };
+
+  const getActiveStepIndex = () => _.findIndex(steps, { status: ProgressStepStatus.Current });
+
+  const updateStepsStatus = (nextIndex: number) => {
+    const updatedSteps = [...steps];
+    _.forEach(updatedSteps, (step, index) => {
+      if (index < nextIndex) {
+        step.status = ProgressStepStatus.Complete;
+      } else if (index === nextIndex) {
+        step.status = ProgressStepStatus.Current;
+      } else {
+        step.status = ProgressStepStatus.Upcoming;
+      }
+    });
+    setSteps(updatedSteps);
+  };
+
+  const handleStepClick = (stepId: string) => {
+    const activeStepIndex = _.findIndex(steps, { id: stepId });
+    updateStepsStatus(activeStepIndex);
+  };
+
+  const handleNext = () => {
+    const nextIndex = getActiveStepIndex() + 1;
+    updateStepsStatus(nextIndex);
+  };
+
+  const handlePrevious = () => {
+    const nextIndex = getActiveStepIndex() - 1;
+    updateStepsStatus(nextIndex);
+  };
+
+  const activeStepIndex = getActiveStepIndex();
+  const showSave = !_.some(steps, { status: ProgressStepStatus.Upcoming });
+  const showPrevious = _.some(steps, { status: ProgressStepStatus.Complete });
+
+  return (
+    <StepsModal
+      onClose={handleClose}
+      open={open}
+      onSave={handleSave}
+      onNext={handleNext}
+      onPrevious={handlePrevious}
+      showPreviousButton={showPrevious}
+      isLoading={userModalLoading}
+      useCancelButton
+      showSaveButton={showSave}
+      saveButtonDisabled={_.isEmpty(csvDataWatch)}
+    >
+      <div className="bg-white px-4">
+        <div className="space-y-10 divide-y divide-gray-200">
+          <div>
+            <div>
+              <h3 className="text-lg leading-6 font-medium text-gray-900">Add new users</h3>
+            </div>
+            <div className="sm:px-6 pt-6">
+              <ProgressSteps steps={steps} onStepClick={handleStepClick}>
+                {activeStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()}
+              </ProgressSteps>
+            </div>
+          </div>
+        </div>
+      </div>
+    </StepsModal>
+  );
+};
diff --git a/src/modules/users/components/MultipleUsersModal/index.ts b/src/modules/users/components/MultipleUsersModal/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8c3b1abdcb63e5dffe1ed477b643f9e595adfe1
--- /dev/null
+++ b/src/modules/users/components/MultipleUsersModal/index.ts
@@ -0,0 +1 @@
+export { MultipleUsersModal } from './MultipleUsersModal';
diff --git a/src/modules/users/components/MultipleUsersModal/types.ts b/src/modules/users/components/MultipleUsersModal/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..643f10cc604b86b9e8a5222db4863299f61fa042
--- /dev/null
+++ b/src/modules/users/components/MultipleUsersModal/types.ts
@@ -0,0 +1,10 @@
+import { initialAppRoles } from 'src/components/UserModal/consts';
+
+export type MultipleUsersModalProps = {
+  open: boolean;
+  onClose: () => void;
+};
+
+export const initialMultipleUsersForm = {
+  appRoles: initialAppRoles,
+};
diff --git a/src/modules/users/components/index.ts b/src/modules/users/components/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b8c3b1abdcb63e5dffe1ed477b643f9e595adfe1
--- /dev/null
+++ b/src/modules/users/components/index.ts
@@ -0,0 +1 @@
+export { MultipleUsersModal } from './MultipleUsersModal';
diff --git a/src/services/users/hooks/use-users.ts b/src/services/users/hooks/use-users.ts
index 75febdcc9d2efbdda99521e89a25142eaf927b18..2d0381de36faf88771aab5af1e9e033f455a68fc 100644
--- a/src/services/users/hooks/use-users.ts
+++ b/src/services/users/hooks/use-users.ts
@@ -9,6 +9,7 @@ import {
   createUser,
   deleteUser,
   clearCurrentUser,
+  createBatchUsers,
 } from '../redux';
 import { getUserById, getUserModalLoading, getUserslLoading } from '../redux/selectors';
 
@@ -47,6 +48,10 @@ export function useUsers() {
     return dispatch(createUser(data));
   }
 
+  function createUsers(data: any) {
+    return dispatch(createBatchUsers(data));
+  }
+
   function deleteUserById(id: string) {
     return dispatch(deleteUser(id));
   }
@@ -64,5 +69,6 @@ export function useUsers() {
     createNewUser,
     deleteUserById,
     clearSelectedUser,
+    createUsers,
   };
 }
diff --git a/src/services/users/redux/actions.ts b/src/services/users/redux/actions.ts
index 1b59cc247c1189732576d92f99006288392f8f96..7643b41e83586929faf4a108ec8fafae56b28331 100644
--- a/src/services/users/redux/actions.ts
+++ b/src/services/users/redux/actions.ts
@@ -1,9 +1,15 @@
+import _ from 'lodash';
 import { Dispatch } from 'redux';
 import { showToast, ToastType } from 'src/common/util/show-toast';
 import { State } from 'src/redux/types';
 import { performApiCall } from 'src/services/api';
 import { AuthActionTypes } from 'src/services/auth';
-import { transformRequestUser, transformUser } from '../transformations';
+import {
+  transformBatchResponse,
+  transformRequestMultipleUsers,
+  transformRequestUser,
+  transformUser,
+} from '../transformations';
 
 export enum UserActionTypes {
   FETCH_USERS = 'users/fetch_users',
@@ -13,6 +19,7 @@ export enum UserActionTypes {
   DELETE_USER = 'users/delete_user',
   SET_USER_MODAL_LOADING = 'users/user_modal_loading',
   SET_USERS_LOADING = 'users/users_loading',
+  CREATE_BATCH_USERS = 'users/create_batch_users',
 }
 
 export const setUsersLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
@@ -202,6 +209,44 @@ export const deleteUser = (id: string) => async (dispatch: Dispatch<any>) => {
   dispatch(setUserModalLoading(false));
 };
 
+export const createBatchUsers = (users: any) => async (dispatch: Dispatch<any>) => {
+  dispatch(setUserModalLoading(true));
+
+  try {
+    const { data } = await performApiCall({
+      path: '/users-batch',
+      method: 'POST',
+      body: transformRequestMultipleUsers(users),
+    });
+
+    const responseData = transformBatchResponse(data);
+
+    dispatch({
+      type: UserActionTypes.CREATE_BATCH_USERS,
+      payload: responseData,
+    });
+
+    // show information about created users
+    if (!_.isEmpty(responseData.success)) {
+      showToast(responseData.success.message, ToastType.Success, Infinity);
+    }
+    if (!_.isEmpty(responseData.existing)) {
+      showToast(responseData.existing.message, ToastType.Error, Infinity);
+    }
+    if (!_.isEmpty(responseData.failed)) {
+      showToast(responseData.failed.message, ToastType.Error, Infinity);
+    }
+
+    dispatch(fetchUsers());
+  } catch (err: any) {
+    dispatch(setUserModalLoading(false));
+    showToast(`${err}`, ToastType.Error);
+    throw err;
+  }
+
+  dispatch(setUserModalLoading(false));
+};
+
 export const clearCurrentUser = () => (dispatch: Dispatch<any>) => {
   dispatch({
     type: UserActionTypes.DELETE_USER,
diff --git a/src/services/users/redux/reducers.ts b/src/services/users/redux/reducers.ts
index 71ff2fa07b42a7b9c5252298b995dd31e294e0fa..2d02771db98e3c3419b651a7d9e721ce4f26c59e 100644
--- a/src/services/users/redux/reducers.ts
+++ b/src/services/users/redux/reducers.ts
@@ -27,6 +27,7 @@ const usersReducer = (state: any = initialUsersState, action: any) => {
     case UserActionTypes.FETCH_USER:
     case UserActionTypes.UPDATE_USER:
     case UserActionTypes.CREATE_USER:
+    case UserActionTypes.CREATE_BATCH_USERS:
       return {
         ...state,
         isModalVisible: false,
diff --git a/src/services/users/transformations.ts b/src/services/users/transformations.ts
index bc2bd05501259f598e289e0b05c2afb43de071d8..21e7db46413d1b240b861315de78fa09fb37c2a5 100644
--- a/src/services/users/transformations.ts
+++ b/src/services/users/transformations.ts
@@ -1,4 +1,5 @@
-import { AppRoles, User, UserRole } from './types';
+import _ from 'lodash';
+import { AppRoles, MultipleUsersData, User, UserRole } from './types';
 
 const transformRoleById = (roleId: any): UserRole => {
   switch (roleId) {
@@ -62,3 +63,31 @@ export const transformRequestUser = (data: Pick<User, 'app_roles' | 'name' | 'em
     name: data.name ?? '',
   };
 };
+
+const extractUsersFromCsv = (csvData: string) => {
+  const csvRows = csvData.split('\n');
+
+  return _.map(csvRows, (row) => {
+    const values = row.split(',');
+    const email = values[0].trim();
+    const name = !_.isNil(values[1]) ? values[1].trim() : '';
+    return { email, name, app_roles: [] };
+  });
+};
+
+export const transformRequestMultipleUsers = (data: MultipleUsersData) => {
+  const batchUsers = extractUsersFromCsv(data.csvUserData);
+  return {
+    users: _.map(batchUsers, (user) =>
+      transformRequestUser({ app_roles: data.appRoles, name: user.name, email: user.email } as User),
+    ),
+  };
+};
+
+export const transformBatchResponse = (response: any): any => {
+  return {
+    success: response.success,
+    existing: response.existing,
+    failed: response.failed,
+  };
+};
diff --git a/src/services/users/types.ts b/src/services/users/types.ts
index f97f43543661c1f0044d81cb0c95c81c8a289384..d22811cba75d1cd8aa84c983de14f632df089dda 100644
--- a/src/services/users/types.ts
+++ b/src/services/users/types.ts
@@ -29,3 +29,8 @@ export interface UserApiRequest {
   name: string;
   status: string;
 }
+
+export interface MultipleUsersData {
+  csvUserData: string;
+  appRoles: AppRoles[];
+}