mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20:27:44 +00:00
feat: one tap (#419)
This commit is contained in:
@@ -3,8 +3,14 @@
|
|||||||
import SignIn from "@/components/sign-in";
|
import SignIn from "@/components/sign-in";
|
||||||
import { SignUp } from "@/components/sign-up";
|
import { SignUp } from "@/components/sign-up";
|
||||||
import { Tabs } from "@/components/ui/tabs2";
|
import { Tabs } from "@/components/ui/tabs2";
|
||||||
|
import { client } from "@/lib/auth-client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
useEffect(() => {
|
||||||
|
client.oneTap();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center flex-col justify-center w-full md:py-10">
|
<div className="flex items-center flex-col justify-center w-full md:py-10">
|
||||||
|
|||||||
@@ -76,16 +76,21 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
const { data: users, isLoading: isUsersLoading } = useQuery({
|
const { data: users, isLoading: isUsersLoading } = useQuery({
|
||||||
queryKey: ["users"],
|
queryKey: ["users"],
|
||||||
queryFn: () =>
|
queryFn: async () => {
|
||||||
client.admin
|
const data = await client.admin.listUsers(
|
||||||
.listUsers({
|
{
|
||||||
query: {
|
query: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
sortBy: "createdAt",
|
sortBy: "createdAt",
|
||||||
sortDirection: "desc",
|
sortDirection: "desc",
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
.then((res) => res.data?.users ?? []),
|
{
|
||||||
|
throw: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return data?.users || [];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateUser = async (e: React.FormEvent) => {
|
const handleCreateUser = async (e: React.FormEvent) => {
|
||||||
|
|||||||
@@ -42,10 +42,9 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { UAParser } from "ua-parser-js";
|
import { UAParser } from "ua-parser-js";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -63,14 +62,7 @@ export default function UserCard(props: {
|
|||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data, isPending, error } = useSession();
|
const { data, isPending, error } = useSession();
|
||||||
const [ua, setUa] = useState<UAParser.UAParserInstance>();
|
|
||||||
|
|
||||||
const session = data || props.session;
|
const session = data || props.session;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUa(new UAParser(session?.session.userAgent));
|
|
||||||
}, [session?.session.userAgent]);
|
|
||||||
|
|
||||||
const [isTerminating, setIsTerminating] = useState<string>();
|
const [isTerminating, setIsTerminating] = useState<string>();
|
||||||
const [isPendingTwoFa, setIsPendingTwoFa] = useState<boolean>(false);
|
const [isPendingTwoFa, setIsPendingTwoFa] = useState<boolean>(false);
|
||||||
const [twoFaPassword, setTwoFaPassword] = useState<string>("");
|
const [twoFaPassword, setTwoFaPassword] = useState<string>("");
|
||||||
@@ -668,7 +660,6 @@ function EditUserDialog(props: { session: Session | null }) {
|
|||||||
function AddPasskey() {
|
function AddPasskey() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [passkeyName, setPasskeyName] = useState("");
|
const [passkeyName, setPasskeyName] = useState("");
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleAddPasskey = async () => {
|
const handleAddPasskey = async () => {
|
||||||
|
|||||||
@@ -26,25 +26,8 @@ export default function AccountSwitcher({
|
|||||||
}: {
|
}: {
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
}) {
|
}) {
|
||||||
const { data: users } = useQuery({
|
|
||||||
queryKey: ["users"],
|
|
||||||
queryFn: async () => {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { data: currentUser } = useSession();
|
const { data: currentUser } = useSession();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleUserSelect = (user: Session) => {
|
|
||||||
// setCurrentUser(user);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddAccount = () => {
|
|
||||||
// Implement add account logic here
|
|
||||||
console.log("Add account clicked");
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export const Logo = (props: SVGProps<any>) => {
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
|
d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
|
||||||
className="fill-black dark:fill-white"
|
className="fill-black dark:fill-white"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
twoFactorClient,
|
twoFactorClient,
|
||||||
adminClient,
|
adminClient,
|
||||||
multiSessionClient,
|
multiSessionClient,
|
||||||
|
oneTapClient,
|
||||||
} from "better-auth/client/plugins";
|
} from "better-auth/client/plugins";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ export const client = createAuthClient({
|
|||||||
passkeyClient(),
|
passkeyClient(),
|
||||||
adminClient(),
|
adminClient(),
|
||||||
multiSessionClient(),
|
multiSessionClient(),
|
||||||
|
oneTapClient({
|
||||||
|
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onError(e) {
|
onError(e) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
organization,
|
organization,
|
||||||
passkey,
|
passkey,
|
||||||
twoFactor,
|
twoFactor,
|
||||||
|
oneTap,
|
||||||
} from "better-auth/plugins";
|
} from "better-auth/plugins";
|
||||||
import { reactInvitationEmail } from "./email/invitation";
|
import { reactInvitationEmail } from "./email/invitation";
|
||||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||||
@@ -120,5 +121,6 @@ export const auth = betterAuth({
|
|||||||
bearer(),
|
bearer(),
|
||||||
admin(),
|
admin(),
|
||||||
multiSession(),
|
multiSession(),
|
||||||
|
oneTap(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -759,6 +759,26 @@ export const contents: Content[] = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "One Tap",
|
||||||
|
href: "/docs/plugins/one-tap",
|
||||||
|
icon: () => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1.2em"
|
||||||
|
height="1.2em"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3.254 4.361a2.861 2.861 0 1 1 5.647.651a.75.75 0 0 0 1.461.34a4.361 4.361 0 1 0-8.495 0a.75.75 0 0 0 1.461-.34a3 3 0 0 1-.074-.651m1.63 5.335V4.26a1.26 1.26 0 0 1 2.518 0v4.077h2.464a2.573 2.573 0 0 1 2.573 2.573V13a1 1 0 0 1-1 1H4.83a1 1 0 0 1-.823-.433l-.764-1.11a1.715 1.715 0 0 1 1.097-2.66l.543-.102Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Authorization",
|
title: "Authorization",
|
||||||
group: true,
|
group: true,
|
||||||
|
|||||||
89
docs/content/docs/plugins/one-tap.mdx
Normal file
89
docs/content/docs/plugins/one-tap.mdx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
title: One Tap
|
||||||
|
description: One Tap plugin for BetterAuth
|
||||||
|
---
|
||||||
|
|
||||||
|
The One Tap plugin allows users to login with a single tap using Google's One Tap API.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step>
|
||||||
|
### Add the server Plugin
|
||||||
|
|
||||||
|
Add the One Tap plugin to your auth config.
|
||||||
|
|
||||||
|
```ts title="auth.ts"
|
||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { oneTap } from "better-auth/plugins";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
plugins: [ // [!code highlight]
|
||||||
|
oneTap(), // [!code highlight]
|
||||||
|
] // [!code highlight]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
### Add the client Plugin
|
||||||
|
|
||||||
|
Add the client plugin and Specify where the user should be redirected if they need to verify 2nd factor
|
||||||
|
|
||||||
|
```ts title="client.ts"
|
||||||
|
import { createAuthClient } from "better-auth/client"
|
||||||
|
import { oneTapClient } from "better-auth/client/plugins"
|
||||||
|
|
||||||
|
const client = createAuthClient({
|
||||||
|
plugins: [
|
||||||
|
oneTapClient({
|
||||||
|
clientId: "YOUR_CLIENT_ID"
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To make the one tap pop up appear, you can call the `oneTap` method.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await authClient.oneTap()
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the plugin will automatically redirect the user to "/" after the user has successfully logged in. It does a hard redirect, so the page will be reloaded. If you want to
|
||||||
|
avoid this, you can pass `fetchOptions` to the `oneTap` method.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
authClient.oneTap({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/dashboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want it to hard redirect to a different page, you can pass the `callbackURL` option to the `oneTap` method.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
authClient.oneTap({
|
||||||
|
callbackURL: "/dashboard"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client Options
|
||||||
|
|
||||||
|
**clientId**: The client ID of your Google One Tap API
|
||||||
|
|
||||||
|
**autoSelect**: Automatically select the first account in the list. Default is `false`
|
||||||
|
|
||||||
|
**context**: The context in which the One Tap API should be used. Default is `signin`
|
||||||
|
|
||||||
|
**cancelOnTapOutside**: Cancel the One Tap popup when the user taps outside of the popup. Default is `true`.
|
||||||
|
|
||||||
|
## Server Options
|
||||||
|
|
||||||
|
**disableSignUp**: Disable the sign up option. Default is `false`. If set to `true`, the user will only be able to sign in with an existing account.
|
||||||
@@ -67,7 +67,6 @@ export const getSession = <Option extends BetterAuthOptions>() =>
|
|||||||
|
|
||||||
const session =
|
const session =
|
||||||
await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
||||||
console.log({ session });
|
|
||||||
|
|
||||||
if (!session || session.session.expiresAt < new Date()) {
|
if (!session || session.session.expiresAt < new Date()) {
|
||||||
deleteSessionCookie(ctx);
|
deleteSessionCookie(ctx);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { APIError } from "better-call";
|
import { APIError } from "better-call";
|
||||||
import { generateCodeVerifier } from "oslo/oauth2";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createAuthEndpoint } from "../call";
|
import { createAuthEndpoint } from "../call";
|
||||||
import { setSessionCookie } from "../../cookies";
|
import { setSessionCookie } from "../../cookies";
|
||||||
import { socialProviderList } from "../../social-providers";
|
import { socialProviderList } from "../../social-providers";
|
||||||
import { createEmailVerificationToken } from "./email-verification";
|
import { createEmailVerificationToken } from "./email-verification";
|
||||||
import { generateState, logger } from "../../utils";
|
import { generateState, logger } from "../../utils";
|
||||||
import { hmac } from "../../crypto/hash";
|
|
||||||
|
|
||||||
export const signInOAuth = createAuthEndpoint(
|
export const signInOAuth = createAuthEndpoint(
|
||||||
"/sign-in/social",
|
"/sign-in/social",
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ export * from "../../plugins/generic-oauth/client";
|
|||||||
export * from "../../plugins/jwt/client";
|
export * from "../../plugins/jwt/client";
|
||||||
export * from "../../plugins/multi-session/client";
|
export * from "../../plugins/multi-session/client";
|
||||||
export * from "../../plugins/email-otp/client";
|
export * from "../../plugins/email-otp/client";
|
||||||
|
export * from "../../plugins/one-tap/client";
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export type InferActions<O extends ClientOptions> = O["plugins"] extends Array<
|
|||||||
>
|
>
|
||||||
? UnionToIntersection<
|
? UnionToIntersection<
|
||||||
Plugin extends BetterAuthClientPlugin
|
Plugin extends BetterAuthClientPlugin
|
||||||
? Plugin["getActions"] extends ($fetch: BetterFetch) => infer Actions
|
? Plugin["getActions"] extends (...args: any) => infer Actions
|
||||||
? Actions
|
? Actions
|
||||||
: {}
|
: {}
|
||||||
: {}
|
: {}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const createInternalAdapter = (
|
|||||||
},
|
},
|
||||||
createSession: async (
|
createSession: async (
|
||||||
userId: string,
|
userId: string,
|
||||||
request?: Request | Headers,
|
request: Request | Headers | undefined,
|
||||||
dontRememberMe?: boolean,
|
dontRememberMe?: boolean,
|
||||||
override?: Partial<Session> & Record<string, any>,
|
override?: Partial<Session> & Record<string, any>,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ export * from "./generic-oauth";
|
|||||||
export * from "./jwt";
|
export * from "./jwt";
|
||||||
export * from "./multi-session";
|
export * from "./multi-session";
|
||||||
export * from "./email-otp";
|
export * from "./email-otp";
|
||||||
|
export * from "./one-tap";
|
||||||
|
|||||||
132
packages/better-auth/src/plugins/one-tap/client.ts
Normal file
132
packages/better-auth/src/plugins/one-tap/client.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type { BetterFetchOption } from "@better-fetch/fetch";
|
||||||
|
import type { BetterAuthClientPlugin } from "../../types";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
google?: {
|
||||||
|
accounts: {
|
||||||
|
id: {
|
||||||
|
initialize: (config: any) => void;
|
||||||
|
prompt: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
googleScriptInitialized?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoogleOneTapOptions {
|
||||||
|
/**
|
||||||
|
* Google client ID
|
||||||
|
*/
|
||||||
|
clientId: string;
|
||||||
|
/**
|
||||||
|
* Auto select the account if the user is already signed in
|
||||||
|
*/
|
||||||
|
autoSelect?: boolean;
|
||||||
|
/**
|
||||||
|
* Cancel the flow when the user taps outside the prompt
|
||||||
|
*/
|
||||||
|
cancelOnTapOutside?: boolean;
|
||||||
|
/**
|
||||||
|
* Context of the Google One Tap flow
|
||||||
|
*/
|
||||||
|
context?: "signin" | "signup" | "use";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoogleOneTapActionOptions
|
||||||
|
extends Omit<GoogleOneTapOptions, "clientId"> {
|
||||||
|
fetchOptions?: BetterFetchOption;
|
||||||
|
callbackURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isRequestInProgress = false;
|
||||||
|
|
||||||
|
export const oneTapClient = (options: GoogleOneTapOptions) => {
|
||||||
|
return {
|
||||||
|
id: "one-tap",
|
||||||
|
getActions: ($fetch, _) => ({
|
||||||
|
oneTap: async (
|
||||||
|
opts?: GoogleOneTapActionOptions,
|
||||||
|
fetchOptions?: BetterFetchOption,
|
||||||
|
) => {
|
||||||
|
if (isRequestInProgress) {
|
||||||
|
console.warn(
|
||||||
|
"A Google One Tap request is already in progress. Please wait.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRequestInProgress = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof window === "undefined" || !window.document) {
|
||||||
|
console.warn(
|
||||||
|
"Google One Tap is only available in browser environments",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
|
||||||
|
const contextValue = context ?? options.context ?? "signin";
|
||||||
|
|
||||||
|
await loadGoogleScript();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
window.google?.accounts.id.initialize({
|
||||||
|
client_id: options.clientId,
|
||||||
|
callback: async (response: { credential: string }) => {
|
||||||
|
await $fetch("/one-tap/callback", {
|
||||||
|
method: "POST",
|
||||||
|
body: { idToken: response.credential },
|
||||||
|
...opts?.fetchOptions,
|
||||||
|
...fetchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Redirect if no fetch options are provided or a callbackURL is specified
|
||||||
|
if (
|
||||||
|
(!opts?.fetchOptions && !fetchOptions) ||
|
||||||
|
opts?.callbackURL
|
||||||
|
) {
|
||||||
|
window.location.href = opts?.callbackURL ?? "/";
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
auto_select: autoSelect,
|
||||||
|
cancel_on_tap_outside: cancelOnTapOutside,
|
||||||
|
context: contextValue,
|
||||||
|
});
|
||||||
|
window.google?.accounts.id.prompt();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Google One Tap flow:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
isRequestInProgress = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getAtoms($fetch) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
} satisfies BetterAuthClientPlugin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGoogleScript = (): Promise<void> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (window.googleScriptInitialized) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://accounts.google.com/gsi/client";
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
script.onload = () => {
|
||||||
|
window.googleScriptInitialized = true;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
};
|
||||||
97
packages/better-auth/src/plugins/one-tap/index.ts
Normal file
97
packages/better-auth/src/plugins/one-tap/index.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { APIError, createAuthEndpoint } from "../../api";
|
||||||
|
import { setSessionCookie } from "../../cookies";
|
||||||
|
import type { BetterAuthPlugin } from "../../types";
|
||||||
|
import { betterFetch } from "@better-fetch/fetch";
|
||||||
|
import { toBoolean } from "../../utils/boolean";
|
||||||
|
|
||||||
|
interface OneTapOptions {
|
||||||
|
/**
|
||||||
|
* Disable the signup flow
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableSignup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const oneTap = (options?: OneTapOptions) =>
|
||||||
|
({
|
||||||
|
id: "one-tap",
|
||||||
|
endpoints: {
|
||||||
|
oneTapCallback: createAuthEndpoint(
|
||||||
|
"/one-tap/callback",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: z.object({
|
||||||
|
idToken: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async (c) => {
|
||||||
|
const { idToken } = c.body;
|
||||||
|
const { data, error } = await betterFetch<{
|
||||||
|
email: string;
|
||||||
|
email_verified: string;
|
||||||
|
name: string;
|
||||||
|
picture: string;
|
||||||
|
sub: string;
|
||||||
|
}>("https://oauth2.googleapis.com/tokeninfo?id_token=" + idToken);
|
||||||
|
if (error) {
|
||||||
|
return c.json({
|
||||||
|
error: "Invalid token",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = await c.context.internalAdapter.findUserByEmail(
|
||||||
|
data.email,
|
||||||
|
);
|
||||||
|
if (!user) {
|
||||||
|
if (options?.disableSignup) {
|
||||||
|
throw new APIError("BAD_GATEWAY", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = await c.context.internalAdapter.createOAuthUser(
|
||||||
|
{
|
||||||
|
email: data.email,
|
||||||
|
emailVerified: toBoolean(data.email_verified),
|
||||||
|
name: data.name,
|
||||||
|
image: data.picture,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
providerId: "google",
|
||||||
|
accountId: data.sub,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!user) {
|
||||||
|
throw new APIError("INTERNAL_SERVER_ERROR", {
|
||||||
|
message: "Could not create user",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const session = await c.context.internalAdapter.createSession(
|
||||||
|
user?.user.id,
|
||||||
|
c.request,
|
||||||
|
);
|
||||||
|
await setSessionCookie(c, {
|
||||||
|
user: user.user,
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
return c.json({
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const session = await c.context.internalAdapter.createSession(
|
||||||
|
user.user.id,
|
||||||
|
c.request,
|
||||||
|
);
|
||||||
|
await setSessionCookie(c, {
|
||||||
|
user: user.user,
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
return c.json({
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}) satisfies BetterAuthPlugin;
|
||||||
@@ -218,15 +218,17 @@ export const getFullOrganization = createAuthEndpoint(
|
|||||||
"/organization/get-full",
|
"/organization/get-full",
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
query: z.object({
|
query: z.optional(
|
||||||
|
z.object({
|
||||||
orgId: z.string().optional(),
|
orgId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
requireHeaders: true,
|
requireHeaders: true,
|
||||||
use: [orgMiddleware, orgSessionMiddleware],
|
use: [orgMiddleware, orgSessionMiddleware],
|
||||||
},
|
},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const session = ctx.context.session;
|
const session = ctx.context.session;
|
||||||
const orgId = ctx.query.orgId || session.session.activeOrganizationId;
|
const orgId = ctx.query?.orgId || session.session.activeOrganizationId;
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return ctx.json(null, {
|
return ctx.json(null, {
|
||||||
status: 400,
|
status: 400,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
|||||||
);
|
);
|
||||||
const newSession = await ctx.context.internalAdapter.createSession(
|
const newSession = await ctx.context.internalAdapter.createSession(
|
||||||
updatedUser.id,
|
updatedUser.id,
|
||||||
|
ctx.request,
|
||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
* Update the session cookie with the new user data
|
* Update the session cookie with the new user data
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ export const totp2fa = (options: TOTPOptions, twoFactorTable: string) => {
|
|||||||
);
|
);
|
||||||
const newSession = await ctx.context.internalAdapter.createSession(
|
const newSession = await ctx.context.internalAdapter.createSession(
|
||||||
user.id,
|
user.id,
|
||||||
|
ctx.request,
|
||||||
);
|
);
|
||||||
await setSessionCookie(ctx, {
|
await setSessionCookie(ctx, {
|
||||||
session: newSession,
|
session: newSession,
|
||||||
|
|||||||
3
packages/better-auth/src/utils/boolean.ts
Normal file
3
packages/better-auth/src/utils/boolean.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function toBoolean(value: any): boolean {
|
||||||
|
return value === "true" || value === true;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user