From ffddd9e8a5cdd04b51ee4183f56da990a7aa792e Mon Sep 17 00:00:00 2001
From: Joseph Livecchi <joewashear007@gmail.com>
Date: Sat, 4 May 2024 15:22:18 -0400
Subject: [PATCH] Added Header Authentication - Client Part  (#2362)

* Updated Login to handle header auth
---
 .../src/components/manager/ManagementApp.jsx  |   2 +-
 .../components/manager/subscribe/Login.tsx    | 109 ++++++++++++++----
 .../components/manager/subscribe/common.tsx   |  11 +-
 packages/loot-core/src/server/main.ts         |  12 +-
 .../loot-core/src/types/server-handlers.d.ts  |   5 +-
 upcoming-release-notes/2362.md                |   6 +
 6 files changed, 113 insertions(+), 32 deletions(-)
 create mode 100644 upcoming-release-notes/2362.md

diff --git a/packages/desktop-client/src/components/manager/ManagementApp.jsx b/packages/desktop-client/src/components/manager/ManagementApp.jsx
index c5200986f..2fb3e9165 100644
--- a/packages/desktop-client/src/components/manager/ManagementApp.jsx
+++ b/packages/desktop-client/src/components/manager/ManagementApp.jsx
@@ -184,7 +184,7 @@ export function ManagementApp({ isLoading }) {
               </>
             ) : (
               <Routes>
-                <Route path="/login" element={<Login />} />
+                <Route path="/login/:method?" element={<Login />} />
                 <Route path="/error" element={<Error />} />
                 <Route path="/config-server" element={<ConfigServer />} />
                 <Route path="/bootstrap" element={<Bootstrap />} />
diff --git a/packages/desktop-client/src/components/manager/subscribe/Login.tsx b/packages/desktop-client/src/components/manager/subscribe/Login.tsx
index 45cc2405d..15871a5ba 100644
--- a/packages/desktop-client/src/components/manager/subscribe/Login.tsx
+++ b/packages/desktop-client/src/components/manager/subscribe/Login.tsx
@@ -1,14 +1,17 @@
 // @ts-strict-ignore
-import React, { type ChangeEvent, useState } from 'react';
+import React, { type ChangeEvent, useState, useEffect } from 'react';
 import { useDispatch } from 'react-redux';
+import { useParams, useSearchParams } from 'react-router-dom';
 
 import { createBudget } from 'loot-core/src/client/actions/budgets';
 import { loggedIn } from 'loot-core/src/client/actions/user';
 import { send } from 'loot-core/src/platform/client/fetch';
 
+import { AnimatedLoading } from '../../../icons/AnimatedLoading';
 import { theme } from '../../../style';
 import { Button, ButtonWithLoading } from '../../common/Button';
 import { BigInput } from '../../common/Input';
+import { Link } from '../../common/Link';
 import { Text } from '../../common/Text';
 import { View } from '../../common/View';
 
@@ -16,14 +19,41 @@ import { useBootstrapped, Title } from './common';
 
 export function Login() {
   const dispatch = useDispatch();
+  const { method = 'password' } = useParams();
+  const [searchParams, _setSearchParams] = useSearchParams();
   const [password, setPassword] = useState('');
   const [loading, setLoading] = useState(false);
-  const [error, setError] = useState(null);
+  const [error, setError] = useState(searchParams.get('error'));
+  const { checked } = useBootstrapped(!searchParams.has('error'));
 
-  const { checked } = useBootstrapped();
+  useEffect(() => {
+    if (checked && !searchParams.has('error')) {
+      (async () => {
+        if (method === 'header') {
+          setError(null);
+          setLoading(true);
+          const { error } = await send('subscribe-sign-in', {
+            password: '',
+            loginMethod: method,
+          });
+          setLoading(false);
+
+          if (error) {
+            setError(error);
+          } else {
+            dispatch(loggedIn());
+          }
+        }
+      })();
+    }
+  }, [checked, searchParams, method, dispatch]);
 
   function getErrorMessage(error) {
     switch (error) {
+      case 'invalid-header':
+        return 'Auto login failed - No header sent';
+      case 'proxy-not-trusted':
+        return 'Auto login failed - Proxy not trusted';
       case 'invalid-password':
         return 'Invalid password';
       case 'network-failure':
@@ -41,7 +71,10 @@ export function Login() {
 
     setError(null);
     setLoading(true);
-    const { error } = await send('subscribe-sign-in', { password });
+    const { error } = await send('subscribe-sign-in', {
+      password,
+      loginMethod: method,
+    });
     setLoading(false);
 
     if (error) {
@@ -86,27 +119,55 @@ export function Login() {
         </Text>
       )}
 
-      <form
-        style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
-        onSubmit={onSubmit}
-      >
-        <BigInput
-          autoFocus={true}
-          placeholder="Password"
-          type="password"
-          onChange={(e: ChangeEvent<HTMLInputElement>) =>
-            setPassword(e.target.value)
-          }
-          style={{ flex: 1, marginRight: 10 }}
-        />
-        <ButtonWithLoading
-          type="primary"
-          loading={loading}
-          style={{ fontSize: 15 }}
+      {method === 'password' && (
+        <form
+          style={{ display: 'flex', flexDirection: 'row', marginTop: 30 }}
+          onSubmit={onSubmit}
         >
-          Sign in
-        </ButtonWithLoading>
-      </form>
+          <BigInput
+            autoFocus={true}
+            placeholder="Password"
+            type="password"
+            onChange={(e: ChangeEvent<HTMLInputElement>) =>
+              setPassword(e.target.value)
+            }
+            style={{ flex: 1, marginRight: 10 }}
+          />
+          <ButtonWithLoading
+            type="primary"
+            loading={loading}
+            style={{ fontSize: 15 }}
+          >
+            Sign in
+          </ButtonWithLoading>
+        </form>
+      )}
+      {method === 'header' && (
+        <View
+          style={{
+            flexDirection: 'row',
+            justifyContent: 'center',
+            marginTop: 15,
+          }}
+        >
+          {error && (
+            <Link
+              variant="button"
+              type="primary"
+              style={{ fontSize: 15 }}
+              to={'/login/password?error=' + error}
+            >
+              Login with Password
+            </Link>
+          )}
+          {!error && (
+            <span>
+              Checking Header Token Login ...{' '}
+              <AnimatedLoading style={{ width: 20, height: 20 }} />
+            </span>
+          )}
+        </View>
+      )}
       <View
         style={{
           flexDirection: 'row',
diff --git a/packages/desktop-client/src/components/manager/subscribe/common.tsx b/packages/desktop-client/src/components/manager/subscribe/common.tsx
index 8ebecc3e7..929b4bbbe 100644
--- a/packages/desktop-client/src/components/manager/subscribe/common.tsx
+++ b/packages/desktop-client/src/components/manager/subscribe/common.tsx
@@ -17,7 +17,7 @@ import { useSetServerURL } from '../../ServerContext';
 // password. Both pages will redirect to the other depending on state;
 // they will also potentially redirect to other pages which do *not*
 // do any checks.
-export function useBootstrapped() {
+export function useBootstrapped(redirect = true) {
   const [checked, setChecked] = useState(false);
   const navigate = useNavigate();
   const location = useLocation();
@@ -27,7 +27,9 @@ export function useBootstrapped() {
     async function run() {
       const ensure = url => {
         if (location.pathname !== url) {
-          navigate(url);
+          if (redirect) {
+            navigate(url);
+          }
         } else {
           setChecked(true);
         }
@@ -41,6 +43,7 @@ export function useBootstrapped() {
         const result = await send('subscribe-needs-bootstrap', {
           url: serverURL,
         });
+
         if ('error' in result || !result.hasServer) {
           console.log('error' in result && result.error);
           navigate('/config-server');
@@ -50,7 +53,7 @@ export function useBootstrapped() {
         await setServerURL(serverURL, { validate: false });
 
         if (result.bootstrapped) {
-          ensure('/login');
+          ensure(`/login/${result.loginMethod}`);
         } else {
           ensure('/bootstrap');
         }
@@ -59,7 +62,7 @@ export function useBootstrapped() {
         if ('error' in result) {
           navigate('/error', { state: { error: result.error } });
         } else if (result.bootstrapped) {
-          ensure('/login');
+          ensure(`/login/${result.loginMethod}`);
         } else {
           ensure('/bootstrap');
         }
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index 59e8feb2e..01ec57e50 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1410,7 +1410,11 @@ handlers['subscribe-needs-bootstrap'] = async function ({
     return { error: res.reason };
   }
 
-  return { bootstrapped: res.data.bootstrapped, hasServer: true };
+  return {
+    bootstrapped: res.data.bootstrapped,
+    loginMethod: res.data.loginMethod || 'password',
+    hasServer: true,
+  };
 };
 
 handlers['subscribe-bootstrap'] = async function ({ password }) {
@@ -1482,11 +1486,15 @@ handlers['subscribe-change-password'] = async function ({ password }) {
   return {};
 };
 
-handlers['subscribe-sign-in'] = async function ({ password }) {
+handlers['subscribe-sign-in'] = async function ({ password, loginMethod }) {
+  if (typeof loginMethod !== 'string' || loginMethod == null) {
+    loginMethod = 'password';
+  }
   let res;
 
   try {
     res = await post(getServer().SIGNUP_SERVER + '/login', {
+      loginMethod,
       password,
     });
   } catch (err) {
diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts
index f03c088aa..78c51f399 100644
--- a/packages/loot-core/src/types/server-handlers.d.ts
+++ b/packages/loot-core/src/types/server-handlers.d.ts
@@ -264,7 +264,10 @@ export interface ServerHandlers {
     password;
   }) => Promise<{ error?: string }>;
 
-  'subscribe-sign-in': (arg: { password }) => Promise<{ error?: string }>;
+  'subscribe-sign-in': (arg: {
+    password;
+    loginMethod?: string;
+  }) => Promise<{ error?: string }>;
 
   'subscribe-sign-out': () => Promise<'ok'>;
 
diff --git a/upcoming-release-notes/2362.md b/upcoming-release-notes/2362.md
new file mode 100644
index 000000000..e50069fe2
--- /dev/null
+++ b/upcoming-release-notes/2362.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [joewashear007]
+---
+
+Add option to authenticate with HTTP header `X-ACTUAL-PASSWORD` 
-- 
GitLab