mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20:27:44 +00:00
feat: add last login method plugin (#4347)
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center flex-col justify-center w-full md:py-10">
|
||||
<div className="md:w-[400px] animate-pulse">
|
||||
<div className="h-10 bg-gray-200 rounded-lg mb-4"></div>
|
||||
<div className="space-y-4">
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
<div className="h-12 bg-gray-200 rounded"></div>
|
||||
<div className="h-10 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useState, useTransition } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { signIn } from "@/lib/auth-client";
|
||||
import { client, signIn } from "@/lib/auth-client";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -29,6 +29,12 @@ export default function SignIn() {
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
|
||||
const LastUsedIndicator = () => (
|
||||
<span className="ml-auto absolute top-0 right-0 px-2 py-1 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-md font-medium">
|
||||
Last Used
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="max-w-md rounded-none">
|
||||
<CardHeader>
|
||||
@@ -83,7 +89,7 @@ export default function SignIn() {
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
className="w-full flex items-center justify-center"
|
||||
disabled={loading}
|
||||
onClick={async () => {
|
||||
startTransition(async () => {
|
||||
@@ -99,7 +105,16 @@ export default function SignIn() {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{loading ? <Loader2 size={16} className="animate-spin" /> : "Login"}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="flex-1">
|
||||
{loading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
"Login"
|
||||
)}
|
||||
</span>
|
||||
{client.isLastUsedLoginMethod("email") && <LastUsedIndicator />}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<div
|
||||
@@ -110,7 +125,7 @@ export default function SignIn() {
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-full gap-2")}
|
||||
className={cn("w-full gap-2 flex relative")}
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
@@ -141,11 +156,12 @@ export default function SignIn() {
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
></path>
|
||||
</svg>
|
||||
Sign in with Google
|
||||
<span>Sign in with Google</span>
|
||||
{client.isLastUsedLoginMethod("google") && <LastUsedIndicator />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-full gap-2")}
|
||||
className={cn("w-full gap-2 flex items-center relative")}
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "github",
|
||||
@@ -164,11 +180,12 @@ export default function SignIn() {
|
||||
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||
></path>
|
||||
</svg>
|
||||
Sign in with GitHub
|
||||
<span>Sign in with GitHub</span>
|
||||
{client.isLastUsedLoginMethod("github") && <LastUsedIndicator />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn("w-full gap-2")}
|
||||
className={cn("w-full gap-2 flex items-center relative")}
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "microsoft",
|
||||
@@ -187,7 +204,10 @@ export default function SignIn() {
|
||||
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
|
||||
></path>
|
||||
</svg>
|
||||
Sign in with Microsoft
|
||||
<span>Sign in with Microsoft</span>
|
||||
{client.isLastUsedLoginMethod("microsoft") && (
|
||||
<LastUsedIndicator />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
oidcClient,
|
||||
genericOAuthClient,
|
||||
deviceAuthorizationClient,
|
||||
lastLoginMethodClient,
|
||||
} from "better-auth/client/plugins";
|
||||
import { toast } from "sonner";
|
||||
import { stripeClient } from "@better-auth/stripe/client";
|
||||
@@ -36,6 +37,7 @@ export const client = createAuthClient({
|
||||
subscription: true,
|
||||
}),
|
||||
deviceAuthorizationClient(),
|
||||
lastLoginMethodClient(),
|
||||
],
|
||||
fetchOptions: {
|
||||
onError(e) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
openAPI,
|
||||
customSession,
|
||||
deviceAuthorization,
|
||||
lastLoginMethod,
|
||||
} from "better-auth/plugins";
|
||||
import { reactInvitationEmail } from "./email/invitation";
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
@@ -223,6 +224,7 @@ export const auth = betterAuth({
|
||||
expiresIn: "3min",
|
||||
interval: "5s",
|
||||
}),
|
||||
lastLoginMethod(),
|
||||
],
|
||||
trustedOrigins: ["exp://"],
|
||||
advanced: {
|
||||
|
||||
@@ -1704,6 +1704,24 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
|
||||
href: "/docs/plugins/have-i-been-pwned",
|
||||
icon: () => <p className="text-xs">';--</p>,
|
||||
},
|
||||
{
|
||||
title: "Last Login Method",
|
||||
href: "/docs/plugins/last-login-method",
|
||||
icon: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 256 256"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m141.66 133.66l-40 40A8 8 0 0 1 88 168v-32H24a8 8 0 0 1 0-16h64V88a8 8 0 0 1 13.66-5.66l40 40a8 8 0 0 1 0 11.32M200 32h-64a8 8 0 0 0 0 16h56v160h-56a8 8 0 0 0 0 16h64a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
title: "Multi Session",
|
||||
icon: () => (
|
||||
|
||||
354
docs/content/docs/plugins/last-login-method.mdx
Normal file
354
docs/content/docs/plugins/last-login-method.mdx
Normal file
@@ -0,0 +1,354 @@
|
||||
---
|
||||
title: Last Login Method
|
||||
description: Track and display the last authentication method used by users
|
||||
---
|
||||
|
||||
The last login method plugin tracks the most recent authentication method used by users (email, OAuth providers, etc.). This enables you to display helpful indicators on login pages, such as "Last signed in with Google" or prioritize certain login methods based on user preferences.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your auth config
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { lastLoginMethod } from "better-auth/plugins" // [!code highlight]
|
||||
|
||||
export const auth = betterAuth({
|
||||
// ... other config options
|
||||
plugins: [
|
||||
lastLoginMethod() // [!code highlight]
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add the client plugin to your auth client
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { lastLoginMethodClient } from "better-auth/client/plugins" // [!code highlight]
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
lastLoginMethodClient() // [!code highlight]
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, the plugin automatically tracks the last authentication method used by users. You can then retrieve and display this information in your application.
|
||||
|
||||
### Getting the Last Used Method
|
||||
|
||||
The client plugin provides several methods to work with the last login method:
|
||||
|
||||
```ts title="app.tsx"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
|
||||
// Get the last used login method
|
||||
const lastMethod = authClient.getLastUsedLoginMethod()
|
||||
console.log(lastMethod) // "google", "email", "github", etc.
|
||||
|
||||
// Check if a specific method was last used
|
||||
const wasGoogle = authClient.isLastUsedLoginMethod("google")
|
||||
|
||||
// Clear the stored method
|
||||
authClient.clearLastUsedLoginMethod()
|
||||
```
|
||||
|
||||
### UI Integration Example
|
||||
|
||||
Here's how to use the plugin to enhance your login page:
|
||||
|
||||
```tsx title="sign-in.tsx"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export function SignInPage() {
|
||||
const lastMethod = authClient.getLastUsedLoginMethod()
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1>Sign In</h1>
|
||||
|
||||
{/* Email sign in */}
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => authClient.signIn.email({...})}
|
||||
variant={lastMethod === "email" ? "default" : "outline"}
|
||||
className="w-full"
|
||||
>
|
||||
Sign in with Email
|
||||
{lastMethod === "email" && (
|
||||
<Badge className="ml-2">Last used</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* OAuth providers */}
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => authClient.signIn.social({ provider: "google" })}
|
||||
variant={lastMethod === "google" ? "default" : "outline"}
|
||||
className="w-full"
|
||||
>
|
||||
Continue with Google
|
||||
{lastMethod === "google" && (
|
||||
<Badge className="ml-2">Last used</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => authClient.signIn.social({ provider: "github" })}
|
||||
variant={lastMethod === "github" ? "default" : "outline"}
|
||||
className="w-full"
|
||||
>
|
||||
Continue with GitHub
|
||||
{lastMethod === "github" && (
|
||||
<Badge className="ml-2">Last used</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Database Persistence
|
||||
|
||||
By default, the last login method is stored only in cookies. For more persistent tracking and analytics, you can enable database storage.
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Enable database storage
|
||||
|
||||
Set `storeInDatabase` to `true` in your plugin configuration:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { lastLoginMethod } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
lastLoginMethod({
|
||||
storeInDatabase: true // [!code highlight]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Run database migration
|
||||
|
||||
The plugin will automatically add a `lastLoginMethod` field to your user table. Run the migration to apply the changes:
|
||||
|
||||
<Tabs items={["migrate", "generate"]}>
|
||||
<Tab value="migrate">
|
||||
```bash
|
||||
npx @better-auth/cli migrate
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="generate">
|
||||
```bash
|
||||
npx @better-auth/cli generate
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Step>
|
||||
<Step>
|
||||
### Access database field
|
||||
|
||||
When database storage is enabled, the `lastLoginMethod` field becomes available in user objects:
|
||||
|
||||
```ts title="user-profile.tsx"
|
||||
import { auth } from "@/lib/auth"
|
||||
|
||||
// Server-side access
|
||||
const session = await auth.api.getSession({ headers })
|
||||
console.log(session?.user.lastLoginMethod) // "google", "email", etc.
|
||||
|
||||
// Client-side access via session
|
||||
const { data: session } = authClient.useSession()
|
||||
console.log(session?.user.lastLoginMethod)
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Database Schema
|
||||
|
||||
When `storeInDatabase` is enabled, the plugin adds the following field to the `user` table:
|
||||
|
||||
Table: `user`
|
||||
|
||||
<DatabaseTable
|
||||
fields={[
|
||||
{ name: "lastLoginMethod", type: "string", description: "The last authentication method used by the user", isOptional: true },
|
||||
]}
|
||||
/>
|
||||
|
||||
### Custom Schema Configuration
|
||||
|
||||
You can customize the database field name:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { lastLoginMethod } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
lastLoginMethod({
|
||||
storeInDatabase: true,
|
||||
schema: {
|
||||
user: {
|
||||
lastLoginMethod: "last_auth_method" // Custom field name
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The last login method plugin accepts the following options:
|
||||
|
||||
### Server Options
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { lastLoginMethod } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
lastLoginMethod({
|
||||
// Cookie configuration
|
||||
cookieName: "last_used_login_method", // Default: "last_used_login_method"
|
||||
maxAge: 60 * 60 * 24 * 30, // Default: 30 days in seconds
|
||||
|
||||
// Database persistence
|
||||
storeInDatabase: false, // Default: false
|
||||
|
||||
// Custom method resolution
|
||||
customResolveMethod: (ctx) => {
|
||||
// Custom logic to determine the login method
|
||||
if (ctx.path === "/oauth/callback/custom-provider") {
|
||||
return "custom-provider"
|
||||
}
|
||||
// Return null to use default resolution
|
||||
return null
|
||||
},
|
||||
|
||||
// Schema customization (when storeInDatabase is true)
|
||||
schema: {
|
||||
user: {
|
||||
lastLoginMethod: "custom_field_name"
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**cookieName**: `string`
|
||||
- The name of the cookie used to store the last login method
|
||||
- Default: `"last_used_login_method"`
|
||||
|
||||
**maxAge**: `number`
|
||||
- Cookie expiration time in seconds
|
||||
- Default: `2592000` (30 days)
|
||||
|
||||
**storeInDatabase**: `boolean`
|
||||
- Whether to store the last login method in the database
|
||||
- Default: `false`
|
||||
- When enabled, adds a `lastLoginMethod` field to the user table
|
||||
|
||||
**customResolveMethod**: `(ctx: GenericEndpointContext) => string | null`
|
||||
- Custom function to determine the login method from the request context
|
||||
- Return `null` to use the default resolution logic
|
||||
- Useful for custom OAuth providers or authentication flows
|
||||
|
||||
**schema**: `object`
|
||||
- Customize database field names when `storeInDatabase` is enabled
|
||||
- Allows mapping the `lastLoginMethod` field to a custom column name
|
||||
|
||||
### Client Options
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { lastLoginMethodClient } from "better-auth/client/plugins"
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
lastLoginMethodClient({
|
||||
cookieName: "last_used_login_method" // Default: "last_used_login_method"
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
**cookieName**: `string`
|
||||
- The name of the cookie to read the last login method from
|
||||
- Must match the server-side `cookieName` configuration
|
||||
- Default: `"last_used_login_method"`
|
||||
|
||||
### Default Method Resolution
|
||||
|
||||
By default, the plugin tracks these authentication methods:
|
||||
|
||||
- **Email authentication**: `"email"`
|
||||
- **OAuth providers**: Provider ID (e.g., `"google"`, `"github"`, `"discord"`)
|
||||
- **OAuth2 callbacks**: Provider ID from URL path
|
||||
- **Sign up methods**: Tracked the same as sign in methods
|
||||
|
||||
The plugin automatically detects the method from these endpoints:
|
||||
- `/callback/:id` - OAuth callback with provider ID
|
||||
- `/oauth2/callback/:id` - OAuth2 callback with provider ID
|
||||
- `/sign-in/email` - Email sign in
|
||||
- `/sign-up/email` - Email sign up
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Custom Provider Tracking
|
||||
|
||||
If you have custom OAuth providers or authentication methods, you can use the `customResolveMethod` option:
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { lastLoginMethod } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
lastLoginMethod({
|
||||
customResolveMethod: (ctx) => {
|
||||
// Track custom SAML provider
|
||||
if (ctx.path === "/saml/callback") {
|
||||
return "saml"
|
||||
}
|
||||
|
||||
// Track magic link authentication
|
||||
if (ctx.path === "/verify-magic-link") {
|
||||
return "magic-link"
|
||||
}
|
||||
|
||||
// Track phone authentication
|
||||
if (ctx.path === "/sign-in/phone") {
|
||||
return "phone"
|
||||
}
|
||||
|
||||
// Return null to use default logic
|
||||
return null
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from "../../plugins/one-time-token/client";
|
||||
export * from "../../plugins/siwe/client";
|
||||
export * from "../../plugins/device-authorization/client";
|
||||
export type * from "@simplewebauthn/server";
|
||||
export * from "../../plugins/last-login-method/client";
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from "./one-time-token";
|
||||
export * from "./mcp";
|
||||
export * from "./siwe";
|
||||
export * from "./device-authorization";
|
||||
export * from "./last-login-method";
|
||||
|
||||
66
packages/better-auth/src/plugins/last-login-method/client.ts
Normal file
66
packages/better-auth/src/plugins/last-login-method/client.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { BetterAuthClientPlugin } from "../../types";
|
||||
|
||||
/**
|
||||
* Configuration for the client-side last login method plugin
|
||||
*/
|
||||
export interface LastLoginMethodClientConfig {
|
||||
/**
|
||||
* Name of the cookie to read the last login method from
|
||||
* @default "last_used_login_method"
|
||||
*/
|
||||
cookieName?: string;
|
||||
}
|
||||
|
||||
function getCookieValue(name: string): string | null {
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookie = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith(`${name}=`));
|
||||
|
||||
return cookie ? cookie.split("=")[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side plugin to retrieve the last used login method
|
||||
*/
|
||||
export const lastLoginMethodClient = (
|
||||
config: LastLoginMethodClientConfig = {},
|
||||
) => {
|
||||
const cookieName = config.cookieName || "last_used_login_method";
|
||||
|
||||
return {
|
||||
id: "last-login-method-client",
|
||||
getActions() {
|
||||
return {
|
||||
/**
|
||||
* Get the last used login method from cookies
|
||||
* @returns The last used login method or null if not found
|
||||
*/
|
||||
getLastUsedLoginMethod: (): string | null => {
|
||||
return getCookieValue(cookieName);
|
||||
},
|
||||
/**
|
||||
* Clear the last used login method cookie
|
||||
* This sets the cookie with an expiration date in the past
|
||||
*/
|
||||
clearLastUsedLoginMethod: (): void => {
|
||||
if (typeof document !== "undefined") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Check if a specific login method was the last used
|
||||
* @param method The method to check
|
||||
* @returns True if the method was the last used, false otherwise
|
||||
*/
|
||||
isLastUsedLoginMethod: (method: string): boolean => {
|
||||
const lastMethod = getCookieValue(cookieName);
|
||||
return lastMethod === method;
|
||||
},
|
||||
};
|
||||
},
|
||||
} satisfies BetterAuthClientPlugin;
|
||||
};
|
||||
139
packages/better-auth/src/plugins/last-login-method/index.ts
Normal file
139
packages/better-auth/src/plugins/last-login-method/index.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { createAuthMiddleware, type BetterAuthPlugin } from "..";
|
||||
import type { GenericEndpointContext } from "../../types";
|
||||
|
||||
/**
|
||||
* Configuration for tracking different authentication methods
|
||||
*/
|
||||
export interface LastLoginMethodOptions {
|
||||
/**
|
||||
* Name of the cookie to store the last login method
|
||||
* @default "last_used_login_method"
|
||||
*/
|
||||
cookieName?: string;
|
||||
/**
|
||||
* Cookie expiration time in seconds
|
||||
* @default 2592000 (30 days)
|
||||
*/
|
||||
maxAge?: number;
|
||||
/**
|
||||
* Custom method to resolve the last login method
|
||||
* @param ctx - The context from the hook
|
||||
* @returns The last login method
|
||||
*/
|
||||
customResolveMethod?: (ctx: GenericEndpointContext) => string | null;
|
||||
/**
|
||||
* Store the last login method in the database. This will create a new field in the user table.
|
||||
* @default false
|
||||
*/
|
||||
storeInDatabase?: boolean;
|
||||
/**
|
||||
* Custom schema for the plugin
|
||||
* @default undefined
|
||||
*/
|
||||
schema?: {
|
||||
user?: {
|
||||
lastLoginMethod?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin to track the last used login method
|
||||
*/
|
||||
export const lastLoginMethod = <O extends LastLoginMethodOptions>(
|
||||
userConfig?: O,
|
||||
) => {
|
||||
const paths = [
|
||||
"/callback/:id",
|
||||
"/oauth2/callback/:id",
|
||||
"/sign-in/email",
|
||||
"/sign-up/email",
|
||||
];
|
||||
const config = {
|
||||
cookieName: "last_used_login_method",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
customResolveMethod: (ctx) => {
|
||||
if (paths.includes(ctx.path)) {
|
||||
return ctx.params?.id ? ctx.params.id : ctx.path.split("/").pop();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
...userConfig,
|
||||
} satisfies LastLoginMethodOptions;
|
||||
|
||||
return {
|
||||
id: "last-login-method",
|
||||
init(ctx) {
|
||||
return {
|
||||
options: {
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
async before(user, context) {
|
||||
if (!config.storeInDatabase) return;
|
||||
if (!context) return;
|
||||
const lastUsedLoginMethod =
|
||||
config.customResolveMethod(context);
|
||||
if (lastUsedLoginMethod) {
|
||||
return {
|
||||
data: {
|
||||
...user,
|
||||
lastLoginMethod: lastUsedLoginMethod,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
hooks: {
|
||||
after: [
|
||||
{
|
||||
matcher() {
|
||||
return true;
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const lastUsedLoginMethod = config.customResolveMethod(ctx);
|
||||
lastUsedLoginMethod &&
|
||||
ctx.setCookie(config.cookieName, lastUsedLoginMethod, {
|
||||
maxAge: config.maxAge,
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
path: "/",
|
||||
});
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: (config.storeInDatabase
|
||||
? {
|
||||
user: {
|
||||
fields: {
|
||||
lastLoginMethod: {
|
||||
type: "string",
|
||||
input: false,
|
||||
required: false,
|
||||
fieldName:
|
||||
config.schema?.user?.lastLoginMethod || "lastLoginMethod",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined) as O["storeInDatabase"] extends true
|
||||
? {
|
||||
user: {
|
||||
fields: {
|
||||
lastLoginMethod: {
|
||||
type: "string";
|
||||
required: false;
|
||||
input: false;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
: undefined,
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { lastLoginMethod } from ".";
|
||||
import { lastLoginMethodClient } from "./client";
|
||||
import { parseCookies } from "../../cookies";
|
||||
|
||||
describe("lastLoginMethod", async () => {
|
||||
const { client, cookieSetter, testUser } = await getTestInstance(
|
||||
{
|
||||
plugins: [lastLoginMethod()],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [lastLoginMethodClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
it("should set the last login method cookie", async () => {
|
||||
const headers = new Headers();
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onSuccess(context) {
|
||||
cookieSetter(headers)(context);
|
||||
},
|
||||
},
|
||||
);
|
||||
const cookies = parseCookies(headers.get("cookie") || "");
|
||||
expect(cookies.get("last_used_login_method")).toBe("email");
|
||||
});
|
||||
|
||||
it("should set the last login method in the database", async () => {
|
||||
const { client, auth } = await getTestInstance({
|
||||
plugins: [lastLoginMethod({ storeInDatabase: true })],
|
||||
});
|
||||
const data = await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{ throw: true },
|
||||
);
|
||||
const session = await auth.api.getSession({
|
||||
headers: new Headers({
|
||||
authorization: `Bearer ${data.token}`,
|
||||
}),
|
||||
});
|
||||
expect(session?.user.lastLoginMethod).toBe("email");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user