mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
feat: new client and more
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-qr-code": "^2.0.15",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
Binary file not shown.
@@ -11,8 +11,9 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { authClient } from "@/lib/client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useState } from "react";
|
||||
import { Key } from "lucide-react";
|
||||
|
||||
export default function Page() {
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -59,7 +60,7 @@ export default function Page() {
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" onClick={async () => {
|
||||
const res = await authClient.signInCredential({
|
||||
await authClient.signIn.credential({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
@@ -73,14 +74,22 @@ export default function Page() {
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
await authClient.signInOAuth({
|
||||
provider: "github",
|
||||
callbackURL: "http://localhost:3000/",
|
||||
await authClient.signIn.oauth({
|
||||
body: {
|
||||
provider: "github",
|
||||
callbackURL: "http://localhost:3000",
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Login with Github
|
||||
</Button>
|
||||
<Button variant="secondary" className="gap-2" onClick={async () => {
|
||||
|
||||
}}>
|
||||
<Key size={16} />
|
||||
Login with Passkey
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useState } from "react";
|
||||
import { authClient } from "@/lib/client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
|
||||
export default function SignUpForm() {
|
||||
const [firstName, setFirstName] = useState("");
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
|
||||
import { AlertCircle, CheckCircle2, Mail } from "lucide-react"
|
||||
import { authClient } from '@/lib/auth-client'
|
||||
|
||||
export default function Component() {
|
||||
const [otp, setOtp] = useState('')
|
||||
const [isOtpSent, setIsOtpSent] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [isError, setIsError] = useState(false)
|
||||
const [isValidated, setIsValidated] = useState(false)
|
||||
|
||||
// In a real app, this email would come from your authentication context
|
||||
const userEmail = "user@example.com"
|
||||
|
||||
const requestOTP = async () => {
|
||||
await authClient.twoFactor.sendOtp();
|
||||
// In a real app, this would call your backend API to send the OTP
|
||||
setMessage('OTP sent to your email')
|
||||
setIsError(false)
|
||||
setIsOtpSent(true)
|
||||
}
|
||||
|
||||
const validateOTP = async () => {
|
||||
await authClient.twoFactor.verifyOtp({
|
||||
body: {
|
||||
code: otp,
|
||||
}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<main className='flex flex-col items-center justify-center min-h-screen'>
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Two-Factor Authentication</CardTitle>
|
||||
<CardDescription>Verify your identity with a one-time password</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid w-full items-center gap-4">
|
||||
{!isOtpSent ? (
|
||||
<Button onClick={requestOTP} className="w-full">
|
||||
<Mail className="mr-2 h-4 w-4" /> Send OTP to Email
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="otp">One-Time Password</Label>
|
||||
<Input
|
||||
id="otp"
|
||||
placeholder="Enter 6-digit OTP"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
maxLength={6}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={validateOTP} disabled={otp.length !== 6 || isValidated}>
|
||||
Validate OTP
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{message && (
|
||||
<div className={`flex items-center gap-2 mt-4 ${isError ? 'text-destructive' : 'text-primary'}`}>
|
||||
{isError ? <AlertCircle className="h-4 w-4" /> : <CheckCircle2 className="h-4 w-4" />}
|
||||
<p className="text-sm">{message}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { AlertCircle, CheckCircle2 } from "lucide-react"
|
||||
import { authClient } from "@/lib/client"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function Component() {
|
||||
const [totpCode, setTotpCode] = useState("")
|
||||
@@ -19,10 +20,10 @@ export default function Component() {
|
||||
setError("TOTP code must be 6 digits")
|
||||
return
|
||||
}
|
||||
authClient.verifyTotp({
|
||||
authClient.twoFactor.verify({
|
||||
body: {
|
||||
code: totpCode,
|
||||
callbackURL: "/"
|
||||
with: "totp",
|
||||
}
|
||||
}).then((res) => {
|
||||
console.log(res)
|
||||
@@ -78,8 +79,12 @@ export default function Component() {
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="text-sm text-muted-foreground">
|
||||
Protect your account with TOTP-based authentication
|
||||
<CardFooter className="text-sm text-muted-foreground gap-2">
|
||||
<Link href="/two-factor/otp">
|
||||
<Button variant="link" size="sm">
|
||||
Switch to Email Verification
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</main>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function Home() {
|
||||
<Button>signin</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Organization />
|
||||
{/* <Organization /> */}
|
||||
<Client />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { setCounter } from "@/server/counter";
|
||||
import { Button } from "./ui/button";
|
||||
import { client } from "@/lib/client";
|
||||
import { client } from "@/lib/auth-client";
|
||||
|
||||
export async function AddCount() {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useAuthStore } from "better-auth/react"
|
||||
import { Button } from "./ui/button";
|
||||
import QRCode from "react-qr-code";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, CardContent, CardHeader } from "./ui/card";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
|
||||
export function Client() {
|
||||
const session = useAuthStore(authClient.$session)
|
||||
const [uri, setUri] = useState<string>()
|
||||
const session = authClient.useSession()
|
||||
type S = NonNullable<typeof session>
|
||||
const a: S['user'] = {
|
||||
id: "1",
|
||||
@@ -14,23 +19,59 @@ export function Client() {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.twoFactorEnabled) {
|
||||
authClient.twoFactor.getTotpUri().then((res) => {
|
||||
if (res.data) {
|
||||
setUri(res.data.totpURI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [session])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
session ? <div>
|
||||
<Button onClick={async () => {
|
||||
if (session.user.twoFactorEnabled) {
|
||||
await authClient.disableTotp()
|
||||
} else {
|
||||
await authClient.enableTotp()
|
||||
}
|
||||
}}>
|
||||
{
|
||||
session.user.twoFactorEnabled ? "Disable" : "Enable"
|
||||
}
|
||||
</Button>
|
||||
</div> : null
|
||||
}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center justify-center gap-2">
|
||||
{
|
||||
session ? <div>
|
||||
<Button onClick={async () => {
|
||||
if (session.user?.twoFactorEnabled) {
|
||||
await authClient.twoFactor.disable()
|
||||
} else {
|
||||
await authClient.twoFactor.enable()
|
||||
}
|
||||
}}>
|
||||
{
|
||||
session.user?.twoFactorEnabled ? "Disable 2FA" : "Enable 2FA"
|
||||
}
|
||||
</Button>
|
||||
</div> : null
|
||||
}
|
||||
{
|
||||
uri ? <Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
View TOTP URI
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="flex flex-col items-center justify-center">
|
||||
{
|
||||
uri ? <div>
|
||||
<p>
|
||||
URI to scan
|
||||
</p>
|
||||
<QRCode value={uri} />
|
||||
</div> : null
|
||||
}
|
||||
</DialogContent>
|
||||
</Dialog> : null
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./ui/table";
|
||||
import { authClient } from "@/lib/client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useAuthStore } from "better-auth/react";
|
||||
|
||||
export const Organization = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { authClient } from "@/lib/client";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export const SignOut = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import { auth } from "./auth";
|
||||
|
||||
export const authClient = createAuthClient<typeof auth>({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { github, passkey } from "better-auth/provider";
|
||||
import { twoFactor } from "better-auth/plugins";
|
||||
import { organization, twoFactor } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
basePath: "/api/auth",
|
||||
@@ -9,10 +9,6 @@ export const auth = betterAuth({
|
||||
clientId: process.env.GITHUB_CLIENT_ID as string,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
|
||||
}),
|
||||
passkey({
|
||||
rpID: "localhost",
|
||||
rpName: "Better Auth",
|
||||
}),
|
||||
],
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
@@ -23,9 +19,15 @@ export const auth = betterAuth({
|
||||
enabled: true,
|
||||
},
|
||||
plugins: [
|
||||
organization(),
|
||||
twoFactor({
|
||||
issuer: "BetterAuth",
|
||||
twoFactorURL: "/two-factor",
|
||||
otpOptions: {
|
||||
async sendOTP(user, otp) {
|
||||
console.log({ user, otp });
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -52,12 +52,13 @@
|
||||
"@simplewebauthn/browser": "^10.0.0",
|
||||
"@simplewebauthn/server": "^10.0.1",
|
||||
"arctic": "^1.9.2",
|
||||
"better-call": "^0.1.28",
|
||||
"better-call": "^0.1.33",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"consola": "^3.2.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"jiti": "^1.21.6",
|
||||
"jose": "^5.7.0",
|
||||
"kysely": "^0.27.4",
|
||||
"mysql2": "^3.11.0",
|
||||
"nanostores": "^0.11.2",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { BetterAuthOptions } from "../types";
|
||||
import { FieldAttribute } from "../db";
|
||||
|
||||
export const accountSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -39,3 +41,53 @@ export interface MigrationTable {
|
||||
name: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export function parseData<T extends Record<string, any>>(
|
||||
data: T,
|
||||
schema: {
|
||||
fields: Record<string, FieldAttribute>;
|
||||
},
|
||||
) {
|
||||
const fields = schema.fields;
|
||||
const parsedData: Record<string, any> = {};
|
||||
for (const key in data) {
|
||||
const field = fields[key];
|
||||
if (!field) {
|
||||
parsedData[key] = data[key];
|
||||
continue;
|
||||
}
|
||||
if (field.returned === false) {
|
||||
continue;
|
||||
}
|
||||
parsedData[key] = data[key];
|
||||
}
|
||||
return parsedData as T;
|
||||
}
|
||||
|
||||
export function getAllFields(options: BetterAuthOptions, table: string) {
|
||||
let schema: Record<string, FieldAttribute> = {};
|
||||
for (const plugin of options.plugins || []) {
|
||||
if (plugin.schema && plugin.schema[table]) {
|
||||
schema = {
|
||||
...schema,
|
||||
...plugin.schema[table].fields,
|
||||
};
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
export function parseUser(options: BetterAuthOptions, user: User) {
|
||||
const schema = getAllFields(options, "user");
|
||||
return parseData(user, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseAccount(options: BetterAuthOptions, account: Account) {
|
||||
const schema = getAllFields(options, "account");
|
||||
return parseData(account, { fields: schema });
|
||||
}
|
||||
|
||||
export function parseSession(options: BetterAuthOptions, session: Session) {
|
||||
const schema = getAllFields(options, "session");
|
||||
return parseData(session, { fields: schema });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthContext } from "../init";
|
||||
import { csrfMiddleware } from "./middlewares/csrf";
|
||||
import { getCSRFToken } from "./routes/csrf";
|
||||
import { signUpCredential } from "./routes/signup";
|
||||
import { parseAccount, parseSession, parseUser } from "../adapters/schema";
|
||||
|
||||
export const router = <C extends AuthContext>(ctx: C) => {
|
||||
const pluginEndpoints = ctx.options.plugins?.reduce(
|
||||
@@ -101,8 +102,25 @@ export const router = <C extends AuthContext>(ctx: C) => {
|
||||
},
|
||||
...middlewares,
|
||||
],
|
||||
onError(e) {
|
||||
ctx.logger.error(e);
|
||||
/**
|
||||
* this is to remove any sensitive data from the response
|
||||
*/
|
||||
async transformResponse(res) {
|
||||
const body = await res.json();
|
||||
if (body?.user) {
|
||||
body.user = parseUser(ctx.options, body.user);
|
||||
}
|
||||
if (body?.session) {
|
||||
body.session = parseSession(ctx.options, body.session);
|
||||
}
|
||||
if (body?.account) {
|
||||
body.account = parseAccount(ctx.options, body.account);
|
||||
}
|
||||
return new Response(body ? JSON.stringify(body) : null, {
|
||||
headers: res.headers,
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
|
||||
export const confirmEmail = createAuthEndpoint(
|
||||
"/confirm-email",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
token: z.string(),
|
||||
email: z.string().email(),
|
||||
}),
|
||||
},
|
||||
async (ctx) => {},
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../call";
|
||||
import { createJWT } from "oslo/jwt";
|
||||
import { TimeSpan } from "oslo";
|
||||
|
||||
export const forgetPassword = createAuthEndpoint(
|
||||
"/send-forget-password",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
email: z.string().email(),
|
||||
}),
|
||||
},
|
||||
async (ctx) => {
|
||||
const { email } = ctx.body;
|
||||
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
||||
if (!user) {
|
||||
return ctx.json(
|
||||
{
|
||||
error: "User not found",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
statusText: "USER_NOT_FOUND",
|
||||
body: {
|
||||
message: "User not found",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
const token = await createJWT(
|
||||
"HS256",
|
||||
Buffer.from(ctx.context.secret),
|
||||
{
|
||||
email: user.user.email,
|
||||
},
|
||||
{
|
||||
expiresIn: new TimeSpan(1, "h"),
|
||||
issuer: "better-auth",
|
||||
subject: "forget-password",
|
||||
audiences: [user.user.email],
|
||||
includeIssuedTimestamp: true,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -7,7 +7,7 @@ import { oAuthProviderList } from "../../providers";
|
||||
import { Argon2id } from "oslo/password";
|
||||
|
||||
export const signInOAuth = createAuthEndpoint(
|
||||
"/signin/oauth",
|
||||
"/sign-in/oauth",
|
||||
{
|
||||
method: "POST",
|
||||
query: z
|
||||
|
||||
@@ -1,125 +1,44 @@
|
||||
import { Endpoint, Prettify } from "better-call";
|
||||
import { BetterAuth } from "../auth";
|
||||
import { HasRequiredKeys, UnionToIntersection } from "type-fest";
|
||||
import {
|
||||
BetterFetchOption,
|
||||
BetterFetchPlugin,
|
||||
BetterFetchResponse,
|
||||
createFetch,
|
||||
} from "@better-fetch/fetch";
|
||||
import { BetterAuthError } from "../error/better-auth-error";
|
||||
import { createDynamicPathProxy } from "./proxy";
|
||||
import { createClient } from "better-call/client";
|
||||
import { BetterFetch, createFetch } from "@better-fetch/fetch";
|
||||
import { getSessionAtom } from "./session-atom";
|
||||
import { getOrganizationAtoms } from "./org-atoms";
|
||||
import { getPasskeyActions } from "./passkey-actions";
|
||||
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
|
||||
import { InferRoutes } from "./path-to-object";
|
||||
import { ClientOptions } from "./type";
|
||||
import { getBaseURL } from "./client-utils";
|
||||
|
||||
type InferContext<T> = T extends (ctx: infer Ctx) => any
|
||||
? Ctx extends
|
||||
| {
|
||||
body: infer Body;
|
||||
}
|
||||
| {
|
||||
params: infer Param;
|
||||
}
|
||||
? (Body extends undefined
|
||||
? {}
|
||||
: {
|
||||
body: Body;
|
||||
}) &
|
||||
(Param extends undefined
|
||||
? {}
|
||||
: {
|
||||
params: Param;
|
||||
})
|
||||
: never
|
||||
: never;
|
||||
|
||||
export interface ClientOptions extends BetterFetchOption {}
|
||||
|
||||
const redirectPlugin = {
|
||||
id: "redirect",
|
||||
name: "Redirect",
|
||||
hooks: {
|
||||
onSuccess(context) {
|
||||
if (context.data.url && context.data.redirect) {
|
||||
console.log("redirecting to", context.data.url);
|
||||
}
|
||||
},
|
||||
},
|
||||
} satisfies BetterFetchPlugin;
|
||||
|
||||
function inferBaeURL() {
|
||||
const url =
|
||||
process.env.AUTH_URL ||
|
||||
process.env.NEXT_PUBLIC_AUTH_URL ||
|
||||
process.env.BETTER_AUTH_URL ||
|
||||
process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
|
||||
process.env.VERCEL_URL ||
|
||||
process.env.NEXT_PUBLIC_VERCEL_URL;
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
if (
|
||||
!url &&
|
||||
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
|
||||
) {
|
||||
return "http://localhost:3000";
|
||||
}
|
||||
throw new BetterAuthError(
|
||||
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
|
||||
);
|
||||
}
|
||||
|
||||
export const createBaseClient = <Auth extends BetterAuth = BetterAuth>(
|
||||
export const createVanillaClient = <Auth extends BetterAuth = never>(
|
||||
options?: ClientOptions,
|
||||
) => {
|
||||
const fetch = createFetch({
|
||||
baseURL: options?.baseURL || inferBaeURL(),
|
||||
type API = Auth extends never ? BetterAuth["api"] : Auth["api"];
|
||||
const $fetch = createFetch({
|
||||
...options,
|
||||
plugins: [...(options?.plugins || []), redirectPlugin],
|
||||
baseURL: getBaseURL(options?.baseURL),
|
||||
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
|
||||
});
|
||||
|
||||
type API = Auth["api"];
|
||||
type Options = API extends {
|
||||
[key: string]: infer T;
|
||||
}
|
||||
? T extends Endpoint
|
||||
? {
|
||||
[key in T["path"]]: T;
|
||||
}
|
||||
: {}
|
||||
: {};
|
||||
|
||||
type O = Prettify<UnionToIntersection<Options>>;
|
||||
return async <OPT extends O, K extends keyof OPT>(
|
||||
path: K,
|
||||
...options: HasRequiredKeys<InferContext<OPT[K]>> extends true
|
||||
? [
|
||||
BetterFetchOption<
|
||||
InferContext<OPT[K]>["body"],
|
||||
any,
|
||||
InferContext<OPT[K]>["params"]
|
||||
>,
|
||||
]
|
||||
: [
|
||||
BetterFetchOption<
|
||||
InferContext<OPT[K]>["body"],
|
||||
InferContext<OPT[K]>["params"]
|
||||
>?,
|
||||
]
|
||||
): Promise<
|
||||
BetterFetchResponse<
|
||||
Awaited<ReturnType<OPT[K] extends Endpoint ? OPT[K] : never>>
|
||||
>
|
||||
> => {
|
||||
const opts = options[0] as {
|
||||
params?: Record<string, any>;
|
||||
body?: Record<string, any>;
|
||||
};
|
||||
return (await fetch(path as string, {
|
||||
...options[0],
|
||||
body: opts.body,
|
||||
params: opts.params,
|
||||
method: opts.body ? "POST" : "GET",
|
||||
onRequest(context) {
|
||||
console.log("request", context.url);
|
||||
},
|
||||
})) as any;
|
||||
const { $session, $sessionSignal } = getSessionAtom<Auth>($fetch);
|
||||
const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
|
||||
const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
|
||||
getOrganizationAtoms($fetch, $session);
|
||||
const actions = {
|
||||
setActiveOrg: (orgId: string | null) => {
|
||||
activeOrgId.set(orgId);
|
||||
},
|
||||
signInPasskey,
|
||||
signUpPasskey,
|
||||
$atoms: {
|
||||
$session,
|
||||
$activeOrganization,
|
||||
$listOrganizations,
|
||||
},
|
||||
};
|
||||
const proxy = createDynamicPathProxy(actions, $fetch, {
|
||||
"/create/organization": $listOrg,
|
||||
"/two-factor/enable": $sessionSignal,
|
||||
"/two-factor/disable": $sessionSignal,
|
||||
}) as unknown as InferRoutes<API> & typeof actions;
|
||||
return proxy;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import { BetterAuthError } from "../error/better-auth-error";
|
||||
export const HIDE_ON_CLIENT_METADATA = {
|
||||
onClient: "hide" as const,
|
||||
};
|
||||
|
||||
export function inferBaeURL() {
|
||||
const url =
|
||||
function checkHasPath(url: string): boolean {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.pathname !== "/";
|
||||
} catch (error) {
|
||||
console.error("Invalid URL:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function withPath(url: string) {
|
||||
const hasPath = checkHasPath(url);
|
||||
if (hasPath) {
|
||||
return url;
|
||||
}
|
||||
return `${url}/api/auth`;
|
||||
}
|
||||
|
||||
export function getBaseURL(url?: string) {
|
||||
if (url) {
|
||||
return withPath(url);
|
||||
}
|
||||
const fromEnv =
|
||||
process.env.AUTH_URL ||
|
||||
process.env.NEXT_PUBLIC_AUTH_URL ||
|
||||
process.env.BETTER_AUTH_URL ||
|
||||
process.env.NEXT_PUBLIC_BETTER_AUTH_URL ||
|
||||
process.env.VERCEL_URL ||
|
||||
process.env.NEXT_PUBLIC_VERCEL_URL;
|
||||
if (url) {
|
||||
return url;
|
||||
process.env.NEXT_PUBLIC_BETTER_AUTH_URL;
|
||||
if (fromEnv) {
|
||||
return withPath(fromEnv);
|
||||
}
|
||||
if (
|
||||
!url &&
|
||||
!fromEnv &&
|
||||
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
|
||||
) {
|
||||
return "http://localhost:3000";
|
||||
return "http://localhost:3000/api/auth";
|
||||
}
|
||||
throw new BetterAuthError(
|
||||
throw new Error(
|
||||
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it } from "vitest";
|
||||
import { getTestInstance } from "../test-utils/test-instance";
|
||||
import { createAuthClient } from "./react";
|
||||
import { passkey } from "../providers";
|
||||
import { createClient } from "better-call/client";
|
||||
|
||||
describe("client path to object", async () => {
|
||||
const auth = await getTestInstance({
|
||||
providers: [
|
||||
passkey({
|
||||
rpID: "test",
|
||||
rpName: "test",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
it("should return a path to object", async () => {
|
||||
const client = createAuthClient({
|
||||
baseURL: "http://localhost:3000/api/auth",
|
||||
customFetchImpl: async (url, options) => {
|
||||
console.log({ url, options });
|
||||
return new Response();
|
||||
},
|
||||
});
|
||||
console.log(client.$atoms.$session.get());
|
||||
});
|
||||
});
|
||||
@@ -1,81 +1 @@
|
||||
import { ClientOptions } from "./base";
|
||||
import { BetterAuth } from "../auth";
|
||||
import {
|
||||
InferredActions,
|
||||
PickDefaultPaths,
|
||||
PickOrganizationPaths,
|
||||
PickProvidePaths,
|
||||
} from "./type";
|
||||
import { getProxy } from "./proxy";
|
||||
import { createClient } from "better-call/client";
|
||||
import { BetterFetch, createFetch } from "@better-fetch/fetch";
|
||||
import { OAuthProvider, OAuthProviderList } from "../types/provider";
|
||||
import { getSessionAtom } from "./session-atom";
|
||||
import { getOrganizationAtoms } from "./org-atoms";
|
||||
import { getPasskeyActions } from "./passkey-actions";
|
||||
import { inferBaeURL } from "./client-utils";
|
||||
import { addCurrentURL, csrfPlugin, redirectPlugin } from "./client-plugins";
|
||||
|
||||
export const createAuthClient = <Auth extends BetterAuth = BetterAuth>(
|
||||
options?: ClientOptions,
|
||||
) => {
|
||||
type API = BetterAuth["api"];
|
||||
|
||||
const client = createClient<API>({
|
||||
...options,
|
||||
baseURL: options?.baseURL || inferBaeURL(),
|
||||
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
|
||||
});
|
||||
|
||||
const $fetch = createFetch({
|
||||
...options,
|
||||
baseURL: options?.baseURL || inferBaeURL(),
|
||||
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
|
||||
});
|
||||
|
||||
const signInOAuth = async (data: {
|
||||
provider: Auth["options"]["providers"] extends Array<infer T>
|
||||
? T extends OAuthProvider
|
||||
? T["id"]
|
||||
: never
|
||||
: OAuthProviderList[number];
|
||||
callbackURL: string;
|
||||
}) => {
|
||||
const res = await client("@post/signin/oauth", {
|
||||
body: data,
|
||||
});
|
||||
if (res.data?.redirect) {
|
||||
window.location.href = res.data.url;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const { $session } = getSessionAtom<Auth>($fetch);
|
||||
const { signInPasskey, signUpPasskey } = getPasskeyActions($fetch);
|
||||
const { $activeOrganization, $listOrganizations, activeOrgId, $listOrg } =
|
||||
getOrganizationAtoms($fetch, $session);
|
||||
|
||||
const actions = {
|
||||
signInOAuth,
|
||||
$session,
|
||||
$activeOrganization,
|
||||
$listOrganizations,
|
||||
setActiveOrg: (orgId: string | null) => {
|
||||
activeOrgId.set(orgId);
|
||||
},
|
||||
signInPasskey,
|
||||
signUpPasskey,
|
||||
};
|
||||
|
||||
type PickedActions = Pick<
|
||||
typeof actions,
|
||||
| PickOrganizationPaths<Auth>
|
||||
| PickDefaultPaths
|
||||
| PickProvidePaths<"passkey", "signInPasskey" | "signUpPasskey", Auth>
|
||||
>;
|
||||
|
||||
const proxy = getProxy(actions, client as BetterFetch, {
|
||||
"create/organization": $listOrg,
|
||||
}) as InferredActions<Auth> & PickedActions;
|
||||
return proxy;
|
||||
};
|
||||
export * from "./base";
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BetterFetchResponse } from "@better-fetch/fetch";
|
||||
import { Context, Endpoint } from "better-call";
|
||||
import {
|
||||
HasRequiredKeys,
|
||||
Prettify,
|
||||
UnionToIntersection,
|
||||
} from "../types/helper";
|
||||
|
||||
type CamelCase<S extends string> =
|
||||
S extends `${infer P1}-${infer P2}${infer P3}`
|
||||
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
|
||||
: Lowercase<S>;
|
||||
|
||||
export type PathToObject<
|
||||
T extends string,
|
||||
Fn extends (...args: any[]) => any,
|
||||
> = T extends `/${infer Segment}/${infer Rest}`
|
||||
? { [K in CamelCase<Segment>]: PathToObject<`/${Rest}`, Fn> }
|
||||
: T extends `/${infer Segment}`
|
||||
? { [K in CamelCase<Segment>]: Fn }
|
||||
: never;
|
||||
|
||||
type MergeRoutes<T> = UnionToIntersection<T>;
|
||||
type InferRoute<API> = API extends {
|
||||
[key: string]: infer T;
|
||||
}
|
||||
? T extends Endpoint
|
||||
? T["options"]["metadata"] extends {
|
||||
onClient: "hide";
|
||||
}
|
||||
? {}
|
||||
: PathToObject<
|
||||
T["path"],
|
||||
T extends (ctx: infer C) => infer R
|
||||
? C extends Context<any, any>
|
||||
? (
|
||||
...data: HasRequiredKeys<C> extends true
|
||||
? [Prettify<C>]
|
||||
: [Prettify<C>?]
|
||||
) => Promise<BetterFetchResponse<Awaited<R>>>
|
||||
: never
|
||||
: never
|
||||
>
|
||||
: never
|
||||
: never;
|
||||
export type InferRoutes<API> = MergeRoutes<InferRoute<API>>;
|
||||
@@ -1,2 +1,23 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
export const useAuthStore = useStore;
|
||||
|
||||
import { createVanillaClient } from "./base";
|
||||
import { BetterFetchOption } from "@better-fetch/fetch";
|
||||
|
||||
export const createAuthClient = (options?: BetterFetchOption) => {
|
||||
const client = createVanillaClient(options);
|
||||
function useSession() {
|
||||
return useStore(client.$atoms.$session);
|
||||
}
|
||||
function useActiveOrganization() {
|
||||
return useStore(client.$atoms.$activeOrganization);
|
||||
}
|
||||
function useListOrganization() {
|
||||
return useStore(client.$atoms.$listOrganizations);
|
||||
}
|
||||
return Object.assign(client, {
|
||||
useSession,
|
||||
useActiveOrganization,
|
||||
useListOrganization,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,53 +1,12 @@
|
||||
import { BetterFetch, BetterFetchOption } from "@better-fetch/fetch";
|
||||
import { createBaseClient } from "./base";
|
||||
import { Atom, PreinitializedWritableAtom } from "nanostores";
|
||||
|
||||
function fromCamelCase(str: string) {
|
||||
const path = str
|
||||
.split(/(?=[A-Z])/)
|
||||
.join("/")
|
||||
.toLowerCase();
|
||||
return `/${path}`;
|
||||
}
|
||||
|
||||
const knownCases = [
|
||||
["sign", "in"],
|
||||
["sign", "up"],
|
||||
["sign", "out"],
|
||||
["invite", "member"],
|
||||
["update", "member"],
|
||||
["delete", "member"],
|
||||
["accept", "invitation"],
|
||||
["reject", "invitation"],
|
||||
["cancel", "invitation"],
|
||||
["has", "permission"],
|
||||
];
|
||||
|
||||
/**
|
||||
* Handles edge cases like signInCredential and
|
||||
* signUpCredential
|
||||
*/
|
||||
function handleEdgeCases(str: string) {
|
||||
const splits = str.split("/").filter((s) => s);
|
||||
let index = 0;
|
||||
for (const path of splits) {
|
||||
const secondPath = splits[index + 1]?.trim();
|
||||
if (secondPath) {
|
||||
const isKnownCase = knownCases.some(
|
||||
([a, b]) => a === path && b === secondPath,
|
||||
);
|
||||
if (isKnownCase) {
|
||||
splits[index] = `${path}-${secondPath}`;
|
||||
splits.splice(1, index + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return splits.join("/");
|
||||
}
|
||||
import { PreinitializedWritableAtom } from "nanostores";
|
||||
|
||||
const knownPathMethods: Record<string, "POST" | "GET"> = {
|
||||
"/sign-out": "POST",
|
||||
"enable/totp": "POST",
|
||||
"/two-factor/disable": "POST",
|
||||
"/two-factor/enable": "POST",
|
||||
"/two-factor/send-otp": "POST",
|
||||
};
|
||||
|
||||
function getMethod(path: string, args?: BetterFetchOption) {
|
||||
@@ -61,40 +20,48 @@ function getMethod(path: string, args?: BetterFetchOption) {
|
||||
return "GET";
|
||||
}
|
||||
|
||||
export function getProxy(
|
||||
actions: Record<string, any>,
|
||||
export function createDynamicPathProxy<T extends Record<string, any>>(
|
||||
routes: T,
|
||||
client: BetterFetch,
|
||||
$signal?: {
|
||||
[key: string]: PreinitializedWritableAtom<boolean>;
|
||||
},
|
||||
) {
|
||||
return new Proxy(actions, {
|
||||
get(target, key) {
|
||||
if (key in target) {
|
||||
return target[key as keyof typeof actions];
|
||||
): T {
|
||||
const handler: ProxyHandler<any> = {
|
||||
get(target, prop: string) {
|
||||
// If the property exists in the initial object, return it directly
|
||||
if (prop in routes) {
|
||||
return routes[prop as string];
|
||||
}
|
||||
return (args?: BetterFetchOption) => {
|
||||
key = fromCamelCase(key as string);
|
||||
key = handleEdgeCases(key);
|
||||
if (args?.params) {
|
||||
const paramPlaceholder = Object.keys(args?.params)
|
||||
.map((key) => `:${key}`)
|
||||
return new Proxy(() => {}, {
|
||||
get: (_, nestedProp: string) => {
|
||||
//@ts-expect-error
|
||||
return handler.get(target, `${prop}.${nestedProp}`);
|
||||
},
|
||||
apply: async (_, __, args) => {
|
||||
if (prop in target) {
|
||||
return target[prop](...args);
|
||||
}
|
||||
const path = prop
|
||||
.split(".")
|
||||
.map((segment) =>
|
||||
segment.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
|
||||
)
|
||||
.join("/");
|
||||
key = paramPlaceholder.length
|
||||
? `${key as string}/${paramPlaceholder}`
|
||||
: key;
|
||||
}
|
||||
return client(key as "/signin/oauth", {
|
||||
...(args || {}),
|
||||
method: getMethod(key, args),
|
||||
onSuccess() {
|
||||
const signal = $signal?.[key as string];
|
||||
if (signal) {
|
||||
signal.set(!signal.get());
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const routePath = `/${path}`;
|
||||
return await client(routePath, {
|
||||
...args[0],
|
||||
method: getMethod(routePath, args[0]),
|
||||
onSuccess() {
|
||||
const signal = $signal?.[routePath as string];
|
||||
if (signal) {
|
||||
signal.set(!signal.get());
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
return new Proxy(routes, handler);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,26 @@
|
||||
import { useStore } from "@nanostores/react";
|
||||
import { createVanillaClient } from "./base";
|
||||
import { BetterFetchOption } from "@better-fetch/fetch";
|
||||
import { BetterAuth } from "../auth";
|
||||
|
||||
export const createAuthClient = <Auth extends BetterAuth>(
|
||||
options?: BetterFetchOption,
|
||||
) => {
|
||||
const client = createVanillaClient<Auth>(options);
|
||||
function useSession() {
|
||||
return useStore(client.$atoms.$session);
|
||||
}
|
||||
function useActiveOrganization() {
|
||||
return useStore(client.$atoms.$activeOrganization);
|
||||
}
|
||||
function useListOrganization() {
|
||||
return useStore(client.$atoms.$listOrganizations);
|
||||
}
|
||||
return Object.assign(client, {
|
||||
useSession,
|
||||
useActiveOrganization,
|
||||
useListOrganization,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthStore = useStore;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom, computed, task } from "nanostores";
|
||||
import { Session, User } from "../adapters/schema";
|
||||
import { Prettify } from "../types/helper";
|
||||
import { Prettify, UnionToIntersection } from "../types/helper";
|
||||
import { BetterAuth } from "../auth";
|
||||
import { FieldAttribute, InferFieldOutput } from "../db";
|
||||
import { BetterFetch } from "@better-fetch/fetch";
|
||||
@@ -54,8 +54,10 @@ export function getSessionAtom<Auth extends BetterAuth>(client: BetterFetch) {
|
||||
: {}
|
||||
: {};
|
||||
|
||||
type UserWithAdditionalFields = User & AdditionalUserFields;
|
||||
type SessionWithAdditionalFields = Session & AdditionalSessionFields;
|
||||
type UserWithAdditionalFields = User &
|
||||
UnionToIntersection<AdditionalUserFields>;
|
||||
type SessionWithAdditionalFields = Session &
|
||||
UnionToIntersection<AdditionalSessionFields>;
|
||||
|
||||
const $signal = atom<boolean>(false);
|
||||
const $session = computed($signal, () =>
|
||||
@@ -70,5 +72,5 @@ export function getSessionAtom<Auth extends BetterAuth>(client: BetterFetch) {
|
||||
} | null;
|
||||
}),
|
||||
);
|
||||
return { $session };
|
||||
return { $session, $sessionSignal: $signal };
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { useStore } from "@nanostores/solid";
|
||||
export const useAuthStore = useStore;
|
||||
@@ -1,67 +1,11 @@
|
||||
import { Context, Endpoint } from "better-call";
|
||||
import { CamelCase } from "type-fest";
|
||||
import {
|
||||
Prettify,
|
||||
HasRequiredKeys,
|
||||
UnionToIntersection,
|
||||
} from "../types/helper";
|
||||
import { BetterFetchResponse } from "@better-fetch/fetch";
|
||||
import { UnionToIntersection } from "../types/helper";
|
||||
import { BetterAuth } from "../auth";
|
||||
import { CustomProvider } from "../providers";
|
||||
import { BetterFetchOption } from "@better-fetch/fetch";
|
||||
import type { useAuthStore as reactStore } from "./react";
|
||||
import type { useAuthStore as vueStore } from "./vue";
|
||||
|
||||
export type InferKeys<T> = T extends `/${infer A}/${infer B}`
|
||||
? CamelCase<`${A}-${InferKeys<B>}`>
|
||||
: T extends `${infer I}/:${infer _}`
|
||||
? I
|
||||
: T extends `${infer I}:${infer _}`
|
||||
? I
|
||||
: T extends `/${infer I}`
|
||||
? CamelCase<I>
|
||||
: CamelCase<T>;
|
||||
|
||||
export type InferActions<Actions> = Actions extends {
|
||||
[key: string]: infer T;
|
||||
}
|
||||
? UnionToIntersection<
|
||||
T extends Endpoint
|
||||
? {
|
||||
[key in InferKeys<T["path"]>]: T extends (ctx: infer C) => infer R
|
||||
? C extends Context<any, any>
|
||||
? (
|
||||
...data: HasRequiredKeys<C> extends true
|
||||
? [Prettify<C>]
|
||||
: [Prettify<C>?]
|
||||
) => Promise<BetterFetchResponse<Awaited<R>>>
|
||||
: never
|
||||
: never;
|
||||
}
|
||||
: never
|
||||
>
|
||||
: never;
|
||||
|
||||
export type ExcludeCredentialPaths<Auth extends BetterAuth> =
|
||||
Auth["options"]["emailAndPassword"] extends {
|
||||
enabled: true;
|
||||
}
|
||||
? ""
|
||||
: "signUpCredential" | "signInCredential";
|
||||
|
||||
export type ExcludedPasskeyPaths =
|
||||
| "passkeyGenerateAuthenticateOptions"
|
||||
| "passkeyGenerateRegisterOptions"
|
||||
| "verifyPasskey";
|
||||
|
||||
export type ExcludedPaths<Auth extends BetterAuth> =
|
||||
| "signinOauth"
|
||||
| "signUpOauth"
|
||||
| "callback"
|
||||
| "session"
|
||||
| ExcludeCredentialPaths<Auth>
|
||||
| ExcludedPasskeyPaths;
|
||||
|
||||
export type OrganizationPaths = "$activeOrganization" | "setActiveOrg";
|
||||
|
||||
type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
|
||||
export type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
|
||||
Auth["options"]["providers"] extends Array<infer T>
|
||||
? T extends CustomProvider
|
||||
? T["endpoints"]
|
||||
@@ -69,32 +13,5 @@ type ProviderEndpoint<Auth extends BetterAuth> = UnionToIntersection<
|
||||
: {}
|
||||
>;
|
||||
|
||||
export type Actions<Auth extends BetterAuth> = ProviderEndpoint<Auth> &
|
||||
Auth["api"];
|
||||
|
||||
export type InferredActions<Auth extends BetterAuth> = Prettify<
|
||||
Omit<InferActions<Actions<Auth>>, ExcludedPaths<Auth>>
|
||||
>;
|
||||
|
||||
export type PickOrganizationPaths<Auth extends BetterAuth> =
|
||||
Auth["options"]["plugins"] extends Array<infer T>
|
||||
? T extends {
|
||||
id: "organization";
|
||||
}
|
||||
? OrganizationPaths
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type PickProvidePaths<
|
||||
ID extends string,
|
||||
PickedPath extends string,
|
||||
Auth extends BetterAuth,
|
||||
> = Auth["options"]["providers"] extends Array<infer P>
|
||||
? P extends {
|
||||
id: ID;
|
||||
}
|
||||
? PickedPath
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type PickDefaultPaths = "$session";
|
||||
export type AuthStore = typeof reactStore | typeof vueStore;
|
||||
export interface ClientOptions extends BetterFetchOption {}
|
||||
|
||||
@@ -1,2 +1,23 @@
|
||||
import { useStore } from "@nanostores/vue";
|
||||
import { BetterFetchOption } from "@better-fetch/fetch";
|
||||
import { createVanillaClient } from "./base";
|
||||
|
||||
export const createAuthClient = (options?: BetterFetchOption) => {
|
||||
const client = createVanillaClient(options);
|
||||
function useSession() {
|
||||
return useStore(client.$atoms.$session);
|
||||
}
|
||||
function useActiveOrganization() {
|
||||
return useStore(client.$atoms.$activeOrganization);
|
||||
}
|
||||
function useListOrganization() {
|
||||
return useStore(client.$atoms.$listOrganizations);
|
||||
}
|
||||
return Object.assign(client, {
|
||||
useSession,
|
||||
useActiveOrganization,
|
||||
useListOrganization,
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthStore = useStore;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TwoFactorProvider, UserWithTwoFactor } from "../types";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto";
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../../../api/call";
|
||||
import { verifyTwoFactorMiddleware } from "../verify-middleware";
|
||||
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
|
||||
import { sessionMiddleware } from "../../../api/middlewares/session";
|
||||
|
||||
export interface BackupCodeOptions {
|
||||
@@ -78,37 +78,37 @@ export function getBackupCodes(user: UserWithTwoFactor, key: string) {
|
||||
export const backupCode2fa = (options?: BackupCodeOptions) => {
|
||||
return {
|
||||
id: "backup_code",
|
||||
verify: createAuthEndpoint(
|
||||
"/verify/backup-code",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
use: [verifyTwoFactorMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const validate = verifyBackupCode(
|
||||
{
|
||||
user: ctx.context.session.user,
|
||||
code: ctx.body.code,
|
||||
},
|
||||
ctx.context.secret,
|
||||
);
|
||||
if (!validate) {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
endpoints: {
|
||||
verifyBackupCode: createAuthEndpoint(
|
||||
"/two-factor/verify-backup-code",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
code: z.string(),
|
||||
}),
|
||||
use: [verifyTwoFactorMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const validate = verifyBackupCode(
|
||||
{
|
||||
status: 401,
|
||||
user: ctx.context.session.user,
|
||||
code: ctx.body.code,
|
||||
},
|
||||
ctx.context.secret,
|
||||
);
|
||||
}
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
),
|
||||
customActions: {
|
||||
if (!validate) {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
),
|
||||
generateBackupCodes: createAuthEndpoint(
|
||||
"/generate/backup-codes",
|
||||
"/two-factor/generate-backup-codes",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TwoFactorOptions, UserWithTwoFactor } from "./types";
|
||||
import {
|
||||
twoFactorMiddleware,
|
||||
verifyTwoFactorMiddleware,
|
||||
} from "./verify-middleware";
|
||||
} from "./two-fa-middleware";
|
||||
import { sessionMiddleware } from "../../api/middlewares/session";
|
||||
import { alphabet, generateRandomString } from "oslo/crypto";
|
||||
import { backupCode2fa, generateBackupCodes } from "./backup-codes";
|
||||
@@ -24,11 +24,11 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
return {
|
||||
id: "two-factor",
|
||||
endpoints: {
|
||||
...totp.customActions,
|
||||
...otp.customActions,
|
||||
...backupCode.customActions,
|
||||
...totp.endpoints,
|
||||
...otp.endpoints,
|
||||
...backupCode.endpoints,
|
||||
enableTwoFactor: createAuthEndpoint(
|
||||
"/enable/two-factor",
|
||||
"/two-factor/enable",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
@@ -40,6 +40,7 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
key: ctx.context.secret,
|
||||
data: secret,
|
||||
});
|
||||
console.log({ encryptedSecret });
|
||||
const backupCodes = await generateBackupCodes(
|
||||
ctx.context.secret,
|
||||
options.backupCodeOptions,
|
||||
@@ -62,7 +63,7 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
},
|
||||
),
|
||||
disableTwoFactor: createAuthEndpoint(
|
||||
"/disable/two-factor",
|
||||
"/two-factor/disable",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
@@ -84,51 +85,6 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
),
|
||||
verifyTwoFactor: createAuthEndpoint(
|
||||
"/verify/two-factor",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
/**
|
||||
* The code to validate
|
||||
*/
|
||||
code: z.string(),
|
||||
with: z.enum(["totp", "otp", "backup_code"]),
|
||||
callbackURL: z.string().optional(),
|
||||
}),
|
||||
use: [verifyTwoFactorMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
const providerId = ctx.body.with;
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
if (!provider) {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
const res = await provider.verify(ctx);
|
||||
if (!res.status) {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
await ctx.context.createSession();
|
||||
if (ctx.body.callbackURL) {
|
||||
return ctx.json({
|
||||
status: true,
|
||||
callbackURL: ctx.body.callbackURL,
|
||||
redirect: true,
|
||||
});
|
||||
}
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
),
|
||||
},
|
||||
options: options,
|
||||
middlewares: [
|
||||
@@ -149,18 +105,11 @@ export const twoFactor = <O extends TwoFactorOptions>(options: O) => {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
backupCodes: {
|
||||
twoFactorBackupCodes: {
|
||||
type: "string",
|
||||
required: false,
|
||||
returned: false,
|
||||
},
|
||||
/**
|
||||
* list of two factor providers id separated by comma
|
||||
*/
|
||||
twoFactorProviders: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import { generateHOTP } from "oslo/otp";
|
||||
import { generateRandomInteger } from "oslo/crypto";
|
||||
import { OTP_RANDOM_NUMBER_COOKIE_NAME } from "../constant";
|
||||
import { z } from "zod";
|
||||
import { verifyTwoFactorMiddleware } from "../verify-middleware";
|
||||
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
|
||||
|
||||
export interface OTPOptions {
|
||||
/**
|
||||
@@ -25,11 +25,11 @@ export const otp2fa = (options?: OTPOptions) => {
|
||||
/**
|
||||
* Generate OTP and send it to the user.
|
||||
*/
|
||||
const generateOTP = createAuthEndpoint(
|
||||
"/generate/otp",
|
||||
const send2FaOTP = createAuthEndpoint(
|
||||
"/two-factor/send-otp",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
use: [verifyTwoFactorMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
if (!options || !options.sendOTP) {
|
||||
@@ -63,7 +63,7 @@ export const otp2fa = (options?: OTPOptions) => {
|
||||
);
|
||||
|
||||
const verifyOTP = createAuthEndpoint(
|
||||
"/verify/otp",
|
||||
"/two-factor/verify-otp",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
@@ -86,34 +86,35 @@ export const otp2fa = (options?: OTPOptions) => {
|
||||
ctx.context.secret,
|
||||
);
|
||||
if (!randomNumber) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "counter cookie not found",
|
||||
throw new APIError("UNAUTHORIZED", {
|
||||
message: "OTP is expired",
|
||||
});
|
||||
}
|
||||
const toCheckOtp = await generateHOTP(
|
||||
Buffer.from(ctx.context.secret),
|
||||
parseInt(randomNumber),
|
||||
);
|
||||
console.log(toCheckOtp, ctx.body.code);
|
||||
|
||||
if (toCheckOtp !== ctx.body.code) {
|
||||
await ctx.context.createSession();
|
||||
return ctx.json({ status: true });
|
||||
if (toCheckOtp === ctx.body.code) {
|
||||
ctx.setCookie(cookie.name, "", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
maxAge: 0,
|
||||
});
|
||||
return ctx.context.valid();
|
||||
} else {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
return ctx.context.invalid();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
id: "otp",
|
||||
verify: verifyOTP,
|
||||
customActions: {
|
||||
generateOTP: generateOTP,
|
||||
endpoints: {
|
||||
send2FaOTP,
|
||||
verifyOTP,
|
||||
},
|
||||
} satisfies TwoFactorProvider;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { TimeSpan } from "oslo";
|
||||
import { alphabet, generateRandomString } from "oslo/crypto";
|
||||
import { TOTPController, createTOTPKeyURI } from "oslo/otp";
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint } from "../../../api/call";
|
||||
import { sessionMiddleware } from "../../../api/middlewares/session";
|
||||
import { APIError } from "better-call";
|
||||
import { TwoFactorProvider, UserWithTwoFactor } from "../types";
|
||||
import { verifyTwoFactorMiddleware } from "../verify-middleware";
|
||||
import { BackupCodeOptions, generateBackupCodes } from "../backup-codes";
|
||||
import { verifyTwoFactorMiddleware } from "../two-fa-middleware";
|
||||
import { BackupCodeOptions } from "../backup-codes";
|
||||
import { symmetricDecrypt } from "../../../crypto";
|
||||
|
||||
export type TOTPOptions = {
|
||||
@@ -36,105 +35,10 @@ export const totp2fa = (options: TOTPOptions) => {
|
||||
const opts = {
|
||||
digits: 6,
|
||||
period: new TimeSpan(options?.period || 30, "s"),
|
||||
secret: {
|
||||
field: "twoFactorSecret",
|
||||
},
|
||||
};
|
||||
|
||||
const enableTOTP = createAuthEndpoint(
|
||||
"/enable/totp",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
if (!options) {
|
||||
ctx.context.logger.error(
|
||||
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "totp isn't configured",
|
||||
});
|
||||
}
|
||||
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
|
||||
const user = ctx.context.session.user as UserWithTwoFactor;
|
||||
const uri = createTOTPKeyURI(
|
||||
options.issuer || "BetterAuth",
|
||||
user.name,
|
||||
Buffer.from(secret),
|
||||
opts,
|
||||
);
|
||||
const backupCodes = await generateBackupCodes(
|
||||
secret,
|
||||
options.backupCodes,
|
||||
);
|
||||
await ctx.context.adapter.update({
|
||||
model: "user",
|
||||
update: {
|
||||
twoFactorSecret: secret,
|
||||
twoFactorEnabled: true,
|
||||
backupCodes: backupCodes.encryptedBackupCodes,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: user.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
return ctx.json({ uri, backupCodes: backupCodes.backupCodes });
|
||||
},
|
||||
);
|
||||
|
||||
async function enable(user: UserWithTwoFactor) {
|
||||
const secret = generateRandomString(16, alphabet("a-z", "0-9", "-"));
|
||||
const uri = createTOTPKeyURI(
|
||||
options.issuer,
|
||||
user.name,
|
||||
Buffer.from(secret),
|
||||
opts,
|
||||
);
|
||||
const backupCodes = await generateBackupCodes(secret, options.backupCodes);
|
||||
return {
|
||||
uri,
|
||||
backupCodes: backupCodes.backupCodes,
|
||||
};
|
||||
}
|
||||
|
||||
const disableTOTP = createAuthEndpoint(
|
||||
"/disable/totp",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
},
|
||||
async (ctx) => {
|
||||
if (!options) {
|
||||
ctx.context.logger.error(
|
||||
"totp isn't configured. please pass totp option on two factor plugin to enable totp",
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "totp isn't configured",
|
||||
});
|
||||
}
|
||||
const user = ctx.context.session.user as UserWithTwoFactor;
|
||||
await ctx.context.adapter.update({
|
||||
model: "user",
|
||||
update: {
|
||||
twoFactorEnabled: false,
|
||||
},
|
||||
where: [
|
||||
{
|
||||
field: "id",
|
||||
value: user.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
);
|
||||
|
||||
const generateTOTP = createAuthEndpoint(
|
||||
"/generate/totp",
|
||||
"/totp/generate",
|
||||
{
|
||||
method: "POST",
|
||||
use: [sessionMiddleware],
|
||||
@@ -148,16 +52,15 @@ export const totp2fa = (options: TOTPOptions) => {
|
||||
message: "totp isn't configured",
|
||||
});
|
||||
}
|
||||
const session = ctx.context.session;
|
||||
const session = ctx.context.session.user as UserWithTwoFactor;
|
||||
const totp = new TOTPController(opts);
|
||||
const secret = (session.user as any).secret;
|
||||
const code = await totp.generate(secret);
|
||||
const code = await totp.generate(Buffer.from(session.twoFactorSecret));
|
||||
return { code };
|
||||
},
|
||||
);
|
||||
|
||||
const getTOTPURI = createAuthEndpoint(
|
||||
"/get/totp/uri",
|
||||
"/two-factor/get-totp-uri",
|
||||
{
|
||||
method: "GET",
|
||||
use: [sessionMiddleware],
|
||||
@@ -175,7 +78,7 @@ export const totp2fa = (options: TOTPOptions) => {
|
||||
return {
|
||||
totpURI: createTOTPKeyURI(
|
||||
options?.issuer || "BetterAuth",
|
||||
user.name,
|
||||
user.email,
|
||||
Buffer.from(user.twoFactorSecret),
|
||||
opts,
|
||||
),
|
||||
@@ -184,7 +87,7 @@ export const totp2fa = (options: TOTPOptions) => {
|
||||
);
|
||||
|
||||
const verifyTOTP = createAuthEndpoint(
|
||||
"/verify/totp",
|
||||
"/two-factor/verify-totp",
|
||||
{
|
||||
method: "POST",
|
||||
body: z.object({
|
||||
@@ -210,17 +113,18 @@ export const totp2fa = (options: TOTPOptions) => {
|
||||
}),
|
||||
);
|
||||
const status = await totp.verify(ctx.body.code, secret);
|
||||
return {
|
||||
status,
|
||||
};
|
||||
if (!status) {
|
||||
return ctx.context.invalid();
|
||||
}
|
||||
return ctx.context.valid();
|
||||
},
|
||||
);
|
||||
return {
|
||||
id: "totp",
|
||||
verify: verifyTOTP,
|
||||
customActions: {
|
||||
endpoints: {
|
||||
generateTOTP: generateTOTP,
|
||||
viewTOTPURI: getTOTPURI,
|
||||
verifyTOTP,
|
||||
},
|
||||
} satisfies TwoFactorProvider;
|
||||
};
|
||||
|
||||
+22
-1
@@ -5,6 +5,7 @@ import { hs256 } from "../../crypto";
|
||||
import { TWO_FACTOR_COOKIE_NAME } from "./constant";
|
||||
import { TwoFactorOptions, UserWithTwoFactor } from "./types";
|
||||
import { z } from "zod";
|
||||
import { signInCredential } from "../../api/routes";
|
||||
|
||||
export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
|
||||
const cookie = await ctx.getSignedCookie(
|
||||
@@ -62,7 +63,7 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
|
||||
}
|
||||
if (hashToMatch === hash) {
|
||||
return {
|
||||
createSession: async () => {
|
||||
valid: async () => {
|
||||
/**
|
||||
* Set the session cookie
|
||||
*/
|
||||
@@ -72,6 +73,26 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => {
|
||||
ctx.context.secret,
|
||||
ctx.context.authCookies.sessionToken.options,
|
||||
);
|
||||
if (ctx.body.callbackURL) {
|
||||
return ctx.json({
|
||||
status: true,
|
||||
callbackURL: ctx.body.callbackURL,
|
||||
redirect: true,
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.json({ status: true });
|
||||
},
|
||||
invalid: async () => {
|
||||
return ctx.json(
|
||||
{ status: false },
|
||||
{
|
||||
status: 401,
|
||||
body: {
|
||||
message: "Invalid code",
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
session: {
|
||||
id: session.id,
|
||||
@@ -45,12 +45,5 @@ export interface UserWithTwoFactor extends User {
|
||||
|
||||
export interface TwoFactorProvider {
|
||||
id: LiteralString;
|
||||
enable?: (user: UserWithTwoFactor) => Promise<void>;
|
||||
disable?: () => Promise<void>;
|
||||
verify: Endpoint<
|
||||
(ctx: any) => Promise<{
|
||||
status: boolean;
|
||||
}>
|
||||
>;
|
||||
customActions?: Record<string, AuthEndpoint>;
|
||||
endpoints?: Record<string, AuthEndpoint>;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ export const passkey = (options: PasskeyOptions) => {
|
||||
{
|
||||
method: "GET",
|
||||
use: [sessionMiddleware],
|
||||
metadata: {
|
||||
client: false,
|
||||
},
|
||||
},
|
||||
async (ctx) => {
|
||||
const session = ctx.context.session;
|
||||
|
||||
@@ -4,9 +4,12 @@ import { beforeAll, afterAll } from "vitest";
|
||||
import { type Listener, listen } from "listhen";
|
||||
import { toNodeHandler } from "better-call";
|
||||
import fs from "fs/promises";
|
||||
import { BetterAuthOptions } from "../types";
|
||||
|
||||
export async function getTestInstance() {
|
||||
const auth = betterAuth({
|
||||
export async function getTestInstance<O extends Partial<BetterAuthOptions>>(
|
||||
options?: O,
|
||||
) {
|
||||
const opts = {
|
||||
providers: [
|
||||
github({
|
||||
clientId: "test",
|
||||
@@ -26,7 +29,12 @@ export async function getTestInstance() {
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
} satisfies BetterAuthOptions;
|
||||
|
||||
const auth = betterAuth({
|
||||
...opts,
|
||||
...options,
|
||||
} as O extends undefined ? typeof opts : O & typeof opts);
|
||||
|
||||
let server: Listener;
|
||||
|
||||
|
||||
@@ -146,11 +146,5 @@ export interface BetterAuthOptions {
|
||||
* @default 32
|
||||
*/
|
||||
minPasswordLength?: number;
|
||||
/**
|
||||
* Two factor configuration
|
||||
*/
|
||||
twoFactor?: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function createCookieGetter(options: BetterAuthOptions) {
|
||||
name:
|
||||
process.env.NODE_ENV === "production"
|
||||
? `${secureCookiePrefix}${cookiePrefix}.${cookieName}`
|
||||
: `${cookiePrefix}${cookieName}`,
|
||||
: `${cookiePrefix}.${cookieName}`,
|
||||
options: {
|
||||
secure,
|
||||
sameSite: "lax",
|
||||
|
||||
@@ -8,7 +8,6 @@ export default defineConfig({
|
||||
cli: "./src/cli/index.ts",
|
||||
react: "./src/client/react.ts",
|
||||
preact: "./src/client/preact.ts",
|
||||
solid: "./src/client/solid.ts",
|
||||
vue: "./src/client/vue.ts",
|
||||
plugins: "./src/plugins/index.ts",
|
||||
},
|
||||
@@ -19,6 +18,7 @@ export default defineConfig({
|
||||
external: [
|
||||
"react",
|
||||
"svelte",
|
||||
"solid-js",
|
||||
"$app/environment",
|
||||
"next",
|
||||
"pg",
|
||||
|
||||
Generated
+46
-5
@@ -127,6 +127,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.52.2
|
||||
version: 7.52.2(react@18.3.1)
|
||||
react-qr-code:
|
||||
specifier: ^2.0.15
|
||||
version: 2.0.15(react@18.3.1)
|
||||
sonner:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -198,8 +201,8 @@ importers:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2
|
||||
better-call:
|
||||
specifier: ^0.1.28
|
||||
version: 0.1.28(typescript@5.5.4)
|
||||
specifier: ^0.1.33
|
||||
version: 0.1.33(typescript@5.5.4)
|
||||
chalk:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -215,6 +218,9 @@ importers:
|
||||
jiti:
|
||||
specifier: ^1.21.6
|
||||
version: 1.21.6
|
||||
jose:
|
||||
specifier: ^5.7.0
|
||||
version: 5.7.0
|
||||
kysely:
|
||||
specifier: ^0.27.4
|
||||
version: 0.27.4
|
||||
@@ -1733,8 +1739,8 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
|
||||
better-call@0.1.28:
|
||||
resolution: {integrity: sha512-9jl3q7Mb+3lRGeJUqkHAzAHph8BKcGHsj84h1gkfcMy2skyrCUbpFuhxMNAKBDtg2ppa66wTWDv0mGLOshxtEA==}
|
||||
better-call@0.1.33:
|
||||
resolution: {integrity: sha512-gzthE/AnimwMCNBNyy9LRqqAtjXkqO+dR4n1OjCiUhiBK4X+NZMKekQUKIzpfwDRC4k3hCshb1LsPhqwiSM7Bw==}
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
|
||||
@@ -2233,6 +2239,9 @@ packages:
|
||||
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
|
||||
hasBin: true
|
||||
|
||||
jose@5.7.0:
|
||||
resolution: {integrity: sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2717,6 +2726,9 @@ packages:
|
||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
pump@3.0.0:
|
||||
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
|
||||
|
||||
@@ -2731,6 +2743,9 @@ packages:
|
||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
qr.js@0.0.0:
|
||||
resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -2755,9 +2770,17 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-qr-code@2.0.15:
|
||||
resolution: {integrity: sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
|
||||
react-remove-scroll-bar@2.3.6:
|
||||
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4487,7 +4510,7 @@ snapshots:
|
||||
rou3: 0.5.1
|
||||
typescript: 5.5.4
|
||||
|
||||
better-call@0.1.28(typescript@5.5.4):
|
||||
better-call@0.1.33(typescript@5.5.4):
|
||||
dependencies:
|
||||
'@better-fetch/fetch': 1.1.4
|
||||
'@types/set-cookie-parser': 2.4.10
|
||||
@@ -5009,6 +5032,8 @@ snapshots:
|
||||
|
||||
jiti@1.21.6: {}
|
||||
|
||||
jose@5.7.0: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -5469,6 +5494,12 @@ snapshots:
|
||||
kleur: 3.0.3
|
||||
sisteransi: 1.0.5
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
pump@3.0.0:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
@@ -5482,6 +5513,8 @@ snapshots:
|
||||
|
||||
pvutils@1.1.3: {}
|
||||
|
||||
qr.js@0.0.0: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
radix3@1.1.2: {}
|
||||
@@ -5508,8 +5541,16 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-qr-code@2.0.15(react@18.3.1):
|
||||
dependencies:
|
||||
prop-types: 15.8.1
|
||||
qr.js: 0.0.0
|
||||
react: 18.3.1
|
||||
|
||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
Reference in New Issue
Block a user