diff --git a/dev/bc-fe/hono/db.sqlite b/dev/bc-fe/hono/db.sqlite index a0bf2dde41..ce459eab41 100644 Binary files a/dev/bc-fe/hono/db.sqlite and b/dev/bc-fe/hono/db.sqlite differ diff --git a/dev/bc-fe/react/src/App.tsx b/dev/bc-fe/react/src/App.tsx index 625fa22b3a..b1c1f40301 100644 --- a/dev/bc-fe/react/src/App.tsx +++ b/dev/bc-fe/react/src/App.tsx @@ -21,6 +21,9 @@ function App() {

{session.user.name}

+

+ {session.user.username} +

{session.user.email}

@@ -58,6 +61,7 @@ function App() { }}> Continue with github + ) @@ -73,6 +77,7 @@ export default App; function SignUp() { const [email, setEmail] = useState("") const [name, setName] = useState("") + const [username, setUsername] = useState("") const [password, setPassword] = useState("") return (
setName(e.target.value)} /> + setUsername(e.target.value)} + /> setPassword(e.target.value)} />
) +} + + + +function SignIn() { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + return ( +
+ setEmail(e.target.value)} + /> + + setPassword(e.target.value)} + /> + +
+ ) } \ No newline at end of file diff --git a/dev/bc-fe/react/src/lib/auth.ts b/dev/bc-fe/react/src/lib/auth.ts index 2e7d688a55..c9ad1c643e 100644 --- a/dev/bc-fe/react/src/lib/auth.ts +++ b/dev/bc-fe/react/src/lib/auth.ts @@ -1,7 +1,12 @@ import { createAuthClient } from "better-auth/react"; -import { twoFactorClient } from "better-auth/client"; +import { twoFactorClient, usernameClient } from "better-auth/client"; export const auth = createAuthClient({ baseURL: "http://localhost:3000/api/auth", - authPlugins: [twoFactorClient], + authPlugins: [ + twoFactorClient({ + twoFactorPage: "/two-factor", + }), + usernameClient, + ], }); diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts index 56b9281cd1..94f48ac06f 100644 --- a/packages/better-auth/src/api/index.ts +++ b/packages/better-auth/src/api/index.ts @@ -10,14 +10,14 @@ import { getSession, resetPassword, sendVerificationEmail, - signInCredential, + signInEmail, signInOAuth, signOut, verifyEmail, } from "./routes"; import { getCSRFToken } from "./routes/csrf"; import { ok, welcome } from "./routes/ok"; -import { signUpCredential } from "./routes/sign-up"; +import { signUpEmail } from "./routes/sign-up"; import { error } from "./routes/error"; import type { z, ZodAny, ZodObject, ZodOptional, ZodString } from "zod"; @@ -85,8 +85,8 @@ export const router = ( getCSRFToken, getSession: typedSession, signOut, - signUpCredential, - signInCredential, + signUpEmail, + signInEmail, forgetPassword, resetPassword, verifyEmail, @@ -136,10 +136,10 @@ export const router = ( for (const hook of plugin.hooks.after) { const match = hook.matcher(context); if (match) { - const hookRes = await hook.handler({ - ...context, + const obj = Object.assign(context, { returned: endpointRes, }); + const hookRes = await hook.handler(obj); if (hookRes && "response" in hookRes) { response = hookRes.response as any; } diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts index ed3341aab8..5d8d0032e5 100644 --- a/packages/better-auth/src/api/routes/sign-in.ts +++ b/packages/better-auth/src/api/routes/sign-in.ts @@ -39,7 +39,6 @@ export const signInOAuth = createAuthEndpoint( if (!provider) { throw new APIError("NOT_FOUND"); } - const cookie = c.context.authCookies; const currentURL = c.query?.currentURL ? new URL(c.query?.currentURL) @@ -78,8 +77,8 @@ export const signInOAuth = createAuthEndpoint( }, ); -export const signInCredential = createAuthEndpoint( - "/sign-in/email-password", +export const signInEmail = createAuthEndpoint( + "/sign-in/email", { method: "POST", body: z.object({ diff --git a/packages/better-auth/src/api/routes/sign-up.ts b/packages/better-auth/src/api/routes/sign-up.ts index 6c21e5b81f..f8682d0b28 100644 --- a/packages/better-auth/src/api/routes/sign-up.ts +++ b/packages/better-auth/src/api/routes/sign-up.ts @@ -3,8 +3,8 @@ import { Argon2id } from "oslo/password"; import { z } from "zod"; import { createAuthEndpoint } from "../call"; -export const signUpCredential = createAuthEndpoint( - "/sign-up/credential", +export const signUpEmail = createAuthEndpoint( + "/sign-up/email", { method: "POST", body: z.object({ diff --git a/packages/better-auth/src/client/base.ts b/packages/better-auth/src/client/base.ts index fe57a0af7a..2d31191f27 100644 --- a/packages/better-auth/src/client/base.ts +++ b/packages/better-auth/src/client/base.ts @@ -28,15 +28,25 @@ export const createAuthClient = ( : {}) & Auth["api"] : Auth["api"]; + /** + * used for plugins only + */ + const $baseFetch = createFetch({ + ...options, + baseURL: getBaseURL(options?.baseURL).withPath, + }); const $fetch = createFetch({ method: "GET", ...options, baseURL: getBaseURL(options?.baseURL).withPath, plugins: [ + ...(options?.plugins || []), + ...(options?.authPlugins + ?.flatMap((plugin) => plugin($baseFetch).fetchPlugins) + .filter((plugin) => plugin !== undefined) || []), + ...(options?.csrfPlugin !== false ? [csrfPlugin] : []), redirectPlugin, addCurrentURL, - ...(options?.csrfPlugin !== false ? [csrfPlugin] : []), - ...(options?.plugins || []), ], }); @@ -121,11 +131,10 @@ export const createAuthClient = ( atom: "$activeOrgSignal", }, { - matcher: (path) => path === "/sign-out", - atom: "$sessionSignal", - }, - { - matcher: (path) => path.startsWith("/sign-up"), + matcher: (path) => + path === "/sign-out" || + path.startsWith("/sign-up") || + path.startsWith("/sign-in"), atom: "$sessionSignal", }, ...pluginProxySignals, diff --git a/packages/better-auth/src/client/client.test.ts b/packages/better-auth/src/client/client.test.ts index 749bf812cd..ab2cc2c944 100644 --- a/packages/better-auth/src/client/client.test.ts +++ b/packages/better-auth/src/client/client.test.ts @@ -30,7 +30,13 @@ describe("client path to object", async () => { client.$atoms.$session; const client2 = createReactClient({ - authPlugins: [organization, twoFactorClient, usernameClient], + authPlugins: [ + organization, + twoFactorClient({ + twoFactorPage: "/two-factor", + }), + usernameClient, + ], }); }); }); diff --git a/packages/better-auth/src/client/create-client-plugin.ts b/packages/better-auth/src/client/create-client-plugin.ts index f142a6392b..16626933ba 100644 --- a/packages/better-auth/src/client/create-client-plugin.ts +++ b/packages/better-auth/src/client/create-client-plugin.ts @@ -1,4 +1,4 @@ -import type { BetterFetch } from "@better-fetch/fetch"; +import type { BetterFetch, BetterFetchPlugin } from "@better-fetch/fetch"; import type { Endpoint } from "better-call"; import type { AuthProxySignal } from "./proxy"; import type { Atom, PreinitializedWritableAtom } from "nanostores"; @@ -25,6 +25,7 @@ export const createClientPlugin = () => { atoms?: Record>; integrations?: Integrations; pathMethods?: Record; + fetchPlugins?: BetterFetchPlugin[]; }, ) => { return ($fetch: BetterFetch) => { diff --git a/packages/better-auth/src/client/fetch-plugins.ts b/packages/better-auth/src/client/fetch-plugins.ts index 1dfb3f29ec..2ce822d53e 100644 --- a/packages/better-auth/src/client/fetch-plugins.ts +++ b/packages/better-auth/src/client/fetch-plugins.ts @@ -7,7 +7,6 @@ export const redirectPlugin = { hooks: { onSuccess(context) { if (context.data?.url && context.data?.redirect) { - console.log("redirecting to", context.data.url); window.location.href = context.data.url; } }, diff --git a/packages/better-auth/src/client/proxy.ts b/packages/better-auth/src/client/proxy.ts index c6baebcbc1..544aef2359 100644 --- a/packages/better-auth/src/client/proxy.ts +++ b/packages/better-auth/src/client/proxy.ts @@ -64,12 +64,18 @@ export function createDynamicPathProxy>( body: method === "GET" ? undefined : body, query: query, method, - onSuccess() { + async onSuccess(context) { const signal = $signal?.find((s) => s.matcher(routePath)); if (!signal) return; const signalAtom = $signals?.[signal.atom]; if (!signalAtom) return; signalAtom.set(!signalAtom.get()); + /** + * call if options.onSuccess + * is passed since we are + * overriding onSuccess + */ + await options?.onSuccess?.(context); }, }); }, diff --git a/packages/better-auth/src/client/type.ts b/packages/better-auth/src/client/type.ts index f7db5f290e..b3121f21f5 100644 --- a/packages/better-auth/src/client/type.ts +++ b/packages/better-auth/src/client/type.ts @@ -1,4 +1,8 @@ -import type { BetterFetch, BetterFetchOption } from "@better-fetch/fetch"; +import type { + BetterFetch, + BetterFetchOption, + BetterFetchPlugin, +} from "@better-fetch/fetch"; import type { Auth } from "../auth"; import type { UnionToIntersection } from "../types/helper"; import type { useAuthStore as reactStore } from "./react"; @@ -30,6 +34,7 @@ export type AuthPlugin = ($fetch: BetterFetch) => { preact?: (useStore: typeof preactStore) => Record; }; pathMethods?: Record; + fetchPlugins?: BetterFetchPlugin[]; }; export interface ClientOptions extends BetterFetchOption { /** diff --git a/packages/better-auth/src/plugins/bearer/index.ts b/packages/better-auth/src/plugins/bearer/index.ts index 19ceed8cd0..a9a7774021 100644 --- a/packages/better-auth/src/plugins/bearer/index.ts +++ b/packages/better-auth/src/plugins/bearer/index.ts @@ -19,7 +19,7 @@ export const bearer = () => { ?.startsWith("Bearer ") || false ); }, - handler: createAuthMiddleware(async (ctx) => { + handler: async (ctx) => { const token = ctx.request?.headers .get("authorization") ?.replace("Bearer ", ""); @@ -38,7 +38,7 @@ export const bearer = () => { ctx.context.authCookies.sessionToken.name }=${signedToken.replace("=", "")}`, ); - }), + }, }, ], }, diff --git a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts index f12cc0546a..f3cc0a7685 100644 --- a/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts +++ b/packages/better-auth/src/plugins/two-factor/backup-codes/index.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { createAuthEndpoint } from "../../../api/call"; import { sessionMiddleware } from "../../../api/middlewares/session"; import { symmetricDecrypt, symmetricEncrypt } from "../../../crypto"; -import { verifyTwoFactorMiddleware } from "../two-fa-middleware"; +import { verifyTwoFactorMiddleware } from "../verify-middleware"; import type { TwoFactorProvider, UserWithTwoFactor } from "../types"; export interface BackupCodeOptions { diff --git a/packages/better-auth/src/plugins/two-factor/client.ts b/packages/better-auth/src/plugins/two-factor/client.ts index 263f7c299f..34926504b8 100644 --- a/packages/better-auth/src/plugins/two-factor/client.ts +++ b/packages/better-auth/src/plugins/two-factor/client.ts @@ -1,8 +1,21 @@ import { createClientPlugin } from "../../client/create-client-plugin"; import type { twoFactor as twoFa } from "../../plugins/two-factor"; -export const twoFactorClient = createClientPlugin>()( - ($fetch) => { +export const twoFactorClient = ( + options: { + twoFactorPage: string; + /** + * Redirect to the two factor page. If twoFactorPage + * is not set this will redirect to the root path. + * @default true + */ + redirect?: boolean; + } = { + redirect: true, + twoFactorPage: "/", + }, +) => { + return createClientPlugin>()(($fetch) => { return { id: "two-factor", authProxySignal: [ @@ -18,6 +31,21 @@ export const twoFactorClient = createClientPlugin>()( "/two-factor/enable": "POST", "/two-factor/send-otp": "POST", }, + fetchPlugins: [ + { + id: "two-factor", + name: "two-factor", + hooks: { + async onSuccess(context) { + if (context.data?.twoFactorRedirect) { + if (options.redirect) { + window.location.href = options.twoFactorPage; + } + } + }, + }, + }, + ], }; - }, -); + }); +}; diff --git a/packages/better-auth/src/plugins/two-factor/index.ts b/packages/better-auth/src/plugins/two-factor/index.ts index 95e6e9f5c8..6bf597e578 100644 --- a/packages/better-auth/src/plugins/two-factor/index.ts +++ b/packages/better-auth/src/plugins/two-factor/index.ts @@ -1,17 +1,15 @@ import { alphabet, generateRandomString } from "oslo/crypto"; import { z } from "zod"; -import { createAuthEndpoint } from "../../api/call"; +import { createAuthEndpoint, createAuthMiddleware } from "../../api/call"; import { sessionMiddleware } from "../../api/middlewares/session"; -import { symmetricEncrypt } from "../../crypto"; +import { hs256, symmetricEncrypt } from "../../crypto"; import type { BetterAuthPlugin } from "../../types/plugins"; import { backupCode2fa, generateBackupCodes } from "./backup-codes"; import { otp2fa } from "./otp"; import { totp2fa } from "./totp"; -import { - twoFactorMiddleware, - verifyTwoFactorMiddleware, -} from "./two-fa-middleware"; + import type { TwoFactorOptions, UserWithTwoFactor } from "./types"; +import type { Session } from "../../adapters/schema"; export const twoFactor = (options: O) => { const totp = totp2fa({ @@ -86,12 +84,65 @@ export const twoFactor = (options: O) => { ), }, options: options, - middlewares: [ - { - path: "/sign-in/credential", - middleware: twoFactorMiddleware(options), - }, - ], + hooks: { + after: [ + { + matcher(context) { + return ( + context.path === "/sign-in/email" || + context.path === "/sign-in/username" + ); + }, + handler: createAuthMiddleware(async (ctx) => { + const returned = (await (ctx as any).returned) as Response; + if (returned?.status !== 200) { + return; + } + const response = (await returned.json()) as { + user: UserWithTwoFactor; + session: Session; + }; + if (!response.user.twoFactorEnabled) { + return; + } + /** + * remove the session cookie. It's set by the sign in credential + */ + ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", { + path: "/", + sameSite: "lax", + httpOnly: true, + secure: false, + maxAge: 0, + }); + const hash = await hs256(ctx.context.secret, response.session.id); + /** + * We set the user id and the session + * id as a hash. Later will fetch for + * sessions with the user id compare + * the hash and set that as session. + */ + await ctx.setSignedCookie( + "better-auth.two-factor", + `${response.session.userId}!${hash}`, + ctx.context.secret, + ctx.context.authCookies.sessionToken.options, + ); + const res = new Response( + JSON.stringify({ + twoFactorRedirect: true, + }), + { + headers: ctx.responseHeader, + }, + ); + return { + response: res, + }; + }), + }, + ], + }, schema: { user: { fields: { diff --git a/packages/better-auth/src/plugins/two-factor/otp/index.ts b/packages/better-auth/src/plugins/two-factor/otp/index.ts index f39130990b..8619154293 100644 --- a/packages/better-auth/src/plugins/two-factor/otp/index.ts +++ b/packages/better-auth/src/plugins/two-factor/otp/index.ts @@ -3,9 +3,8 @@ import { generateRandomInteger } from "oslo/crypto"; import { generateHOTP } from "oslo/otp"; import { z } from "zod"; import { createAuthEndpoint } from "../../../api/call"; -import { sessionMiddleware } from "../../../api/middlewares/session"; import { OTP_RANDOM_NUMBER_COOKIE_NAME } from "../constant"; -import { verifyTwoFactorMiddleware } from "../two-fa-middleware"; +import { verifyTwoFactorMiddleware } from "../verify-middleware"; import type { TwoFactorProvider, UserWithTwoFactor } from "../types"; export interface OTPOptions { diff --git a/packages/better-auth/src/plugins/two-factor/totp/index.ts b/packages/better-auth/src/plugins/two-factor/totp/index.ts index 9cd1bb4fe7..ce6c95c8bc 100644 --- a/packages/better-auth/src/plugins/two-factor/totp/index.ts +++ b/packages/better-auth/src/plugins/two-factor/totp/index.ts @@ -6,7 +6,7 @@ import { createAuthEndpoint } from "../../../api/call"; import { sessionMiddleware } from "../../../api/middlewares/session"; import { symmetricDecrypt } from "../../../crypto"; import type { BackupCodeOptions } from "../backup-codes"; -import { verifyTwoFactorMiddleware } from "../two-fa-middleware"; +import { verifyTwoFactorMiddleware } from "../verify-middleware"; import type { TwoFactorProvider, UserWithTwoFactor } from "../types"; export type TOTPOptions = { diff --git a/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts similarity index 58% rename from packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts rename to packages/better-auth/src/plugins/two-factor/verify-middleware.ts index ffd1650005..3f77c6599a 100644 --- a/packages/better-auth/src/plugins/two-factor/two-fa-middleware.ts +++ b/packages/better-auth/src/plugins/two-factor/verify-middleware.ts @@ -1,11 +1,9 @@ import { APIError } from "better-call"; -import { z } from "zod"; import type { Session } from "../../adapters/schema"; import { createAuthMiddleware } from "../../api/call"; -import { signInCredential } from "../../api/routes"; import { hs256 } from "../../crypto"; import { TWO_FACTOR_COOKIE_NAME } from "./constant"; -import type { TwoFactorOptions, UserWithTwoFactor } from "./types"; +import type { UserWithTwoFactor } from "./types"; export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => { const cookie = await ctx.getSignedCookie( @@ -107,69 +105,3 @@ export const verifyTwoFactorMiddleware = createAuthMiddleware(async (ctx) => { message: "invalid two factor authentication", }); }); - -export const twoFactorMiddleware = (options: TwoFactorOptions) => - createAuthMiddleware( - { - body: z.object({ - email: z.string().email(), - password: z.string(), - /** - * Callback URL to - * redirect to after - * the user has signed in. - */ - callbackURL: z.string().optional(), - }), - }, - async (ctx) => { - //@ts-ignore - const signIn = await signInCredential({ - ...ctx, - body: ctx.body, - }); - if (!signIn?.user) { - return new Response(null, { - status: 401, - }); - } - const user = signIn.user as UserWithTwoFactor; - if (!user.twoFactorEnabled) { - return new Response(JSON.stringify(signIn), { - headers: ctx.responseHeader, - }); - } - /** - * remove the session cookie. It's set by the sign in credential - */ - ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", { - path: "/", - sameSite: "lax", - httpOnly: true, - secure: false, - maxAge: 0, - }); - const hash = await hs256(ctx.context.secret, signIn.session.id); - /** - * We set the user id and the session - * id as a hash. Later will fetch for - * sessions with the user id compare - * the hash and set that as session. - */ - await ctx.setSignedCookie( - "better-auth.two-factor", - `${signIn.session.userId}!${hash}`, - ctx.context.secret, - ctx.context.authCookies.sessionToken.options, - ); - return new Response( - JSON.stringify({ - url: options.twoFactorURL || ctx.body.callbackURL || "/", - redirect: true, - }), - { - headers: ctx.responseHeader, - }, - ); - }, - ); diff --git a/packages/better-auth/src/plugins/username/index.ts b/packages/better-auth/src/plugins/username/index.ts index d8aa9a80fc..440d2ff700 100644 --- a/packages/better-auth/src/plugins/username/index.ts +++ b/packages/better-auth/src/plugins/username/index.ts @@ -4,14 +4,14 @@ import type { BetterAuthPlugin } from "../../types/plugins"; import { Argon2id } from "oslo/password"; import { APIError } from "better-call"; import type { Account, User } from "../../adapters/schema"; -import { signUpCredential } from "../../api/routes/sign-up"; +import { signUpEmail } from "../../api/routes/sign-up"; export const username = () => { return { id: "username", endpoints: { signInUsername: createAuthEndpoint( - "/sign-in/username-password", + "/sign-in/username", { method: "POST", body: z.object({ @@ -102,7 +102,7 @@ export const username = () => { { method: "POST", body: z.object({ - username: z.string().min(3).max(20).optional(), + username: z.string().min(3).max(20), name: z.string(), email: z.string().email(), password: z.string(), @@ -111,7 +111,11 @@ export const username = () => { }), }, async (ctx) => { - const res = await signUpCredential(ctx); + const res = await signUpEmail({ + ...ctx, + //@ts-expect-error + _flag: undefined, + }); if (!res) { return ctx.json(null, { status: 400, @@ -121,9 +125,14 @@ export const username = () => { }, }); } - await ctx.context.internalAdapter.updateUserByEmail(res.user.email, { - username: ctx.body.username, - }); + const updatedUser = + await ctx.context.internalAdapter.updateUserByEmail( + res.user.email, + { + username: ctx.body.username, + }, + ); + console.log(updatedUser); return ctx.json(res); }, ), @@ -135,6 +144,8 @@ export const username = () => { username: { type: "string", required: false, + unique: true, + returned: true, }, }, }, diff --git a/packages/better-auth/src/types/plugins.ts b/packages/better-auth/src/types/plugins.ts index e944387dfc..e9483e114b 100644 --- a/packages/better-auth/src/types/plugins.ts +++ b/packages/better-auth/src/types/plugins.ts @@ -26,23 +26,19 @@ export type BetterAuthPlugin = { hooks?: { before?: { matcher: (context: GenericEndpointContext) => boolean; - handler: Endpoint< - (context: GenericEndpointContext) => Promise; - }> - >; + handler: (context: GenericEndpointContext) => Promise; + }>; }[]; after?: { matcher: (context: GenericEndpointContext) => boolean; - handler: Endpoint< - ( - context: GenericEndpointContext & { - returned: EndpointResponse; - }, - ) => Promise - >; + handler: ( + context: GenericEndpointContext & { + returned: EndpointResponse; + }, + ) => Promise; }[]; }; /**