Skip to content

Commit 8fd9c93

Browse files
use errore for better auth client methods
Wrap auth and organization Better Auth client calls in typed helpers that return errors as values, and update form actions to handle them consistently.
1 parent 457cb2d commit 8fd9c93

11 files changed

Lines changed: 208 additions & 53 deletions

File tree

apps/dashboard/app/lib/auth/error.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import { z } from "zod";
55

66
import type { FormActionError } from "~/components/action-data-context";
77

8+
export class AuthClientFetchError extends errore.createTaggedError({
9+
name: "AuthClientFetchError",
10+
message: "$operation request failed",
11+
}) {}
12+
13+
export class AuthError extends errore.createTaggedError({
14+
name: "AuthError",
15+
message: "$operation failed",
16+
}) {}
17+
818
export class SessionError extends errore.createTaggedError({
919
name: "SessionError",
1020
message: "Session $operation failed",
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
11
import { isDefined } from "@lite-app/shared/is-defined";
22
import { href } from "react-router";
33

4-
import { organization } from "~/lib/auth";
54
import { isAdmin } from "~/lib/auth/session";
5+
import { getFullOrganization, listOrganizations } from "~/lib/organization";
66

77
export async function getAuthenticatedRedirectHref() {
88
const [admin, activeOrganization, organizations] = await Promise.all([
99
isAdmin(),
10-
organization.getFullOrganization(),
11-
organization.list(),
10+
getFullOrganization(),
11+
listOrganizations(),
1212
]);
13-
if (!isDefined(activeOrganization.data)) {
14-
if (!admin || (organizations.data ?? []).length > 0) {
13+
if (activeOrganization instanceof Error) {
14+
console.error(activeOrganization);
15+
throw activeOrganization;
16+
}
17+
if (organizations instanceof Error) {
18+
console.error(organizations);
19+
throw organizations;
20+
}
21+
if (!isDefined(activeOrganization)) {
22+
if (!admin || (organizations ?? []).length > 0) {
1523
return href("/organization/list");
1624
}
1725
return href("/organization/create");
1826
}
1927
return href(`/organization/:slug`, {
20-
slug: activeOrganization.data.slug,
28+
slug: activeOrganization.slug,
2129
});
2230
}
Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,82 @@
1+
import { isDefined } from "@lite-app/shared/is-defined";
12
import { createAuthClient } from "better-auth/client";
23
import { adminClient, organizationClient } from "better-auth/client/plugins";
34

5+
import { AuthClientFetchError, AuthError } from "~/lib/auth/error";
6+
47
export const authClient = createAuthClient({
58
plugins: [adminClient(), organizationClient()],
69
});
7-
export const { organization } = authClient;
10+
11+
async function unwrapAuthClientResult<T>({
12+
operation,
13+
promise,
14+
}: {
15+
operation: string;
16+
promise: Promise<{ data: T; error: unknown }>;
17+
}) {
18+
const result = await promise.catch(
19+
(error) =>
20+
new AuthClientFetchError({
21+
operation,
22+
cause: error,
23+
})
24+
);
25+
if (result instanceof Error) {
26+
return result;
27+
}
28+
const { data, error } = result;
29+
const success = isDefined(data) && !isDefined(error);
30+
if (!success) {
31+
return new AuthError({
32+
operation,
33+
cause: error,
34+
});
35+
}
36+
return data;
37+
}
38+
39+
export function getSession(
40+
...params: Parameters<typeof authClient.getSession>
41+
) {
42+
return unwrapAuthClientResult({
43+
operation: "get session",
44+
promise: authClient.getSession(...params),
45+
});
46+
}
47+
48+
export function signUpEmail(
49+
...params: Parameters<typeof authClient.signUp.email>
50+
) {
51+
return unwrapAuthClientResult({
52+
operation: "sign up",
53+
promise: authClient.signUp.email(...params),
54+
});
55+
}
56+
57+
export function signInEmail(
58+
...params: Parameters<typeof authClient.signIn.email>
59+
) {
60+
return unwrapAuthClientResult({
61+
operation: "sign in",
62+
promise: authClient.signIn.email(...params),
63+
});
64+
}
65+
66+
export function requestPasswordReset(
67+
...params: Parameters<typeof authClient.requestPasswordReset>
68+
) {
69+
return unwrapAuthClientResult({
70+
operation: "request password reset",
71+
promise: authClient.requestPasswordReset(...params),
72+
});
73+
}
74+
75+
export function resetPassword(
76+
...params: Parameters<typeof authClient.resetPassword>
77+
) {
78+
return unwrapAuthClientResult({
79+
operation: "reset password",
80+
promise: authClient.resetPassword(...params),
81+
});
82+
}
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { isDefined } from "@lite-app/shared/is-defined";
22

3-
import { authClient } from "~/lib/auth";
3+
import { getSession } from "~/lib/auth";
44
import { ADMIN_ROLE } from "~/lib/auth/roles";
55

66
export async function isAdmin() {
7-
const session = await authClient.getSession();
8-
if (!isDefined(session.data)) {
7+
const session = await getSession();
8+
if (session instanceof Error) {
9+
console.error(session);
910
return false;
1011
}
11-
return session.data.user.role === ADMIN_ROLE;
12+
if (!isDefined(session)) {
13+
return false;
14+
}
15+
return session.user.role === ADMIN_ROLE;
1216
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { isDefined } from "@lite-app/shared/is-defined";
2+
3+
import { authClient } from "~/lib/auth";
4+
import { AuthClientFetchError } from "~/lib/auth/error";
5+
import { OrganizationError } from "~/lib/organization/error";
6+
7+
async function unwrapOrganizationClientResult<T>({
8+
operation,
9+
promise,
10+
}: {
11+
operation: string;
12+
promise: Promise<{ data: T; error: unknown }>;
13+
}) {
14+
const result = await promise.catch(
15+
(error) =>
16+
new AuthClientFetchError({
17+
operation,
18+
cause: error,
19+
})
20+
);
21+
if (result instanceof Error) {
22+
return result;
23+
}
24+
const { data, error } = result;
25+
if (isDefined(error) || !isDefined(data)) {
26+
return new OrganizationError({
27+
operation,
28+
cause: error,
29+
});
30+
}
31+
return data;
32+
}
33+
34+
export function getFullOrganization(
35+
...params: Parameters<typeof authClient.organization.getFullOrganization>
36+
) {
37+
return unwrapOrganizationClientResult({
38+
operation: "get full organization",
39+
promise: authClient.organization.getFullOrganization(...params),
40+
});
41+
}
42+
43+
export function listOrganizations(
44+
...params: Parameters<typeof authClient.organization.list>
45+
) {
46+
return unwrapOrganizationClientResult({
47+
operation: "list organizations",
48+
promise: authClient.organization.list(...params),
49+
});
50+
}
51+
52+
export function createOrganization(
53+
...params: Parameters<typeof authClient.organization.create>
54+
) {
55+
return unwrapOrganizationClientResult({
56+
operation: "create organization",
57+
promise: authClient.organization.create(...params),
58+
});
59+
}
60+
61+
export function inviteMember(
62+
...params: Parameters<typeof authClient.organization.inviteMember>
63+
) {
64+
return unwrapOrganizationClientResult({
65+
operation: "invite member",
66+
promise: authClient.organization.inviteMember(...params),
67+
});
68+
}

apps/dashboard/app/routes/_auth.request-password-reset/route.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { withMinimumDelay } from "@lite-app/shared/delay";
2-
import { isDefined } from "@lite-app/shared/is-defined";
32
import { Card } from "@lite-app/ui/components/card";
43
import { FieldError } from "@lite-app/ui/components/field-error";
54
import { Input } from "@lite-app/ui/components/input";
@@ -29,7 +28,7 @@ import {
2928
} from "~/components/form-card";
3029
import { FormFields } from "~/components/form-fields";
3130
import { SubmitButton } from "~/components/submit-button";
32-
import { authClient } from "~/lib/auth";
31+
import { requestPasswordReset } from "~/lib/auth";
3332
import { mapAuthErrorToFormActionError } from "~/lib/auth/error";
3433
import { parseFormData } from "~/lib/form/form-data";
3534

@@ -52,25 +51,24 @@ export async function clientAction({
5251
}
5352
const { email } = formData;
5453

55-
const { data, error } = await withMinimumDelay(
56-
authClient.requestPasswordReset({
54+
const passwordResetRequest = await withMinimumDelay(
55+
requestPasswordReset({
5756
email,
5857
redirectTo: href("/reset-password"),
5958
})
6059
);
61-
const success = isDefined(data) && !isDefined(error);
6260

63-
if (!success) {
61+
if (passwordResetRequest instanceof Error) {
6462
return {
6563
status: "error",
66-
error: mapAuthErrorToFormActionError(error),
64+
error: mapAuthErrorToFormActionError(passwordResetRequest.cause),
6765
};
6866
}
6967
return {
7068
status: "success",
7169
success: {
7270
type: "alert",
73-
title: data.message,
71+
title: passwordResetRequest.message,
7472
},
7573
};
7674
}

apps/dashboard/app/routes/_auth.reset-password/route.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
} from "~/components/form-card";
3030
import { FormFields } from "~/components/form-fields";
3131
import { SubmitButton } from "~/components/submit-button";
32-
import { authClient } from "~/lib/auth";
32+
import { resetPassword } from "~/lib/auth";
3333
import {
3434
comparePasswords,
3535
mapAuthErrorToFormActionError,
@@ -73,18 +73,17 @@ export async function clientAction({
7373
};
7474
}
7575

76-
const { error } = await withMinimumDelay(
77-
authClient.resetPassword({
76+
const passwordReset = await withMinimumDelay(
77+
resetPassword({
7878
newPassword: password,
7979
...(isDefined(token) && { token }),
8080
})
8181
);
82-
const success = !isDefined(error);
8382

84-
if (!success) {
83+
if (passwordReset instanceof Error) {
8584
return {
8685
status: "error",
87-
error: mapAuthErrorToFormActionError(error),
86+
error: mapAuthErrorToFormActionError(passwordReset.cause),
8887
};
8988
}
9089
throw redirect(href("/signin"));

apps/dashboard/app/routes/_auth.signin/route.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { withMinimumDelay } from "@lite-app/shared/delay";
2-
import { isDefined } from "@lite-app/shared/is-defined";
32
import { Card } from "@lite-app/ui/components/card";
43
import { FieldError } from "@lite-app/ui/components/field-error";
54
import { Input } from "@lite-app/ui/components/input";
@@ -28,7 +27,7 @@ import {
2827
} from "~/components/form-card";
2928
import { FormFields } from "~/components/form-fields";
3029
import { SubmitButton } from "~/components/submit-button";
31-
import { authClient } from "~/lib/auth";
30+
import { signInEmail } from "~/lib/auth";
3231
import { mapAuthErrorToFormActionError } from "~/lib/auth/error";
3332
import { getAuthenticatedRedirectHref } from "~/lib/auth/href";
3433
import { parseFormData } from "~/lib/form/form-data";
@@ -53,18 +52,17 @@ export async function clientAction({
5352
}
5453
const { email, password } = formData;
5554

56-
const { error } = await withMinimumDelay(
57-
authClient.signIn.email({
55+
const session = await withMinimumDelay(
56+
signInEmail({
5857
email,
5958
password,
6059
})
6160
);
62-
const success = !isDefined(error);
6361

64-
if (!success) {
62+
if (session instanceof Error) {
6563
return {
6664
status: "error",
67-
error: mapAuthErrorToFormActionError(error),
65+
error: mapAuthErrorToFormActionError(session.cause),
6866
};
6967
}
7068
throw redirectDocument(await getAuthenticatedRedirectHref());

apps/dashboard/app/routes/_auth.signup/route.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { withMinimumDelay } from "@lite-app/shared/delay";
2-
import { isDefined } from "@lite-app/shared/is-defined";
32
import { Card } from "@lite-app/ui/components/card";
43
import { FieldError } from "@lite-app/ui/components/field-error";
54
import { Input } from "@lite-app/ui/components/input";
@@ -27,7 +26,7 @@ import {
2726
} from "~/components/form-card";
2827
import { FormFields } from "~/components/form-fields";
2928
import { SubmitButton } from "~/components/submit-button";
30-
import { authClient } from "~/lib/auth";
29+
import { signUpEmail } from "~/lib/auth";
3130
import {
3231
comparePasswords,
3332
mapAuthErrorToFormActionError,
@@ -84,20 +83,19 @@ export async function clientAction({
8483
};
8584
}
8685

87-
const { error } = await withMinimumDelay(
88-
authClient.signUp.email({
86+
const account = await withMinimumDelay(
87+
signUpEmail({
8988
name,
9089
email,
9190
password,
9291
image: pickAvatar(),
9392
})
9493
);
95-
const success = !isDefined(error);
9694

97-
if (!success) {
95+
if (account instanceof Error) {
9896
return {
9997
status: "error",
100-
error: mapAuthErrorToFormActionError(error),
98+
error: mapAuthErrorToFormActionError(account.cause),
10199
};
102100
}
103101
throw redirect(href("/organization/create"));

0 commit comments

Comments
 (0)