Skip to content

Commit 9bd7ce4

Browse files
committed
feat: Fallback to basic auth for instances and clusters
https://harperdb.atlassian.net/browse/STUDIO-588
1 parent 9cb1df3 commit 9bd7ce4

File tree

10 files changed

+70
-29
lines changed

10 files changed

+70
-29
lines changed

src/config/getInstanceClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ export function getInstanceClient({ id = OverallAppSignIn, operationsUrl, port,
2323
baseURL = newURL.toString();
2424
}
2525
}
26+
const auth = authStore.checkForBasicAuth(id);
2627
const client = axios.create({
27-
withCredentials: true,
28+
auth,
29+
withCredentials: !auth,
2830
timeout: 15000,
2931
headers: {
3032
'Content-Type': 'application/json',

src/features/auth/hooks/useClusterInstanceSignIn.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { InstanceClientIdConfig, InstanceTypeConfig } from '@/config/instanceCli
33
import { currentUserQueryKey } from '@/features/auth/queries/getCurrentUser';
44
import { authStore, OverallAppSignIn } from '@/features/auth/store/authStore';
55
import { useInstanceLoginMutation } from '@/features/instance/operations/mutations/useInstanceLoginMutation';
6-
import { getInstanceUserInfo } from '@/features/instance/operations/queries/getInstanceUserInfo';
76
import { UsernameSignInSchema } from '@/features/instance/operations/schemas/signInSchema';
87
import { SchemaHdbInstance } from '@/lib/api.gen';
98
import { Cluster } from '@/lib/api.patch';
@@ -36,9 +35,8 @@ export function useClusterInstanceSignIn({
3635
...instanceParams,
3736
},
3837
{
39-
onSuccess: async (response) => {
40-
toast.success(response.message);
41-
const user = await getInstanceUserInfo(instanceParams);
38+
onSuccess: async ({ message, user }) => {
39+
toast.success(message);
4240
// If we sign in to the cluster, we've authenticated against all of its instances too.
4341
if (cluster?.instances?.length && !instance) {
4442
for (const clusterInstance of cluster.instances) {

src/features/auth/hooks/useLogout.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { isLocalStudio } from '@/config/constants';
33
import { InstanceClientConfig } from '@/config/instanceClientConfig';
44
import { useInstanceClient } from '@/config/useInstanceClient';
55
import { logoutOnSuccess } from '@/features/auth/handlers/logoutOnSuccess';
6-
import { authStore } from '@/features/auth/store/authStore';
6+
import { authStore, OverallAppSignIn } from '@/features/auth/store/authStore';
77
import { onInstanceLogoutSubmit } from '@/features/instance/operations/mutations/onInstanceLogoutSubmit';
88
import { useMutation } from '@tanstack/react-query';
99

1010
async function onLogoutSubmit(instanceClientConfig: InstanceClientConfig) {
1111
await authStore.signOutFromPotentiallyAuthenticatedInstances();
1212
if (isLocalStudio) {
13-
await onInstanceLogoutSubmit(instanceClientConfig);
13+
await onInstanceLogoutSubmit({ ...instanceClientConfig, entityId: OverallAppSignIn });
1414
} else {
1515
await apiClient.post('/Logout/');
1616
}

src/features/auth/store/authStore.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ class AuthStore {
4343
private readonly broadListeners: Array<(connection: AuthenticatedConnection, id: EntityIds) => void> = [];
4444
private readonly specificListeners: Record<EntityIds, Array<(connection: AuthenticatedConnection, id: EntityIds) => void>> = {};
4545

46-
private readonly localStorageKey = 'Studio:PotentiallyAuthenticated';
46+
private readonly potentiallyAuthenticatedKey = 'Studio:PotentiallyAuthenticated';
47+
private readonly basicAuthKeyPrefix = 'Studio:BasicAuth:';
4748
private readonly potentiallyAuthenticated: Record<EntityIds, AuthenticatedConnectionKey>;
4849
private readonly checkedAuthentication: Record<EntityIds, boolean> = {};
4950
private readonly allConnections: Record<EntityIds, AuthenticatedConnection> = {};
5051

5152
constructor() {
52-
this.potentiallyAuthenticated = JSON.parse(localStorage.getItem(this.localStorageKey) || '{}');
53+
this.potentiallyAuthenticated = JSON.parse(localStorage.getItem(this.potentiallyAuthenticatedKey) || '{}');
5354
}
5455

5556
public getAllConnections(): Record<EntityIds, AuthenticatedConnection> {
@@ -152,6 +153,19 @@ class AuthStore {
152153
return undefined;
153154
}
154155

156+
public flagForBasicAuth(id: EntityIds, credentials: null | { username: string; password: string; }) {
157+
if (credentials === null) {
158+
localStorage.removeItem(this.basicAuthKeyPrefix + id);
159+
} else {
160+
localStorage.setItem(this.basicAuthKeyPrefix + id, btoa(JSON.stringify(credentials)));
161+
}
162+
}
163+
164+
public checkForBasicAuth(id: EntityIds): undefined | { username: string; password: string; } {
165+
const value = localStorage.getItem(this.basicAuthKeyPrefix + id);
166+
return value ? JSON.parse(atob(value)) : undefined;
167+
}
168+
155169
public async signOutFromPotentiallyAuthenticatedInstances() {
156170
for (const entityId in this.potentiallyAuthenticated) {
157171
this.allConnections[entityId].user = null;
@@ -162,7 +176,7 @@ class AuthStore {
162176
}
163177
try {
164178
const instanceClient = getInstanceClient({ id: entityId });
165-
await onInstanceLogoutSubmit({ instanceClient });
179+
await onInstanceLogoutSubmit({ entityId, instanceClient });
166180
} catch (err: unknown) {
167181
console.error(`Failed to log out from ${entityId}, carrying on`, err);
168182
}
@@ -202,14 +216,14 @@ class AuthStore {
202216
private flagKeyAsSignedIn(id: EntityIds, key: AuthenticatedConnectionKey) {
203217
if (this.potentiallyAuthenticated[id] !== key) {
204218
this.potentiallyAuthenticated[id] = key;
205-
localStorage.setItem(this.localStorageKey, JSON.stringify(this.potentiallyAuthenticated));
219+
localStorage.setItem(this.potentiallyAuthenticatedKey, JSON.stringify(this.potentiallyAuthenticated));
206220
}
207221
}
208222

209223
private flagKeyAsSignedOut(id: EntityIds) {
210224
if (this.potentiallyAuthenticated[id]) {
211225
delete this.potentiallyAuthenticated[id];
212-
localStorage.setItem(this.localStorageKey, JSON.stringify(this.potentiallyAuthenticated));
226+
localStorage.setItem(this.potentiallyAuthenticatedKey, JSON.stringify(this.potentiallyAuthenticated));
213227
}
214228
}
215229

src/features/cluster/FinishSetup.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { authStore } from '@/features/auth/store/authStore';
1313
import {
1414
useInstanceResetPasswordMutation,
1515
} from '@/features/instance/operations/mutations/useInstanceResetPasswordMutation';
16-
import { getInstanceUserInfo } from '@/features/instance/operations/queries/getInstanceUserInfo';
1716
import { AddUserFormSchema } from '@/features/instance/operations/schemas/addUserFormSchema';
1817
import { useCloudAuth } from '@/hooks/useAuth';
1918
import { getOperationsUrlForCluster } from '@/lib/urls/getOperationsUrlForCluster';
@@ -73,10 +72,9 @@ export function FinishSetup() {
7372
initialUsername: defaultClusterUsername,
7473
desiredUsername: formData.username,
7574
}, {
76-
onSuccess: async (response) => {
77-
toast.success(response.message);
78-
const user = await getInstanceUserInfo({ instanceClient });
79-
authStore.setUserForEntity(cluster || null, user);
75+
onSuccess: async ({ message, user }) => {
76+
toast.success(message);
77+
authStore.setUserForEntity(cluster!, user);
8078
void router.invalidate();
8179
await navigate({ to: redirect?.startsWith('/') ? redirect : defaultInstanceRouteUpOne });
8280
},

src/features/cluster/InstanceLogInCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function InstanceLogInCell({ instance }: { readonly instance: Instance })
1515
const operationsUrl = useMemo(() => getOperationsUrlForInstance(instance), [instance]);
1616
const instanceClient = useInstanceClient(operationsUrl);
1717
const onSignOutClick = useCallback(async () => {
18-
await onInstanceLogoutSubmit({ instanceClient });
18+
await onInstanceLogoutSubmit({ instanceClient, entityId: instance.id });
1919
authStore.setUserForEntity(instance, null);
2020
}, [instance, instanceClient]);
2121

src/features/clusters/components/ClusterCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function ClusterCard({ cluster }: { cluster: Cluster; }) {
5555
console.error('Failed to lookup cluster details, proceeding without checking instances.', err);
5656
return null;
5757
});
58-
await onInstanceLogoutSubmit({ instanceClient });
58+
await onInstanceLogoutSubmit({ entityId: cluster.id, instanceClient });
5959
if (fullCluster?.instances?.length) {
6060
// Flag all cluster instances as signed out as well.
6161
for (const instance of fullCluster.instances) {

src/features/instance/operations/mutations/onInstanceLogoutSubmit.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { InstanceClientConfig } from '@/config/instanceClientConfig';
1+
import { InstanceClientIdConfig } from '@/config/instanceClientConfig';
2+
import { authStore } from '@/features/auth/store/authStore';
23

34
interface LogoutInfoResponse {
45
message: string;
56
}
67

7-
export async function onInstanceLogoutSubmit({ instanceClient }: InstanceClientConfig): Promise<LogoutInfoResponse> {
8+
export async function onInstanceLogoutSubmit({ instanceClient, entityId }: InstanceClientIdConfig): Promise<LogoutInfoResponse> {
89
const { data } = await instanceClient.post('/', {
910
operation: 'logout',
1011
});
12+
authStore.flagForBasicAuth(entityId, null);
1113
if (data) {
1214
return data;
1315
} else {

src/features/instance/operations/mutations/useInstanceLoginMutation.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,53 @@
1-
import { InstanceClientConfig } from '@/config/instanceClientConfig';
1+
import { InstanceClientIdConfig } from '@/config/instanceClientConfig';
2+
import { authStore } from '@/features/auth/store/authStore';
3+
import { getInstanceUserInfo } from '@/features/instance/operations/queries/getInstanceUserInfo';
4+
import { LocalUser } from '@/lib/api.patch';
25
import { useMutation } from '@tanstack/react-query';
36

4-
interface InstanceLoginCredentials extends InstanceClientConfig {
7+
interface InstanceLoginCredentials extends InstanceClientIdConfig {
58
username: string;
69
password: string;
710
}
811

912
export interface LoginInfoResponse {
1013
message: string;
14+
user: LocalUser;
1115
}
1216

1317
export async function onInstanceLoginSubmit({
1418
username,
1519
password,
1620
instanceClient,
21+
entityId,
1722
}: InstanceLoginCredentials): Promise<LoginInfoResponse> {
18-
const { data } = await instanceClient.post('/', {
23+
const { data: { message } } = await instanceClient.post('/', {
1924
operation: 'login',
2025
username,
2126
password,
2227
});
23-
if (data) {
24-
return data;
25-
} else {
26-
throw new Error('Something went wrong');
28+
29+
// Attempt to use the login with session storage only.
30+
try {
31+
const user = await getInstanceUserInfo({ instanceClient });
32+
return {
33+
message,
34+
user,
35+
};
36+
} catch (err) {
37+
console.error('Failed to get user after login, trying basic auth', err);
38+
39+
const user = await getInstanceUserInfo({
40+
instanceClient,
41+
auth: {
42+
username,
43+
password,
44+
},
45+
});
46+
authStore.flagForBasicAuth(entityId, { username, password });
47+
return {
48+
message,
49+
user,
50+
};
2751
}
2852
}
2953

src/features/instance/operations/mutations/useInstanceResetPasswordMutation.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ async function onInstanceResetPassword({
3434
username: initialUsername,
3535
password: tempPassword,
3636
instanceClient,
37+
entityId: clusterId,
3738
});
38-
// then create a new user
39+
// then alter or create a new user
3940
if (desiredUsername === defaultClusterUsername) {
4041
await onAlterUser({
4142
username: desiredUsername,
@@ -58,6 +59,7 @@ async function onInstanceResetPassword({
5859
username: desiredUsername,
5960
password: newPassword,
6061
instanceClient,
62+
entityId: clusterId,
6163
});
6264
}
6365
// and finally, tell the central manager that we changed their password.
@@ -66,6 +68,7 @@ async function onInstanceResetPassword({
6668
} catch (err) {
6769
// If something went wrong, logout as well.
6870
await onInstanceLogoutSubmit({
71+
entityId: clusterId,
6972
instanceClient,
7073
});
7174
throw err;

0 commit comments

Comments
 (0)