chore: move env to core

This commit is contained in:
Bereket Engida
2024-10-31 12:58:36 +03:00
parent bdb08b193a
commit 7d61ca0b24
18 changed files with 307 additions and 84 deletions

View File

@@ -29,11 +29,11 @@ export default function Dashboard() {
</View>
</View>
</CardHeader>
<View className="my-2">
<View className="my-2 flex-row items-center justify-between px-6">
<Button
variant="default"
size="sm"
className="mx-6 flex-row items-center gap-2 "
className="flex-row items-center gap-2 "
>
<Ionicons name="edit" size={16} color="white" />
<Text>Edit User</Text>

View File

@@ -0,0 +1,147 @@
import * as DialogPrimitive from '@rn-primitives/dialog';
import * as React from 'react';
import { Platform, StyleSheet, View, type ViewProps } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { X } from '@/lib/icons/X';
import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlayWeb = React.forwardRef<DialogPrimitive.OverlayRef, DialogPrimitive.OverlayProps>(
({ className, ...props }, ref) => {
const { open } = DialogPrimitive.useRootContext();
return (
<DialogPrimitive.Overlay
className={cn(
'bg-black/80 flex justify-center items-center p-2 absolute top-0 right-0 bottom-0 left-0',
open ? 'web:animate-in web:fade-in-0' : 'web:animate-out web:fade-out-0',
className
)}
{...props}
ref={ref}
/>
);
}
);
DialogOverlayWeb.displayName = 'DialogOverlayWeb';
const DialogOverlayNative = React.forwardRef<
DialogPrimitive.OverlayRef,
DialogPrimitive.OverlayProps
>(({ className, children, ...props }, ref) => {
return (
<DialogPrimitive.Overlay
style={StyleSheet.absoluteFill}
className={cn('flex bg-black/80 justify-center items-center p-2', className)}
{...props}
ref={ref}
>
<Animated.View entering={FadeIn.duration(150)} exiting={FadeOut.duration(150)}>
<>{children}</>
</Animated.View>
</DialogPrimitive.Overlay>
);
});
DialogOverlayNative.displayName = 'DialogOverlayNative';
const DialogOverlay = Platform.select({
web: DialogOverlayWeb,
default: DialogOverlayNative,
});
const DialogContent = React.forwardRef<
DialogPrimitive.ContentRef,
DialogPrimitive.ContentProps & { portalHost?: string }
>(({ className, children, portalHost, ...props }, ref) => {
const { open } = DialogPrimitive.useRootContext();
return (
<DialogPortal hostName={portalHost}>
<DialogOverlay>
<DialogPrimitive.Content
ref={ref}
className={cn(
'max-w-lg gap-4 border border-border web:cursor-default bg-background p-6 shadow-lg web:duration-200 rounded-lg',
open
? 'web:animate-in web:fade-in-0 web:zoom-in-95'
: 'web:animate-out web:fade-out-0 web:zoom-out-95',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close
className={
'absolute right-4 top-4 p-0.5 web:group rounded-sm opacity-70 web:ring-offset-background web:transition-opacity web:hover:opacity-100 web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 web:disabled:pointer-events-none'
}
>
<X
size={Platform.OS === 'web' ? 16 : 18}
className={cn('text-muted-foreground', open && 'text-accent-foreground')}
/>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: ViewProps) => (
<View className={cn('flex flex-col gap-1.5 text-center sm:text-left', className)} {...props} />
);
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: ViewProps) => (
<View
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end gap-2', className)}
{...props}
/>
);
DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<DialogPrimitive.TitleRef, DialogPrimitive.TitleProps>(
({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
'text-lg native:text-xl text-foreground font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
)
);
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
DialogPrimitive.DescriptionRef,
DialogPrimitive.DescriptionProps
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm native:text-base text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
};

View File

@@ -1,9 +1,10 @@
import { createAuthClient } from "better-auth/react";
import { expoClient } from "better-auth/client/plugins";
import * as SecureStorage from "expo-secure-store";
import * as Constant from "expo-constants";
console.log(Constant);
export const authClient = createAuthClient({
baseURL: "http://192.168.1.7:3000",
baseURL: "http://172.20.10.3:3000",
disableDefaultFetchPlugins: true,
plugins: [
expoClient({

View File

@@ -1,18 +0,0 @@
import Constants from "expo-constants";
/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
export const getBaseUrl = () => {
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
if (!localhost) {
// return "https://turbo.t3.gg";
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
}
return `http://${localhost}:3000`;
};

View File

@@ -0,0 +1,4 @@
import { X } from 'lucide-react-native';
import { iconWithClassName } from './iconWithClassName';
iconWithClassName(X);
export { X };

View File

@@ -0,0 +1,14 @@
import type { LucideIcon } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
export function iconWithClassName(icon: LucideIcon) {
cssInterop(icon, {
className: {
target: 'style',
nativeStyleToProp: {
color: true,
opacity: true,
},
},
});
}

View File

@@ -73,6 +73,7 @@ export const auth = betterAuth({
sendOnSignUp: true,
},
account: {
enabled: true,
accountLinking: {
trustedProviders: ["google", "github"],
},
@@ -99,8 +100,8 @@ export const auth = betterAuth({
google: {
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
accessType: "offline",
prompt: "select_account",
// accessType: "offline",
// prompt: "select_account",
},
discord: {
clientId: process.env.DISCORD_CLIENT_ID || "",
@@ -125,5 +126,5 @@ export const auth = betterAuth({
username(),
expo(),
],
trustedOrigins: ["better-auth://"],
trustedOrigins: ["better-auth://", "exp://"],
});

View File

@@ -229,6 +229,8 @@
"@types/react": "^18.3.3",
"better-sqlite3": "^11.3.0",
"drizzle-orm": "^0.33.0",
"expo-linking": "~6.3.1",
"expo-secure-store": "~13.0.2",
"expo-web-browser": "~13.0.3",
"happy-dom": "^15.7.4",
"hono": "^4.5.4",
@@ -265,7 +267,6 @@
"nanoid": "^5.0.7",
"nanostores": "^0.11.2",
"oslo": "^1.2.1",
"std-env": "^3.7.0",
"uncrypto": "^0.1.3",
"zod": "^3.22.5"
},

View File

@@ -7,9 +7,8 @@ import { HIDE_METADATA } from "../../utils/hide-metadata";
import { setSessionCookie } from "../../cookies";
import { logger } from "../../utils/logger";
import type { OAuth2Tokens } from "../../oauth2";
import { compareHash, hmac } from "../../crypto/hash";
import { createEmailVerificationToken } from "./email-verification";
import { isDevelopment } from "std-env";
import { isDevelopment } from "../../utils/env";
export const callbackOAuth = createAuthEndpoint(
"/callback/:id",
@@ -115,6 +114,7 @@ export const callbackOAuth = createAuthEndpoint(
});
let user = dbUser?.user;
if (dbUser) {
const hasBeenLinked = dbUser.accounts.find(
(a) => a.providerId === provider.id,
@@ -127,7 +127,7 @@ export const callbackOAuth = createAuthEndpoint(
);
if (
(!isTrustedProvider && !userInfo.emailVerified) ||
!c.context.options.account?.accountLinking?.enabled
c.context.options.account?.accountLinking?.enabled === false
) {
if (isDevelopment) {
logger.warn(
@@ -199,7 +199,6 @@ export const callbackOAuth = createAuthEndpoint(
redirectOnError("unable_to_create_user");
}
}
if (!user) {
return redirectOnError("unable_to_create_user");
}

View File

@@ -3,7 +3,7 @@ import { TimeSpan } from "oslo";
import type { BetterAuthOptions } from "../types/options";
import type { GenericEndpointContext } from "../types/context";
import { BetterAuthError } from "../error";
import { env, isProduction } from "std-env";
import { env, isProduction } from "../utils/env";
import type { Session, User } from "../types";
export function getCookies(options: BetterAuthOptions) {

View File

@@ -4,7 +4,7 @@ import { createKyselyAdapter } from "./adapters/kysely-adapter/dialect";
import { getAdapter } from "./db/utils";
import { hashPassword, verifyPassword } from "./crypto/password";
import { createInternalAdapter } from "./db";
import { env, isProduction } from "std-env";
import { env, isProduction } from "./utils/env";
import type {
Adapter,
BetterAuthOptions,

View File

@@ -1,10 +1,12 @@
import type { BetterAuthClientPlugin, Store } from "better-auth";
import * as Browser from "expo-web-browser";
import * as Linking from "expo-linking";
import * as SecureStorage from "expo-secure-store";
import { parseSetCookieHeader } from "../../cookies";
interface ExpoClientOptions {
scheme: string;
storage: {
storage?: {
getItemAsync: (key: string) => Promise<string | null> | string | null;
setItemAsync: (key: string, value: string) => Promise<void> | void;
deleteItemAsync: (key: string) => Promise<void> | void;
@@ -19,10 +21,43 @@ interface StoredCookie {
expires: Date | null;
}
function getSetCookie(header: string) {
const parsed = parseSetCookieHeader(header);
const toSetCookie: Record<string, StoredCookie> = {};
parsed.forEach((cookie, key) => {
const expiresAt = cookie["expires"];
const maxAge = cookie["max-age"];
const expires = expiresAt
? new Date(String(expiresAt))
: maxAge
? new Date(Date.now() + Number(maxAge))
: null;
toSetCookie[key] = {
value: cookie["value"],
expires,
};
});
return JSON.stringify(toSetCookie);
}
function getCookie(cookie: string) {
let parsed = {} as Record<string, StoredCookie>;
try {
parsed = JSON.parse(cookie) as Record<string, StoredCookie>;
} catch (e) {}
const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
if (value.expires && value.expires < new Date()) {
return acc;
}
return `${acc}; ${key}=${value.value}`;
}, "");
return toSend;
}
export const expoClient = (opts: ExpoClientOptions) => {
let store: Store | null = null;
const cookieName = opts.cookies?.name || "better-auth_cookie";
const storage = opts.storage;
const storage = opts.storage || SecureStorage;
const scheme = opts.scheme;
if (!scheme) {
throw new Error(
@@ -43,68 +78,40 @@ export const expoClient = (opts: ExpoClientOptions) => {
hooks: {
async onSuccess(context) {
const setCookie = context.response.headers.get("set-cookie");
if (setCookie) {
const parsed = parseSetCookieHeader(setCookie);
const toSetCookie: Record<string, StoredCookie> = {};
parsed.forEach((cookie, key) => {
const expiresAt = cookie["expires"];
const maxAge = cookie["max-age"];
const expires = expiresAt
? new Date(String(expiresAt))
: maxAge
? new Date(Date.now() + Number(maxAge))
: null;
toSetCookie[key] = {
value: cookie["value"],
expires,
};
});
await storage.setItemAsync(
cookieName,
JSON.stringify(toSetCookie),
);
}
const toSetCookie = getSetCookie(setCookie || "");
await storage.setItemAsync(cookieName, toSetCookie);
if (
context.data.redirect &&
context.request.url.toString().includes("/sign-in")
) {
const callbackURL = JSON.parse(context.request.body)?.callbackURL;
const to = callbackURL;
const signInURL = context.data?.url;
const result = await Browser.openAuthSessionAsync(
signInURL,
callbackURL,
);
const result = await Browser.openAuthSessionAsync(signInURL, to);
if (result.type !== "success") return;
const url = new URL(result.url);
const cookie = String(url.searchParams.get("cookie"));
if (!cookie) return;
await storage.setItemAsync(cookieName, cookie);
await storage.setItemAsync(cookieName, getSetCookie(cookie));
store?.notify("$sessionSignal");
}
},
},
async init(url, options) {
options = options || {};
const cookie = await storage.getItemAsync(cookieName);
const parsed = cookie
? (JSON.parse(cookie) as Record<string, StoredCookie>)
: {};
const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
if (value.expires && value.expires < new Date()) {
return acc;
}
return `${acc}; ${key}=${value.value}`;
}, "");
const storedCookie = await storage.getItemAsync(cookieName);
const cookie = getCookie(storedCookie || "{}");
options.credentials = "omit";
options.headers = {
...options.headers,
cookie: toSend,
cookie,
origin: schemeURL,
};
if (options.body?.callbackURL) {
if (options.body.callbackURL.startsWith("/")) {
const url = `${schemeURL}${options.body.callbackURL}`;
const url = Linking.createURL(options.body.callbackURL, {
scheme,
});
options.body.callbackURL = url;
}
}

View File

@@ -1,8 +1,19 @@
import type { BetterAuthPlugin } from "better-auth";
import { isDevelopment } from "../../utils/env";
export const expo = () => {
return {
id: "expo",
init: (ctx) => {
return {
options: {
/**
* Add expo go as a trusted origin on dev
*/
trustedOrigins: isDevelopment ? ["exp://"] : [],
},
};
},
hooks: {
after: [
{
@@ -13,7 +24,6 @@ export const expo = () => {
const response = ctx.context.returned as Response;
if (response.status === 302) {
const location = response.headers.get("location");
if (!location) {
return;
}
@@ -33,6 +43,7 @@ export const expo = () => {
const url = new URL(location);
url.searchParams.set("cookie", cookie);
response.headers.set("location", url.toString());
console.log("Redirecting to", url.toString());
return {
response,
};

View File

@@ -20,7 +20,7 @@ import type { BetterAuthPlugin } from "../../types/plugins";
import { setSessionCookie } from "../../cookies";
import { BetterAuthError } from "../../error";
import { generateId } from "../../utils/id";
import { env } from "std-env";
import { env } from "../../utils/env";
interface WebAuthnChallengeValue {
expectedChallenge: string;

View File

@@ -0,0 +1,57 @@
//https://github.com/unjs/std-env/blob/main/src/env.ts
const _envShim = Object.create(null);
export type EnvObject = Record<string, string | undefined>;
const _getEnv = (useShim?: boolean) =>
globalThis.process?.env ||
//@ts-expect-error
globalThis.Deno?.env.toObject() ||
//@ts-expect-error
globalThis.__env__ ||
(useShim ? _envShim : globalThis);
export const env = new Proxy<EnvObject>(_envShim, {
get(_, prop) {
const env = _getEnv();
return env[prop as any] ?? _envShim[prop];
},
has(_, prop) {
const env = _getEnv();
return prop in env || prop in _envShim;
},
set(_, prop, value) {
const env = _getEnv(true);
env[prop as any] = value;
return true;
},
deleteProperty(_, prop) {
if (!prop) {
return false;
}
const env = _getEnv(true);
delete env[prop as any];
return true;
},
ownKeys() {
const env = _getEnv(true);
return Object.keys(env);
},
});
function toBoolean(val: boolean | string | undefined) {
return val ? val !== "false" : false;
}
export const nodeENV =
(typeof process !== "undefined" && process.env && process.env.NODE_ENV) || "";
/** Detect if `NODE_ENV` environment variable is `production` */
export const isProduction = nodeENV === "production";
/** Detect if `NODE_ENV` environment variable is `dev` or `development` */
export const isDevelopment = nodeENV === "dev" || nodeENV === "development";
/** Detect if `NODE_ENV` environment variable is `test` */
export const isTest = nodeENV === "test" || toBoolean(env.TEST);

View File

@@ -1,4 +1,4 @@
import { isTest } from "std-env";
import { isTest } from "../utils/env";
export function getIp(req: Request | Headers): string | null {
const testIP = "127.0.0.1";

View File

@@ -1,4 +1,4 @@
// import { env } from "std-env";
import { env } from "../utils/env";
import { BetterAuthError } from "../error";
function checkHasPath(url: string): boolean {
@@ -20,7 +20,6 @@ function withPath(url: string, path = "/api/auth") {
path = path.startsWith("/") ? path : `/${path}`;
return `${url}${path}`;
}
const env = process.env;
export function getBaseURL(url?: string, path?: string) {
if (url) {

12
pnpm-lock.yaml generated
View File

@@ -1617,9 +1617,6 @@ importers:
oslo:
specifier: ^1.2.1
version: 1.2.1
std-env:
specifier: ^3.7.0
version: 3.7.0
uncrypto:
specifier: ^0.1.3
version: 0.1.3
@@ -1651,9 +1648,12 @@ importers:
drizzle-orm:
specifier: ^0.33.0
version: 0.33.0(@cloudflare/workers-types@4.20241011.0)(@libsql/client@0.12.0)(@prisma/client@5.20.0(prisma@5.20.0))(@types/better-sqlite3@7.6.11)(@types/pg@8.11.10)(@types/react@18.3.9)(better-sqlite3@11.3.0)(bun-types@1.1.32)(kysely@0.27.4)(mysql2@3.11.3)(pg@8.13.0)(postgres@3.4.4)(prisma@5.20.0)(react@18.3.1)
expo-constants:
specifier: ~16.0.2
version: 16.0.2(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13))
expo-linking:
specifier: ~6.3.1
version: 6.3.1(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13))
expo-secure-store:
specifier: ~13.0.2
version: 13.0.2(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13))
expo-web-browser:
specifier: ~13.0.3
version: 13.0.3(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13))