Skip to content

Commit 5be4b12

Browse files
chore(site): refactor AuthProvider to not use authXService (#10184)
* Move xstate transitions to provider * Centrlize auth logic in the provider * Remove actor * Remove auth xservice * Add loader while AuthProvider is loading * Simplify and fix a few computed states * Add a few replaces * Fix logout * Remove unused import * Fix RequireAuth test * Fix wait loader * Fix tests * Wrap signout with callback
1 parent 7c66878 commit 5be4b12

28 files changed

+422
-769
lines changed

site/src/api/api.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,9 @@ export const logout = async (): Promise<void> => {
120120
await axios.post("/api/v2/users/logout");
121121
};
122122

123-
export const getAuthenticatedUser = async (): Promise<
124-
TypesGen.User | undefined
125-
> => {
126-
try {
127-
const response = await axios.get<TypesGen.User>("/api/v2/users/me");
128-
return response.data;
129-
} catch (error) {
130-
if (axios.isAxiosError(error) && error.response?.status === 401) {
131-
return undefined;
132-
}
133-
134-
throw error;
135-
}
123+
export const getAuthenticatedUser = async () => {
124+
const response = await axios.get<TypesGen.User>("/api/v2/users/me");
125+
return response.data;
136126
};
137127

138128
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {

site/src/api/queries/authCheck.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AuthorizationRequest } from "api/typesGenerated";
2+
import * as API from "api/api";
3+
4+
export const AUTHORIZATION_KEY = "authorization";
5+
6+
export const getAuthorizationKey = (req: AuthorizationRequest) =>
7+
[AUTHORIZATION_KEY, req] as const;
8+
9+
export const checkAuthorization = (req: AuthorizationRequest) => {
10+
return {
11+
queryKey: getAuthorizationKey(req),
12+
queryFn: () => API.checkAuthorization(req),
13+
};
14+
};

site/src/api/queries/users.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { QueryClient, QueryOptions } from "react-query";
22
import * as API from "api/api";
33
import {
4+
AuthorizationRequest,
45
GetUsersResponse,
56
UpdateUserPasswordRequest,
7+
UpdateUserProfileRequest,
8+
User,
69
UsersRequest,
710
} from "api/typesGenerated";
11+
import { getMetadataAsJSON } from "utils/metadata";
12+
import { getAuthorizationKey } from "./authCheck";
813

914
export const users = (req: UsersRequest): QueryOptions<GetUsersResponse> => {
1015
return {
@@ -83,3 +88,76 @@ export const authMethods = () => {
8388
queryFn: API.getAuthMethods,
8489
};
8590
};
91+
92+
export const me = () => {
93+
return {
94+
queryKey: ["me"],
95+
queryFn: async () =>
96+
getMetadataAsJSON<User>("user") ?? API.getAuthenticatedUser(),
97+
};
98+
};
99+
100+
export const hasFirstUser = () => {
101+
return {
102+
queryKey: ["hasFirstUser"],
103+
queryFn: API.hasFirstUser,
104+
};
105+
};
106+
107+
export const login = (
108+
authorization: AuthorizationRequest,
109+
queryClient: QueryClient,
110+
) => {
111+
return {
112+
mutationFn: async (credentials: { email: string; password: string }) =>
113+
loginFn({ ...credentials, authorization }),
114+
onSuccess: async (data: Awaited<ReturnType<typeof loginFn>>) => {
115+
queryClient.setQueryData(["me"], data.user);
116+
queryClient.setQueryData(
117+
getAuthorizationKey(authorization),
118+
data.permissions,
119+
);
120+
},
121+
};
122+
};
123+
124+
const loginFn = async ({
125+
email,
126+
password,
127+
authorization,
128+
}: {
129+
email: string;
130+
password: string;
131+
authorization: AuthorizationRequest;
132+
}) => {
133+
await API.login(email, password);
134+
const [user, permissions] = await Promise.all([
135+
API.getAuthenticatedUser(),
136+
API.checkAuthorization(authorization),
137+
]);
138+
return {
139+
user,
140+
permissions,
141+
};
142+
};
143+
144+
export const logout = (queryClient: QueryClient) => {
145+
return {
146+
mutationFn: API.logout,
147+
onSuccess: () => {
148+
queryClient.removeQueries();
149+
},
150+
};
151+
};
152+
153+
export const updateProfile = () => {
154+
return {
155+
mutationFn: ({
156+
userId,
157+
req,
158+
}: {
159+
userId: string;
160+
req: UpdateUserProfileRequest;
161+
}) => API.updateProfile(userId, req),
162+
};
163+
};
Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,133 @@
1-
import { useActor, useInterpret } from "@xstate/react";
2-
import { createContext, FC, PropsWithChildren, useContext } from "react";
3-
import { authMachine } from "xServices/auth/authXService";
4-
import { ActorRefFrom } from "xstate";
1+
import { checkAuthorization } from "api/queries/authCheck";
2+
import {
3+
authMethods,
4+
hasFirstUser,
5+
login,
6+
logout,
7+
me,
8+
updateProfile as updateProfileOptions,
9+
} from "api/queries/users";
10+
import {
11+
AuthMethods,
12+
UpdateUserProfileRequest,
13+
User,
14+
} from "api/typesGenerated";
15+
import {
16+
createContext,
17+
FC,
18+
PropsWithChildren,
19+
useCallback,
20+
useContext,
21+
} from "react";
22+
import { useMutation, useQuery, useQueryClient } from "react-query";
23+
import { permissionsToCheck, Permissions } from "./permissions";
24+
import { displaySuccess } from "components/GlobalSnackbar/utils";
25+
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
26+
import { isApiError } from "api/errors";
527

6-
interface AuthContextValue {
7-
authService: ActorRefFrom<typeof authMachine>;
8-
}
28+
type AuthContextValue = {
29+
isSignedOut: boolean;
30+
isSigningOut: boolean;
31+
isConfiguringTheFirstUser: boolean;
32+
isSignedIn: boolean;
33+
isSigningIn: boolean;
34+
isUpdatingProfile: boolean;
35+
user: User | undefined;
36+
permissions: Permissions | undefined;
37+
authMethods: AuthMethods | undefined;
38+
signInError: unknown;
39+
updateProfileError: unknown;
40+
signOut: () => void;
41+
signIn: (email: string, password: string) => Promise<void>;
42+
updateProfile: (data: UpdateUserProfileRequest) => void;
43+
};
944

1045
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
1146

1247
export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
13-
const authService = useInterpret(authMachine);
48+
const meOptions = me();
49+
const userQuery = useQuery(meOptions);
50+
const authMethodsQuery = useQuery(authMethods());
51+
const hasFirstUserQuery = useQuery(hasFirstUser());
52+
const permissionsQuery = useQuery({
53+
...checkAuthorization({ checks: permissionsToCheck }),
54+
enabled: userQuery.data !== undefined,
55+
});
56+
57+
const queryClient = useQueryClient();
58+
const loginMutation = useMutation(
59+
login({ checks: permissionsToCheck }, queryClient),
60+
);
61+
const logoutMutation = useMutation(logout(queryClient));
62+
const updateProfileMutation = useMutation({
63+
...updateProfileOptions(),
64+
onSuccess: (user) => {
65+
queryClient.setQueryData(meOptions.queryKey, user);
66+
displaySuccess("Updated settings.");
67+
},
68+
});
69+
70+
const isSignedOut =
71+
userQuery.isError &&
72+
isApiError(userQuery.error) &&
73+
userQuery.error.response.status === 401;
74+
const isSigningOut = logoutMutation.isLoading;
75+
const isLoading =
76+
authMethodsQuery.isLoading ||
77+
userQuery.isLoading ||
78+
hasFirstUserQuery.isLoading ||
79+
(userQuery.isSuccess && permissionsQuery.isLoading);
80+
const isConfiguringTheFirstUser = !hasFirstUserQuery.data;
81+
const isSignedIn = userQuery.isSuccess && userQuery.data !== undefined;
82+
const isSigningIn = loginMutation.isLoading;
83+
const isUpdatingProfile = updateProfileMutation.isLoading;
84+
85+
const signOut = useCallback(() => {
86+
logoutMutation.mutate();
87+
}, [logoutMutation]);
88+
89+
const signIn = async (email: string, password: string) => {
90+
await loginMutation.mutateAsync({ email, password });
91+
};
92+
93+
const updateProfile = (req: UpdateUserProfileRequest) => {
94+
updateProfileMutation.mutate({ userId: userQuery.data!.id, req });
95+
};
96+
97+
if (isLoading) {
98+
return <FullScreenLoader />;
99+
}
14100

15101
return (
16-
<AuthContext.Provider value={{ authService }}>
102+
<AuthContext.Provider
103+
value={{
104+
isSignedOut,
105+
isSigningOut,
106+
isConfiguringTheFirstUser,
107+
isSignedIn,
108+
isSigningIn,
109+
isUpdatingProfile,
110+
signOut,
111+
signIn,
112+
updateProfile,
113+
user: userQuery.data,
114+
permissions: permissionsQuery.data as Permissions | undefined,
115+
authMethods: authMethodsQuery.data,
116+
signInError: loginMutation.error,
117+
updateProfileError: updateProfileMutation.error,
118+
}}
119+
>
17120
{children}
18121
</AuthContext.Provider>
19122
);
20123
};
21124

22-
type UseAuthReturnType = ReturnType<
23-
typeof useActor<AuthContextValue["authService"]>
24-
>;
25-
26-
export const useAuth = (): UseAuthReturnType => {
125+
export const useAuth = () => {
27126
const context = useContext(AuthContext);
28127

29128
if (!context) {
30129
throw new Error("useAuth should be used inside of <AuthProvider />");
31130
}
32131

33-
const auth = useActor(context.authService);
34-
35-
return auth;
132+
return context;
36133
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
export const checks = {
2+
readAllUsers: "readAllUsers",
3+
updateUsers: "updateUsers",
4+
createUser: "createUser",
5+
createTemplates: "createTemplates",
6+
updateTemplates: "updateTemplates",
7+
deleteTemplates: "deleteTemplates",
8+
viewAuditLog: "viewAuditLog",
9+
viewDeploymentValues: "viewDeploymentValues",
10+
createGroup: "createGroup",
11+
viewUpdateCheck: "viewUpdateCheck",
12+
viewExternalAuthConfig: "viewExternalAuthConfig",
13+
viewDeploymentStats: "viewDeploymentStats",
14+
editWorkspaceProxies: "editWorkspaceProxies",
15+
} as const;
16+
17+
export const permissionsToCheck = {
18+
[checks.readAllUsers]: {
19+
object: {
20+
resource_type: "user",
21+
},
22+
action: "read",
23+
},
24+
[checks.updateUsers]: {
25+
object: {
26+
resource_type: "user",
27+
},
28+
action: "update",
29+
},
30+
[checks.createUser]: {
31+
object: {
32+
resource_type: "user",
33+
},
34+
action: "create",
35+
},
36+
[checks.createTemplates]: {
37+
object: {
38+
resource_type: "template",
39+
},
40+
action: "update",
41+
},
42+
[checks.updateTemplates]: {
43+
object: {
44+
resource_type: "template",
45+
},
46+
action: "update",
47+
},
48+
[checks.deleteTemplates]: {
49+
object: {
50+
resource_type: "template",
51+
},
52+
action: "delete",
53+
},
54+
[checks.viewAuditLog]: {
55+
object: {
56+
resource_type: "audit_log",
57+
},
58+
action: "read",
59+
},
60+
[checks.viewDeploymentValues]: {
61+
object: {
62+
resource_type: "deployment_config",
63+
},
64+
action: "read",
65+
},
66+
[checks.createGroup]: {
67+
object: {
68+
resource_type: "group",
69+
},
70+
action: "create",
71+
},
72+
[checks.viewUpdateCheck]: {
73+
object: {
74+
resource_type: "deployment_config",
75+
},
76+
action: "read",
77+
},
78+
[checks.viewExternalAuthConfig]: {
79+
object: {
80+
resource_type: "deployment_config",
81+
},
82+
action: "read",
83+
},
84+
[checks.viewDeploymentStats]: {
85+
object: {
86+
resource_type: "deployment_stats",
87+
},
88+
action: "read",
89+
},
90+
[checks.editWorkspaceProxies]: {
91+
object: {
92+
resource_type: "workspace_proxy",
93+
},
94+
action: "create",
95+
},
96+
} as const;
97+
98+
export type Permissions = Record<keyof typeof permissionsToCheck, boolean>;

0 commit comments

Comments
 (0)