feat: remember me and many more imporves

This commit is contained in:
Bereket Engida
2024-08-29 00:10:35 +03:00
parent ed3579d19e
commit f1e363bbff
36 changed files with 401 additions and 178 deletions

Binary file not shown.

View File

@@ -15,15 +15,20 @@ import { authClient } from "@/lib/auth-client";
import { useState } from "react";
import { Key } from "lucide-react";
import { PasswordInput } from "@/components/ui/password-input";
import { Checkbox } from "@/components/ui/checkbox";
import { toast } from "sonner";
import { useRouter } from "next/navigation";
export default function Page() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const router = useRouter()
return (
<div className="h-[50rem] w-full dark:bg-black bg-white dark:bg-grid-white/[0.2] bg-grid-black/[0.2] relative flex items-center justify-center">
{/* Radial gradient for the container to give a faded look */}
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
<Card className="mx-auto max-w-sm">
<Card className="mx-auto max-w-sm z-50">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
@@ -63,12 +68,22 @@ export default function Page() {
placeholder="Password"
/>
</div>
<div className="flex items-center gap-2">
<Checkbox onClick={() => {
setRememberMe(!rememberMe)
}} />
<Label>Remember me</Label>
</div>
<Button type="submit" className="w-full" onClick={async () => {
await authClient.signIn.credential({
const res = await authClient.signIn.credential({
email,
password,
callbackURL: "/"
callbackURL: "/",
dontRememberMe: !rememberMe
})
if (res.error) {
toast.error(res.error.message)
}
}}>
Login
</Button>
@@ -85,9 +100,14 @@ export default function Page() {
Login with Github
</Button>
<Button variant="secondary" className="gap-2" onClick={async () => {
await authClient.passkey.signIn({
const res = await authClient.passkey.signIn({
callbackURL: "/"
})
if (res?.error) {
toast.error(res.error.message)
} else {
router.push("/")
}
}}>
<Key size={16} />
Login with Passkey

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeWrapper } from "@/components/theme-provider";
import { Toaster } from "@/components/ui/sonner";
const inter = Inter({ subsets: ["latin"] });
@@ -16,10 +17,13 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<ThemeWrapper forcedTheme="dark" attribute="class">
<body className={inter.className}>{children}</body>
</ThemeWrapper>
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeWrapper forcedTheme="dark" attribute="class">
{children}
</ThemeWrapper>
<Toaster />
</body>
</html>
);
}

View File

@@ -11,7 +11,7 @@ export default async function TypewriterEffectSmoothDemo() {
{/* Radial gradient for the container to give a faded look */}
<div className="absolute pointer-events-none inset-0 flex items-center justify-center dark:bg-black bg-white [mask-image:radial-gradient(ellipse_at_center,transparent_20%,black)]"></div>
{
session ? <UserCard user={session.user} /> : null
session ? <UserCard session={session} /> : null
}
</div>

View File

@@ -8,9 +8,7 @@ export const SignOut = () => {
<Button
onClick={async () => {
await authClient.signOut({
body: {
callbackURL: "/"
}
})
}}
>

View File

@@ -12,6 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
richColors
closeButton
toastOptions={{
classNames: {
toast:

View File

@@ -3,21 +3,21 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "./ui/button";
import { LogOut } from "lucide-react";
import { Check, LogOut } from "lucide-react";
import { authClient } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import AddPasskey from "./add-passkey";
import { Session, User } from "@/lib/types";
import { toast } from "sonner";
export default function UserCard({
user,
}: {
user: {
name: string;
email: string;
image?: string;
};
export default function UserCard(props: {
session: {
user: User;
session: Session
} | null
}) {
const router = useRouter();
const session = authClient.useSession(props.session)
return (
<Card>
<CardHeader>
@@ -26,23 +26,42 @@ export default function UserCard({
<CardContent className="grid gap-8">
<div className="flex items-center gap-4">
<Avatar className="hidden h-9 w-9 sm:flex">
<AvatarImage src={user.image || "#"} alt="Avatar" />
<AvatarFallback>{user.name.charAt(0)}</AvatarFallback>
<AvatarImage src={session?.user.image || "#"} alt="Avatar" />
<AvatarFallback>{session?.user.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="grid gap-1">
<p className="text-sm font-medium leading-none">{user.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
<p className="text-sm font-medium leading-none">{session?.user.name}</p>
<p className="text-sm text-muted-foreground">{session?.user.email}</p>
</div>
</div>
<div className="border-y py-4 flex items-center justify-between">
<div className="border-y py-4 flex items-center justify-between gap-2">
<AddPasskey />
{
session?.user.twoFactorEnabled ? <Button variant="secondary" className="gap-2" onClick={async () => {
const res = await authClient.twoFactor.disable()
if (res.error) {
toast.error(res.error.message)
}
}}>
Disable 2FA
</Button> : <Button variant="outline" className="gap-2" onClick={async () => {
const res = await authClient.twoFactor.enable()
if (res.error) {
toast.error(res.error.message)
}
}}>
<p>
Enable 2FA
</p>
</Button>
}
</div>
</CardContent>
<CardFooter>
<Button className="gap-2 z-10" variant="secondary">
<LogOut size={16} />
<span className="text-sm" onClick={async () => {
const res = await authClient.signOut()
await authClient.signOut()
router.refresh()
}}>
Sign Out

View File

@@ -0,0 +1,5 @@
import { InferSession, InferUser } from "better-auth/types";
import type { auth } from "./auth";
export type User = InferUser<typeof auth>;
export type Session = InferSession<typeof auth>;

View File

@@ -15,7 +15,11 @@ export async function middleware(request: NextRequest) {
permission: {
invitation: ["create"],
},
options: {
headers: request.headers,
},
});
console.log({ canInvite });
return NextResponse.next();
}

View File

@@ -15,6 +15,7 @@
".": "./dist/index.js",
"./provider": "./dist/provider.js",
"./client": "./dist/client.js",
"./types": "./dist/types.js",
"./cli": "./dist/cli.js",
"./react": "./dist/react.js",
"./preact": "./dist/preact.js",
@@ -52,7 +53,7 @@
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"arctic": "^1.9.2",
"better-call": "^0.1.33",
"better-call": "^0.1.36",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"consola": "^3.2.3",

View File

@@ -93,16 +93,11 @@ export const createInternalAdapter = (
session.expiresAt.valueOf() - maxAge.valueOf() + updateDate <=
Date.now();
if (shouldBeUpdated) {
const updatedSession = await adapter.update<Session>({
const updatedSession = await adapter.create<Session>({
model: tables.session.tableName,
where: [
{
field: "id",
value: session.id,
},
],
update: {
data: {
...session,
id: generateRandomString(32, alphabet("a-z", "0-9", "A-Z")),
expiresAt: new Date(Date.now() + sessionExpiration),
},
});

View File

@@ -1,22 +1,27 @@
import { createRouter, Endpoint } from "better-call";
import { Context, createRouter, Endpoint } from "better-call";
import {
signInOAuth,
callbackOAuth,
getSession,
signOut,
signInCredential,
forgetPassword,
resetPassword,
verifyEmail,
sendVerificationEmail,
getSession,
} from "./routes";
import { AuthContext } from "../init";
import { csrfMiddleware } from "./middlewares/csrf";
import { getCSRFToken } from "./routes/csrf";
import { signUpCredential } from "./routes/sign-up";
import { parseAccount, parseSession, parseUser } from "../adapters/schema";
import { BetterAuthOptions, InferSession, InferUser } from "../types";
import { Prettify } from "../types/helper";
export const router = <C extends AuthContext>(ctx: C) => {
export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
ctx: C,
option: Option,
) => {
const pluginEndpoints = ctx.options.plugins?.reduce(
(acc, plugin) => {
return {
@@ -65,11 +70,30 @@ export const router = <C extends AuthContext>(ctx: C) => {
.filter((plugin) => plugin !== undefined)
.flat() || [];
async function typedSession(
ctx: Context<
"/session",
{
method: "GET";
requireHeaders: true;
}
>,
) {
const handler = await getSession(ctx);
return handler as {
session: Prettify<InferSession<Option>>;
user: Prettify<InferUser<Option>>;
} | null;
}
typedSession.path = getSession.path;
typedSession.method = getSession.method;
typedSession.options = getSession.options;
typedSession.headers = getSession.headers;
const baseEndpoints = {
signInOAuth,
callbackOAuth,
getCSRFToken,
getSession,
getSession: typedSession,
signOut,
signUpCredential,
signInCredential,
@@ -85,15 +109,49 @@ export const router = <C extends AuthContext>(ctx: C) => {
};
let api: Record<string, any> = {};
for (const [key, value] of Object.entries(endpoints)) {
api[key] = (context: any) => {
api[key] = async (context: any) => {
for (const plugin of ctx.options.plugins || []) {
if (plugin.hooks?.before) {
for (const hook of plugin.hooks.before) {
const match = hook.matcher(context);
if (match) {
const hookRes = await hook.handler(context);
if (hookRes && "context" in hookRes) {
context = {
...context,
...hookRes.context,
};
}
}
}
}
}
//@ts-ignore
return value({
const endpointRes = value({
...context,
context: {
...ctx,
...context.context,
},
});
let response = endpointRes;
for (const plugin of ctx.options.plugins || []) {
if (plugin.hooks?.after) {
for (const hook of plugin.hooks.after) {
const match = hook.matcher(context);
if (match) {
const hookRes = await hook.handler({
...context,
returned: endpointRes,
});
if (hookRes && "response" in hookRes) {
response = hookRes.response as any;
}
}
}
}
}
return response;
};
api[key].path = value.path;
api[key].method = value.method;
@@ -134,7 +192,7 @@ export const router = <C extends AuthContext>(ctx: C) => {
});
},
onError(e) {
console.log(e);
// console.log(e);
},
});
};

View File

@@ -19,16 +19,14 @@ export const csrfMiddleware = createAuthMiddleware(
return;
}
const url = new URL(ctx.request.url);
console.log({
url: ctx.request.url,
});
console.log(url.origin, ctx.context.options.baseURL);
/**
* If origin is the same as baseURL or if the
* origin is in the trustedOrigins then we
* don't need to check the CSRF token.
*/
if (
url.origin === ctx.context.baseURL ||
url.origin === ctx.context.options.baseURL ||
ctx.context.options.trustedOrigins?.includes(url.origin)
) {
return;

View File

@@ -4,6 +4,7 @@ import { APIError } from "better-call";
import { parseState } from "../../utils/state";
import { userSchema } from "../../adapters/schema";
import { HIDE_ON_CLIENT_METADATA } from "../../client/client-utils";
import { generateId } from "../../utils/id";
export const callbackOAuth = createAuthEndpoint(
"/callback/:id",
@@ -36,11 +37,11 @@ export const callbackOAuth = createAuthEndpoint(
c.context.logger.error("Code verification failed");
throw new APIError("UNAUTHORIZED");
}
const user = await provider.userInfo.getUserInfo(tokens);
const id = generateId();
const data = userSchema.safeParse({
...user,
id: user?.id.toString(),
id,
});
if (!user || data.success === false) {
throw new APIError("BAD_REQUEST");
@@ -52,7 +53,7 @@ export const callbackOAuth = createAuthEndpoint(
}
//find user in db
const dbUser = await c.context.internalAdapter.findUserByEmail(user.email);
let userId = dbUser?.user.id;
const userId = dbUser?.user.id;
if (dbUser) {
//check if user has already linked this provider
const hasBeenLinked = dbUser.accounts.find(
@@ -76,14 +77,13 @@ export const callbackOAuth = createAuthEndpoint(
}
} else {
try {
await c.context.internalAdapter.createOAuthUser(user, {
await c.context.internalAdapter.createOAuthUser(data.data, {
...tokens,
id: `${provider.id}:${user.id}`,
providerId: provider.id,
accountId: user.id,
userId: user.id,
userId: id,
});
userId = user.id;
} catch (e) {
const url = new URL(currentURL || callbackURL);
url.searchParams.set("error", "unable_to_create_user");
@@ -93,10 +93,9 @@ export const callbackOAuth = createAuthEndpoint(
}
//this should never happen
if (!userId) throw new APIError("INTERNAL_SERVER_ERROR");
//create session
const session = await c.context.internalAdapter.createSession(
userId,
userId || id,
c.request,
);
try {

View File

@@ -1,6 +1,5 @@
import { Context } from "better-call";
import { createAuthEndpoint } from "../call";
import { HIDE_ON_CLIENT_METADATA } from "../../client/client-utils";
export const getSession = createAuthEndpoint(
"/session",
@@ -31,6 +30,15 @@ export const getSession = createAuthEndpoint(
const updatedSession = await ctx.context.internalAdapter.updateSession(
session.session,
);
await ctx.setSignedCookie(
ctx.context.authCookies.sessionToken.name,
updatedSession.id,
ctx.context.secret,
{
...ctx.context.authCookies.sessionToken.options,
maxAge: updatedSession.expiresAt.valueOf() - Date.now(),
},
);
return ctx.json({
session: updatedSession,
user: session.user,

View File

@@ -87,16 +87,18 @@ export const signInCredential = createAuthEndpoint(
email: z.string().email(),
password: z.string(),
callbackURL: z.string().optional(),
/**
* If this is true the session will only be valid for the current browser session
* @default false
*/
dontRememberMe: z.boolean().default(false).optional(),
}),
},
async (ctx) => {
if (!ctx.context.options?.emailAndPassword?.enabled) {
ctx.context.logger.error("Email and password is not enabled");
return ctx.json(null, {
body: {
message: "Email and password is not enabled",
},
status: 400,
throw new APIError("BAD_REQUEST", {
message: "Email and password is not enabled",
});
}
const currentSession = await getSessionFromCtx(ctx);
@@ -114,8 +116,8 @@ export const signInCredential = createAuthEndpoint(
const user = await ctx.context.internalAdapter.findUserByEmail(email);
if (!user) {
ctx.context.logger.error("User not found", { email });
return ctx.json(null, {
status: 401,
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
});
}
const credentialAccount = user.accounts.find(
@@ -124,20 +126,17 @@ export const signInCredential = createAuthEndpoint(
const currentPassword = credentialAccount?.password;
if (!currentPassword) {
ctx.context.logger.error("Password not found", { email });
return ctx.json(null, {
status: 401,
body: { message: "Unexpected error" },
throw new APIError("UNAUTHORIZED", {
message: "Unexpected error",
});
}
const validPassword = await argon2id.verify(currentPassword, password);
if (!validPassword) {
ctx.context.logger.error("Invalid password");
return ctx.json(null, {
status: 401,
body: { message: "Invalid email or password" },
throw new APIError("UNAUTHORIZED", {
message: "Invalid email or password",
});
}
const session = await ctx.context.internalAdapter.createSession(
user.user.id,
ctx.request,
@@ -146,7 +145,12 @@ export const signInCredential = createAuthEndpoint(
ctx.context.authCookies.sessionToken.name,
session.id,
ctx.context.secret,
ctx.context.authCookies.sessionToken.options,
ctx.body.dontRememberMe
? {
...ctx.context.authCookies.sessionToken.options,
maxAge: undefined,
}
: ctx.context.authCookies.sessionToken.options,
);
return ctx.json({
user: user.user,

View File

@@ -1,7 +1,7 @@
import { router } from "./api";
import type { BetterAuthOptions } from "./types/options";
import type { UnionToIntersection } from "type-fest";
import type { Plugin } from "./types/plugins";
import type { BetterAuthPlugin } from "./types/plugins";
import { init } from "./init";
import type { CustomProvider } from "./providers";
@@ -9,20 +9,20 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
const authContext = init(options);
type PluginEndpoint = UnionToIntersection<
O["plugins"] extends Array<infer T>
? T extends Plugin
? T extends BetterAuthPlugin
? T["endpoints"]
: Record<string, never>
: Record<string, never>
: {}
: {}
>;
type ProviderEndpoint = UnionToIntersection<
O["providers"] extends Array<infer T>
? T extends CustomProvider
? T["endpoints"]
: Record<string, never>
: Record<string, never>
: {}
: {}
>;
const { handler, endpoints } = router(authContext);
const { handler, endpoints } = router(authContext, options);
type Endpoint = typeof endpoints;
return {
handler,
@@ -31,11 +31,7 @@ export const betterAuth = <O extends BetterAuthOptions>(options: O) => {
};
};
export type BetterAuth<
Endpoints extends Record<string, any> = ReturnType<
typeof router
>["endpoints"],
> = {
export type BetterAuth<Endpoints extends Record<string, any> = {}> = {
handler: (request: Request) => Promise<Response>;
api: Endpoints;
options: BetterAuthOptions;

View File

@@ -19,7 +19,7 @@ export const createVanillaClient = <Auth extends BetterAuth = never>(
: BAuth["api"];
const $fetch = createFetch({
...options,
baseURL: getBaseURL(options?.baseURL),
baseURL: getBaseURL(options?.baseURL).withPath,
plugins: [redirectPlugin, addCurrentURL, csrfPlugin],
});
const { $session, $sessionSignal } = getSessionAtom<Auth>($fetch);

View File

@@ -64,6 +64,7 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
method,
onSuccess() {
const signal = $signal?.[routePath as string];
console.log({ routePath, signal });
if (signal) {
signal.set(!signal.get());
}

View File

@@ -2,13 +2,23 @@ import { useStore } from "@nanostores/react";
import { createVanillaClient } from "./base";
import { BetterFetchOption } from "@better-fetch/fetch";
import { BetterAuth } from "../auth";
import { InferSession, InferUser } from "../types";
export const createAuthClient = <Auth extends BetterAuth>(
options?: BetterFetchOption,
) => {
const client = createVanillaClient<Auth>(options);
function useSession() {
return useStore(client.$atoms.$session);
function useSession(
initialValue: {
user: InferUser<Auth>;
session: InferSession<Auth>;
} | null = null,
) {
const session = useStore(client.$atoms.$session);
if (session) {
return session;
}
return initialValue;
}
function useActiveOrganization() {
return useStore(client.$atoms.$activeOrganization);

View File

@@ -1,64 +1,12 @@
import { atom, computed, task } from "nanostores";
import { Session, User } from "../adapters/schema";
import { Prettify, UnionToIntersection } from "../types/helper";
import { Prettify } from "../types/helper";
import { BetterAuth } from "../auth";
import { FieldAttribute, InferFieldOutput } from "../db";
import { BetterFetch } from "@better-fetch/fetch";
import { InferSession, InferUser } from "../types/models";
export function getSessionAtom<Auth extends BetterAuth>(client: BetterFetch) {
type AdditionalSessionFields = Auth["options"]["plugins"] extends Array<
infer T
>
? T extends {
schema: {
session: {
fields: infer Field;
};
};
}
? Field extends Record<string, FieldAttribute>
? {
[key in keyof Field]: InferFieldOutput<Field[key]>;
}
: {}
: {}
: {};
type AdditionalUserFields = Auth["options"]["plugins"] extends Array<infer T>
? T extends {
schema: {
user: {
fields: infer Field;
};
};
}
? Field extends Record<infer Key, FieldAttribute>
? Prettify<
{
[key in Key as Field[key]["required"] extends false
? never
: Field[key]["defaultValue"] extends
| boolean
| string
| number
| Date
| Function
? key
: never]: InferFieldOutput<Field[key]>;
} & {
[key in Key as Field[key]["returned"] extends false
? never
: key]?: InferFieldOutput<Field[key]>;
}
>
: {}
: {}
: {};
type UserWithAdditionalFields = User &
UnionToIntersection<AdditionalUserFields>;
type SessionWithAdditionalFields = Session &
UnionToIntersection<AdditionalSessionFields>;
type UserWithAdditionalFields = InferUser<Auth["options"]>;
type SessionWithAdditionalFields = InferSession<Auth["options"]>;
const $signal = atom<boolean>(false);
const $session = computed($signal, () =>
task(async () => {

View File

@@ -1,3 +1,4 @@
import { Context, ContextTools } from "better-call";
import { createKyselyAdapter } from "./adapters/kysely";
import { getAdapter } from "./adapters/utils";
import { createInternalAdapter } from "./db";
@@ -13,13 +14,13 @@ import { createLogger } from "./utils/logger";
export const init = (options: BetterAuthOptions) => {
const adapter = getAdapter(options);
const db = createKyselyAdapter(options);
const baseURL = getBaseURL(options.baseURL, options.basePath);
const { baseURL, withPath } = getBaseURL(options.baseURL, options.basePath);
return {
options: {
...options,
baseURL,
baseURL: baseURL,
},
baseURL,
baseURL: withPath,
secret:
options.secret ||
process.env.BETTER_AUTH_SECRET ||

View File

@@ -3,7 +3,6 @@ import { createAuthMiddleware, optionsMiddleware } from "../../api/call";
import { OrganizationOptions } from "./organization";
import { defaultRoles, Role } from "./access";
import { Session, User } from "../../adapters/schema";
import { getSession } from "../../api/routes";
import { sessionMiddleware } from "../../api/middlewares/session";
export const orgMiddleware = createAuthMiddleware(async (ctx) => {

View File

@@ -9,7 +9,7 @@ import {
updateOrganization,
} from "./routes/crud-org";
import { AccessControl, defaultRoles, defaultStatements, Role } from "./access";
import { getSession } from "../../api/routes";
import { getSessionFromCtx } from "../../api/routes";
import { AuthContext } from "../../init";
import {
acceptInvitation,
@@ -18,6 +18,10 @@ import {
rejectInvitation,
} from "./routes/crud-invites";
import { deleteMember, updateMember } from "./routes/crud-members";
import { sessionMiddleware } from "../../api/middlewares/session";
import { orgMiddleware, orgSessionMiddleware } from "./call";
import { getOrgAdapter } from "./adapter";
import { APIError } from "better-call";
export interface OrganizationOptions {
/**
@@ -94,7 +98,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
roles,
getSession: async (context: AuthContext) => {
//@ts-expect-error
return await getSession(context);
return await getSessionFromCtx(context);
},
});
@@ -112,6 +116,7 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
"/org/has-permission",
{
method: "POST",
requireHeaders: true,
body: z.object({
permission: z.record(z.string(), z.array(z.string())),
}) as unknown as ZodObject<{
@@ -122,10 +127,42 @@ export const organization = <O extends OrganizationOptions>(options?: O) => {
>;
}>;
}>,
use: [orgSessionMiddleware],
},
async () => {
const hasPerm = true;
return hasPerm;
async (ctx) => {
if (!ctx.context.session.session.activeOrganizationId) {
throw new APIError("BAD_REQUEST", {
message: "No active organization",
});
}
const adapter = getOrgAdapter(ctx.context.adapter);
const member = await adapter.findMemberByOrgId({
userId: ctx.context.session.user.id,
organizationId:
ctx.context.session.session.activeOrganizationId || "",
});
if (!member) {
throw new APIError("UNAUTHORIZED", {
message: "You are not a member of this organization",
});
}
const role = roles[member.role];
const result = role.authorize(ctx.body.permission as any);
if (result.error) {
return ctx.json(
{
error: result.error,
success: false,
},
{
status: 403,
},
);
}
return ctx.json({
error: null,
success: true,
});
},
),
},

View File

@@ -1,5 +0,0 @@
import { BetterAuthPlugin } from "../../types/plugins";
export const rememberMePlugin = async () => {
return {} satisfies BetterAuthPlugin;
};

View File

@@ -0,0 +1,13 @@
import { ContextTools } from "better-call";
import { AuthContext } from "../init";
export type GenericEndpointContext = ContextTools & {
context: AuthContext;
} & {
body: any;
request: Request;
headers: Headers;
params?: Record<string, string> | undefined;
query: any;
method: "*";
};

View File

@@ -1 +1,2 @@
export * from "./options";
export * from "./models";

View File

@@ -0,0 +1,73 @@
import { BetterAuthOptions } from ".";
import { Session, User } from "../adapters/schema";
import { BetterAuth } from "../auth";
import { FieldAttribute, InferFieldOutput } from "../db";
import { Prettify, UnionToIntersection } from "./helper";
type AdditionalSessionFields<Options extends BetterAuthOptions> =
Options["plugins"] extends Array<infer T>
? T extends {
schema: {
session: {
fields: infer Field;
};
};
}
? Field extends Record<string, FieldAttribute>
? {
[key in keyof Field]: InferFieldOutput<Field[key]>;
}
: {}
: {}
: {};
type AdditionalUserFields<Options extends BetterAuthOptions> =
Options["plugins"] extends Array<infer T>
? T extends {
schema: {
user: {
fields: infer Field;
};
};
}
? Field extends Record<infer Key, FieldAttribute>
? Prettify<
{
[key in Key as Field[key]["required"] extends false
? never
: Field[key]["defaultValue"] extends
| boolean
| string
| number
| Date
| Function
? key
: never]: InferFieldOutput<Field[key]>;
} & {
[key in Key as Field[key]["returned"] extends false
? never
: key]?: InferFieldOutput<Field[key]>;
}
>
: {}
: {}
: {};
export type InferUser<O extends BetterAuthOptions | BetterAuth> =
UnionToIntersection<
User &
(O extends BetterAuthOptions
? AdditionalUserFields<O>
: O extends BetterAuth
? AdditionalUserFields<O["options"]>
: {})
>;
export type InferSession<O extends BetterAuthOptions | BetterAuth> =
UnionToIntersection<
Session &
(O extends BetterAuthOptions
? AdditionalSessionFields<O>
: O extends BetterAuth
? AdditionalSessionFields<O["options"]>
: {})
>;

View File

@@ -1,7 +1,7 @@
import { Dialect } from "kysely";
import type { FieldAttribute } from "../db/field";
import type { Provider } from "./provider";
import type { Plugin } from "./plugins";
import type { BetterAuthPlugin } from "./plugins";
import type { Adapter } from "./adapter";
import { User } from "../adapters/schema";
@@ -55,7 +55,7 @@ export interface BetterAuthOptions {
/**
* Plugins
*/
plugins?: Plugin[];
plugins?: BetterAuthPlugin[];
/**
* Advanced options
*/

View File

@@ -1,8 +1,9 @@
import { Migration } from "kysely";
import { AuthEndpoint, AuthMiddleware } from "../api/call";
import { AuthEndpoint } from "../api/call";
import { FieldAttribute } from "../db/field";
import { LiteralString } from "./helper";
import { Endpoint } from "better-call";
import { Endpoint, EndpointResponse } from "better-call";
import { GenericEndpointContext } from "./context";
export type PluginSchema = {
[table: string]: {
@@ -22,6 +23,28 @@ export type BetterAuthPlugin = {
path: string;
middleware: Endpoint;
}[];
hooks?: {
before?: {
matcher: (context: GenericEndpointContext) => boolean;
handler: Endpoint<
(context: GenericEndpointContext) => Promise<void | {
context: Partial<GenericEndpointContext>;
}>
>;
}[];
after?: {
matcher: (context: GenericEndpointContext) => boolean;
handler: Endpoint<
(
context: GenericEndpointContext & {
returned: EndpointResponse;
},
) => Promise<void | {
response: EndpointResponse;
}>
>;
}[];
};
/**
* Schema the plugin needs
*

View File

@@ -11,10 +11,16 @@ function checkHasPath(url: string): boolean {
function withPath(url: string, path = "/api/auth") {
const hasPath = checkHasPath(url);
if (hasPath) {
return url;
return {
baseURL: new URL(url).origin,
withPath: url,
};
}
path = path.startsWith("/") ? path : `/${path}`;
return `${url}${path}`;
return {
baseURL: url,
withPath: `${url}${path}`,
};
}
export function getBaseURL(url?: string, path?: string) {
@@ -33,7 +39,10 @@ export function getBaseURL(url?: string, path?: string) {
!fromEnv &&
(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test")
) {
return "http://localhost:3000/api/auth";
return {
baseURL: "http://localhost:3000",
withPath: "http://localhost:3000/api/auth",
};
}
throw new Error(
"Could not infer baseURL from environment variables. Please pass it as an option to the createClient function.",

View File

@@ -4,6 +4,7 @@ export default defineConfig({
entry: {
index: "./src/index.ts",
provider: "./src/providers/index.ts",
types: "./src/types/index.ts",
client: "./src/client/index.ts",
cli: "./src/cli/index.ts",
react: "./src/client/react.ts",

28
pnpm-lock.yaml generated
View File

@@ -51,7 +51,7 @@ importers:
devDependencies:
'@types/bun':
specifier: latest
version: 1.1.7
version: 1.1.8
vite:
specifier: ^5.3.5
version: 5.3.5(@types/node@20.14.12)
@@ -478,8 +478,8 @@ importers:
specifier: ^1.9.2
version: 1.9.2
better-call:
specifier: ^0.1.33
version: 0.1.33(typescript@5.5.4)
specifier: ^0.1.36
version: 0.1.36(typescript@5.5.4)
chalk:
specifier: ^5.3.0
version: 5.3.0
@@ -2659,8 +2659,8 @@ packages:
'@types/better-sqlite3@7.6.11':
resolution: {integrity: sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==}
'@types/bun@1.1.7':
resolution: {integrity: sha512-iIIn26SOX8qI5E8Juh+0rUgBmFHvll1akscwerhp9O/fHZGdQBWNLJkkRg/3z2Mh6a3ZgWUIkXViLZZYg47TXw==}
'@types/bun@1.1.8':
resolution: {integrity: sha512-PIwVFQKPviksiibobyvcWtMvMFMTj91T8dQEh9l1P3Ypr3ZuVn9w7HSr+5mTNrPqD1xpdDLEErzZPU8gqHBu6g==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@@ -3042,10 +3042,10 @@ packages:
peerDependencies:
typescript: ^5.0.0
better-call@0.1.33:
resolution: {integrity: sha512-gzthE/AnimwMCNBNyy9LRqqAtjXkqO+dR4n1OjCiUhiBK4X+NZMKekQUKIzpfwDRC4k3hCshb1LsPhqwiSM7Bw==}
better-call@0.1.36:
resolution: {integrity: sha512-+FsoIB8tMVRciTTN9UUXXoJEzAqIaaNPrxa9kDYoTaGCTCimNKbHNuLwsnIAibamG0Lo7CFCiSL4yyV+I32O4A==}
peerDependencies:
typescript: ^5.0.0
typescript: ^5.6.0-beta
better-sqlite3@11.1.2:
resolution: {integrity: sha512-gujtFwavWU4MSPT+h9B+4pkvZdyOUkH54zgLdIrMmmmd4ZqiBIrRNBzNzYVFO417xo882uP5HBu4GjOfaSrIQw==}
@@ -3089,8 +3089,8 @@ packages:
bun-html-live-reload@0.1.3:
resolution: {integrity: sha512-PW1sp9ZmBAqiAa0aUhHpFc6sEQmC6FgRNKVAvcjSDUMqASzgq7xYpNkEt2Z6VjuiPXKtOx/49b6sLLmjyojrOw==}
bun-types@1.1.25:
resolution: {integrity: sha512-WpRb8/N3S5IE8UYdIn39+0Is1XzxsC78+MCe5cIdaer0lfFs6+DREtQH9TM6KJNKTxBYDvbx81RwbvxS5+CkVQ==}
bun-types@1.1.26:
resolution: {integrity: sha512-n7jDe62LsB2+WE8Q8/mT3azkPaatKlj/2MyP6hi3mKvPz9oPpB6JW/Ll6JHtNLudasFFuvfgklYSE+rreGvBjw==}
bundle-require@5.0.0:
resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==}
@@ -8820,9 +8820,9 @@ snapshots:
dependencies:
'@types/node': 20.14.12
'@types/bun@1.1.7':
'@types/bun@1.1.8':
dependencies:
bun-types: 1.1.25
bun-types: 1.1.26
'@types/cookie@0.6.0': {}
@@ -9275,7 +9275,7 @@ snapshots:
rou3: 0.5.1
typescript: 5.5.4
better-call@0.1.33(typescript@5.5.4):
better-call@0.1.36(typescript@5.5.4):
dependencies:
'@better-fetch/fetch': 1.1.4
'@types/set-cookie-parser': 2.4.10
@@ -9343,7 +9343,7 @@ snapshots:
bun-html-live-reload@0.1.3: {}
bun-types@1.1.25:
bun-types@1.1.26:
dependencies:
'@types/node': 20.12.14
'@types/ws': 8.5.11

View File

@@ -1,4 +1,4 @@
## TODO
[ ] handle migration when the config removes existing schema
[ ] refresh oauth tokens
[ ] remember me functionality
[x] remember me functionality

View File

@@ -5,6 +5,7 @@
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"disableReferencedProjectLoad": true,
"moduleDetection": "force",
"isolatedModules": true,
"strict": true,