From 26521b70a79c42442f44c8053590bbb8c5e5f1b1 Mon Sep 17 00:00:00 2001
From: kamtschatka <simon.schatka@gmx.at>
Date: Sat, 21 Sep 2024 21:17:12 +0200
Subject: feature(extension): Allow login directly with an API key

* [Feature request] NextAuth Providers for OAuth/SSO #92
Added API key based authentication to the extension to make the extension usable when OAuth is in use

* Minor UI tweak

---------

Co-authored-by: MohamedBassem <me@mbassem.com>
---
 apps/browser-extension/src/SignInPage.tsx | 94 ++++++++++++++++++++++++++++---
 1 file changed, 86 insertions(+), 8 deletions(-)

(limited to 'apps/browser-extension/src/SignInPage.tsx')

diff --git a/apps/browser-extension/src/SignInPage.tsx b/apps/browser-extension/src/SignInPage.tsx
index 4e846070..f1899d5a 100644
--- a/apps/browser-extension/src/SignInPage.tsx
+++ b/apps/browser-extension/src/SignInPage.tsx
@@ -7,14 +7,20 @@ import Logo from "./Logo";
 import usePluginSettings from "./utils/settings";
 import { api } from "./utils/trpc";
 
+const enum LoginState {
+  NONE = "NONE",
+  USERNAME_PASSWORD = "USERNAME/PASSWORD",
+  API_KEY = "API_KEY",
+}
+
 export default function SignInPage() {
   const navigate = useNavigate();
   const { setSettings } = usePluginSettings();
 
   const {
     mutate: login,
-    error,
-    isPending,
+    error: usernamePasswordError,
+    isPending: userNamePasswordRequestIsPending,
   } = api.apiKeys.exchange.useMutation({
     onSuccess: (resp) => {
       setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id }));
@@ -22,6 +28,20 @@ export default function SignInPage() {
     },
   });
 
+  const {
+    mutate: validateApiKey,
+    error: apiKeyValidationError,
+    isPending: apiKeyValueRequestIsPending,
+  } = api.apiKeys.validate.useMutation({
+    onSuccess: () => {
+      setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey }));
+      navigate("/options");
+    },
+  });
+
+  const [lastLoginAttemptSource, setLastLoginAttemptSource] =
+    useState<LoginState>(LoginState.NONE);
+
   const [formData, setFormData] = useState<{
     email: string;
     password: string;
@@ -30,18 +50,40 @@ export default function SignInPage() {
     password: "",
   });
 
-  const onSubmit = (e: React.FormEvent) => {
+  const [apiKeyFormData, setApiKeyFormData] = useState<{
+    apiKey: string;
+  }>({
+    apiKey: "",
+  });
+
+  const onUserNamePasswordSubmit = (e: React.FormEvent) => {
     e.preventDefault();
+    setLastLoginAttemptSource(LoginState.USERNAME_PASSWORD);
     const randStr = (Math.random() + 1).toString(36).substring(5);
     login({ ...formData, keyName: `Browser extension: (${randStr})` });
   };
 
+  const onApiKeySubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    setLastLoginAttemptSource(LoginState.API_KEY);
+    validateApiKey({ ...apiKeyFormData });
+  };
+
   let errorMessage = "";
-  if (error) {
-    if (error.data?.code == "UNAUTHORIZED") {
+  let loginError;
+  switch (lastLoginAttemptSource) {
+    case LoginState.USERNAME_PASSWORD:
+      loginError = usernamePasswordError;
+      break;
+    case LoginState.API_KEY:
+      loginError = apiKeyValidationError;
+      break;
+  }
+  if (loginError) {
+    if (loginError.data?.code == "UNAUTHORIZED") {
       errorMessage = "Wrong username or password";
     } else {
-      errorMessage = error.message;
+      errorMessage = loginError.message;
     }
   }
 
@@ -50,7 +92,10 @@ export default function SignInPage() {
       <Logo />
       <p className="text-lg">Login</p>
       <p className="text-red-500">{errorMessage}</p>
-      <form className="flex flex-col gap-y-2" onSubmit={onSubmit}>
+      <form
+        className="flex flex-col gap-y-2"
+        onSubmit={onUserNamePasswordSubmit}
+      >
         <div className="flex flex-col gap-y-1">
           <label className="my-auto font-bold">Email</label>
           <Input
@@ -78,10 +123,43 @@ export default function SignInPage() {
             className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
           />
         </div>
-        <Button type="submit" disabled={isPending}>
+        <Button
+          type="submit"
+          disabled={
+            userNamePasswordRequestIsPending || apiKeyValueRequestIsPending
+          }
+        >
           Login
         </Button>
       </form>
+      <div className="flex w-full flex-row items-center gap-3">
+        <hr className="flex-1" />
+        Or
+        <hr className="flex-1" />
+      </div>
+
+      <form className="flex flex-col gap-y-2" onSubmit={onApiKeySubmit}>
+        <div className="flex flex-col gap-y-1">
+          <label className="my-auto font-bold">API Key</label>
+          <Input
+            value={apiKeyFormData.apiKey}
+            onChange={(e) =>
+              setApiKeyFormData((f) => ({ ...f, apiKey: e.target.value }))
+            }
+            type="text"
+            name="apiKey"
+            className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
+          />
+        </div>
+        <Button
+          type="submit"
+          disabled={
+            userNamePasswordRequestIsPending || apiKeyValueRequestIsPending
+          }
+        >
+          Login with API key
+        </Button>
+      </form>
     </div>
   );
 }
-- 
cgit v1.3-1-g0d28

