fix: 添加better auth 接口配置

This commit is contained in:
imeepos
2026-01-13 14:58:48 +08:00
parent 02d0c807cd
commit 9fa65a9ac3
10 changed files with 720 additions and 7376 deletions

5
app/auth.tsx Normal file
View File

@@ -0,0 +1,5 @@
export default () => {
return null;
}

View File

@@ -1,27 +1,114 @@
import { createAuthClient } from "better-auth/react";
import { usernameClient, phoneNumberClient, emailOTPClient, genericOAuthClient, adminClient, organizationClient, jwtClient } from 'better-auth/client/plugins'
import * as SecureStore from "expo-secure-store";
import { expoClient } from "@better-auth/expo/client";
import 'reflect-metadata'
import loomartPlugin from './plugins/loomart-plugin';
import { expoClient } from '@better-auth/expo/client'
import { createSkerClientPlugin } from '@repo/sdk'
import {
adminClient,
emailOTPClient,
genericOAuthClient,
jwtClient,
organizationClient,
phoneNumberClient,
usernameClient,
} from 'better-auth/client/plugins'
import { createAuthClient } from 'better-auth/react'
import { fetchWithLogger } from './fetch-logger'
import type { Subscription } from './plugins/stripe'
import { stripeClient } from './plugins/stripe-plugin'
import { storage } from './storage'
import type { ApiError } from './types'
export interface CreditBalance {
tokenUsage: number
remainingTokenBalance: number
currentCreditBalance: number
unitPrice: number
}
export interface SubscriptionListItem extends Subscription {
type?: 'metered' | 'licenced'
limits?: Record<string, number>
creditBalance?: CreditBalance
highestTierPlan?: string
highestTierCredits?: number
}
export interface SubscriptionListParams {
query?: {
referenceId?: string
}
}
export interface SubscriptionRestoreParams {
referenceId?: string
subscriptionId?: string
}
export interface SubscriptionRestoreResponse {
success: boolean
subscription: {
id: string
stripeSubscriptionId: string
cancelAtPeriodEnd: boolean
periodEnd?: Date
status: string
}
}
export interface BillingPortalParams {
locale?: string
referenceId?: string
returnUrl?: string
}
export interface BillingPortalResponse {
url: string
redirect: boolean
}
export interface ISubscription {
list: (params?: SubscriptionListParams) => Promise<{ data?: SubscriptionListItem[]; error?: ApiError }>
restore: (params?: SubscriptionRestoreParams) => Promise<{ data?: SubscriptionRestoreResponse; error?: ApiError }>
billingPortal: (params?: BillingPortalParams) => Promise<{ data?: BillingPortalResponse; error?: ApiError }>
}
export const getAuthToken = async () => (await storage.getItem('bestaibest.better-auth.session_token')) || ''
export const setAuthToken = async (token: string) => {
await storage.setItem(`bestaibest.better-auth.session_token`, token)
}
export const authClient = createAuthClient({
baseURL: "http://localhost:14333/api/auth",
trustedOrigins: ["duooomi://"],
storage: SecureStore,
scheme: "duooomi",
plugins: [
loomartPlugin(),
usernameClient(),
phoneNumberClient(),
emailOTPClient(),
genericOAuthClient(),
jwtClient(),
adminClient(),
organizationClient(),
expoClient({
storage: SecureStore
})
],
});
baseURL: 'https://api.mixvideo.bowong.cc',
trustedOrigins: ['duooomi://', 'https://api.mixvideo.bowong.cc'],
storage,
scheme: 'duooomi',
fetchOptions: {
auth: {
type: 'Bearer',
token: async () => {
const Authorization = await getAuthToken()
return Authorization
},
},
customFetchImpl: fetchWithLogger as typeof fetch,
},
plugins: [
createSkerClientPlugin(),
stripeClient(),
usernameClient(),
phoneNumberClient(),
emailOTPClient(),
genericOAuthClient(),
jwtClient(),
adminClient(),
organizationClient(),
expoClient({
storage: storage as any,
}),
],
})
export const { signIn, signUp, signOut, useSession, $Infer, loomart, admin } = authClient;
export const { signIn, signUp, signOut, useSession, $Infer, admin, forgetPassword, resetPassword, emailOtp } =
authClient
export const subscription: ISubscription = Reflect.get(authClient, 'subscription')

101
lib/fetch-logger.ts Normal file
View File

@@ -0,0 +1,101 @@
import { router } from 'expo-router'
import { storage } from './storage'
interface FetchLoggerOptions {
enableLogging?: boolean
logRequest?: boolean
logResponse?: boolean
logError?: boolean
}
const defaultOptions: FetchLoggerOptions = {
enableLogging: __DEV__,
logRequest: true,
logResponse: true,
logError: true,
}
const originalFetch = global.fetch
export const createFetchWithLogger = (options: FetchLoggerOptions = {}) => {
const config = { ...defaultOptions, ...options }
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const startTime = Date.now()
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
const method = init?.method || 'GET'
try {
const token = await storage.getItem('token')
if (token) {
init = {
...init,
headers: {
...init?.headers,
authorization: `Bearer ${token}`,
},
}
}
const response = await originalFetch(input, init)
if (response.status === 401) {
console.warn('🔐 401 未授权,跳转到登录页')
// router.replace('/auth')
router.push('/auth')
}
return response
} catch (error) {
const duration = Date.now() - startTime
if (config.enableLogging && config.logError) {
console.group(`❌ [FETCH 错误] ${method} ${url}`)
console.error('📍 URL:', url)
console.error('🔧 Method:', method)
console.error('⏱️ Duration:', `${duration}ms`)
if (init?.headers) {
console.error('📋 Request Headers:', JSON.stringify(init.headers, null, 2))
}
if (init?.body) {
try {
const bodyContent = typeof init.body === 'string' ? init.body : JSON.stringify(init.body)
console.error('📦 Request Body:', bodyContent)
} catch (e) {
console.error('📦 Request Body: [无法序列化]')
}
}
console.error('💥 Error Type:', error?.constructor?.name || 'Unknown')
console.error('💥 Error Message:', error instanceof Error ? error.message : String(error))
if (error instanceof Error && error.stack) {
console.error('📚 Stack Trace:', error.stack)
}
if (error && typeof error === 'object') {
console.error('🔍 Error Details:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2))
}
console.groupEnd()
}
throw error
}
}
}
export const fetchWithLogger = createFetchWithLogger()
export const setupGlobalFetchLogger = (options?: FetchLoggerOptions) => {
const loggedFetch = createFetchWithLogger(options)
global.fetch = loggedFetch as typeof fetch
return () => {
global.fetch = originalFetch
}
}

View File

@@ -1,11 +1,11 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { NativeModules, Platform } from 'react-native'
import { storage } from './storage'
// 导入语言资源
import zhCN from '../locales/zh-CN.json'
import enUS from '../locales/en-US.json'
import zhCN from '../locales/zh-CN.json'
// 定义支持的语言
export const SUPPORTED_LANGUAGES = {
@@ -52,7 +52,7 @@ const languageDetector = {
detect: async (callback: (lng: string) => void) => {
try {
// 1. 优先使用用户保存的语言设置
const savedLanguage = await AsyncStorage.getItem('app_language')
const savedLanguage = await storage.getItem('app_language')
if (savedLanguage) {
// 兼容旧的语言代码格式
if (savedLanguage === 'zh' || savedLanguage === 'zh-CN') {
@@ -63,7 +63,7 @@ const languageDetector = {
return
}
}
// 2. 如果没有保存的语言,尝试使用系统语言
const systemLang = getSystemLanguage()
callback(systemLang)
@@ -73,11 +73,11 @@ const languageDetector = {
callback('zh-CN')
}
},
init: () => {},
init: () => { },
cacheUserLanguage: async (lng: string) => {
try {
// AsyncStorage 在 Android 和 iOS 上都正常工作
await AsyncStorage.setItem('app_language', lng)
await storage.setItem('app_language', lng)
} catch (error) {
console.error('Error saving language:', error)
}
@@ -97,25 +97,25 @@ i18n.use(languageDetector)
translation: enUS,
},
},
// 默认语言
fallbackLng: 'zh-CN',
// 调试模式(开发环境开启)
debug: false,
// 兼容性配置:使用 v3 格式处理复数,避免 Android 端 Intl.PluralRules 不支持的问题
compatibilityJSON: 'v3',
// 插值配置
interpolation: {
escapeValue: false, // React已经处理了XSS防护
},
// 命名空间配置
defaultNS: 'translation',
ns: ['translation'],
// 键值分隔符
keySeparator: '.',
nsSeparator: ':',

View File

@@ -1,7 +0,0 @@
import type { loomart } from "./loomart";
export default () => {
return {
id: "loomart",
$InferServerPlugin: {} as ReturnType<typeof loomart>,
}
}

7333
lib/plugins/loomart.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
import type { BetterAuthClientPlugin } from "better-auth";
import type { stripe } from "./stripe";
export const stripeClient = <
O extends {
subscription: boolean;
},
>(
options?: O,
) => {
return {
id: "stripe-client",
$InferServerPlugin: {} as ReturnType<
typeof stripe<
O["subscription"] extends true
? {
stripeClient: any;
stripeWebhookSecret: string;
subscription: {
enabled: true;
plans: [];
};
}
: {
stripeClient: any;
stripeWebhookSecret: string;
}
>
>,
pathMethods: {
"/subscription/restore": "POST",
"/subscription/billing-portal": "POST",
"/alipay/app-pay": "POST",
"/alipay/auth-info": "POST",
},
} satisfies BetterAuthClientPlugin;
};

422
lib/plugins/stripe.d.ts vendored Normal file
View File

@@ -0,0 +1,422 @@
import { GenericEndpointContext, User, Session, InferOptionSchema, BetterAuthPlugin } from 'better-auth';
import Stripe from 'stripe';
declare const subscriptions: {
subscription: {
fields: {
plan: {
type: "string";
required: true;
};
referenceId: {
type: "string";
required: true;
};
stripeCustomerId: {
type: "string";
required: false;
};
stripeSubscriptionId: {
type: "string";
required: false;
unique: true;
};
status: {
type: "string";
defaultValue: string;
};
periodStart: {
type: "date";
required: false;
};
periodEnd: {
type: "date";
required: false;
};
trialStart: {
type: "date";
required: false;
};
trialEnd: {
type: "date";
required: false;
};
cancelAtPeriodEnd: {
type: "boolean";
required: false;
defaultValue: false;
};
seats: {
type: "number";
required: false;
};
};
};
};
declare const user: {
user: {
fields: {
stripeCustomerId: {
type: "string";
required: false;
};
metadata: {
type: "json";
required: false;
};
};
};
};
type StripePlan = {
/**
* Monthly price id
*/
priceId?: string;
/**
* To use lookup key instead of price id
*
* https://docs.stripe.com/products-prices/
* manage-prices#lookup-keys
*/
lookupKey?: string;
/**
* A yearly discount price id
*
* useful when you want to offer a discount for
* yearly subscription
*/
annualDiscountPriceId?: string;
/**
* To use lookup key instead of price id
*
* https://docs.stripe.com/products-prices/
* manage-prices#lookup-keys
*/
annualDiscountLookupKey?: string;
/**
* Plan name
*/
name: string;
/**
* Limits for the plan
*/
limits?: Record<string, number>;
/**
* Plan group name
*
* useful when you want to group plans or
* when a user can subscribe to multiple plans.
*/
group?: string;
/**
* Free trial days
*/
freeTrial?: {
/**
* Number of days
*/
days: number;
/**
* A function that will be called when the trial
* starts.
*
* @param subscription
* @returns
*/
onTrialStart?: (subscription: Subscription) => Promise<void>;
/**
* A function that will be called when the trial
* ends
*
* @param subscription - Subscription
* @returns
*/
onTrialEnd?: (data: {
subscription: Subscription;
}, ctx: GenericEndpointContext) => Promise<void>;
/**
* A function that will be called when the trial
* expired.
* @param subscription - Subscription
* @returns
*/
onTrialExpired?: (subscription: Subscription, ctx: GenericEndpointContext) => Promise<void>;
};
};
type actionType = 'upgrade-subscription' | 'list-subscription' | 'cancel-subscription' | 'restore-subscription' | 'meter-event' | 'credit-topup' | 'adjust-meter-event' | 'meter-event-summary' | 'create-subscription' | 'get-pricing-table' | 'billing-portal';
interface Subscription {
/**
* Database identifier
*/
id: string;
/**
* The plan name
*/
plan: string;
/**
* Stripe customer id
*/
stripeCustomerId?: string;
/**
* Stripe subscription id
*/
stripeSubscriptionId?: string;
/**
* Trial start date
*/
trialStart?: Date;
/**
* Trial end date
*/
trialEnd?: Date;
/**
* Price Id for the subscription
*/
priceId?: string;
/**
* To what reference id the subscription belongs to
* @example
* - userId for a user
* - workspace id for a saas platform
* - website id for a hosting platform
*
* @default - userId
*/
referenceId: string;
/**
* Subscription status
*/
status: 'active' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'past_due' | 'paused' | 'trialing' | 'unpaid';
/**
* The billing cycle start date
*/
periodStart?: Date;
/**
* The billing cycle end date
*/
periodEnd?: Date;
/**
* Cancel at period end
*/
cancelAtPeriodEnd?: boolean;
/**
* A field to group subscriptions so you can have multiple subscriptions
* for one reference id
*/
groupId?: string;
/**
* Number of seats for the subscription (useful for team plans)
*/
seats?: number;
}
interface AlipayNotifyData {
notify_time: string;
notify_type: string;
notify_id: string;
app_id: string;
charset: string;
version: string;
sign_type: string;
sign: string;
trade_no: string;
out_trade_no: string;
out_biz_no?: string;
buyer_id?: string;
buyer_logon_id?: string;
seller_id?: string;
seller_email?: string;
trade_status: 'WAIT_BUYER_PAY' | 'TRADE_CLOSED' | 'TRADE_SUCCESS' | 'TRADE_FINISHED';
total_amount: string;
receipt_amount?: string;
invoice_amount?: string;
buyer_pay_amount?: string;
point_amount?: string;
refund_fee?: string;
subject?: string;
body?: string;
gmt_create?: string;
gmt_payment?: string;
gmt_refund?: string;
gmt_close?: string;
fund_bill_list?: string;
passback_params?: string;
voucher_detail_list?: string;
[key: string]: any;
}
interface StripeOptions {
/**
* Stripe Client
*/
stripeClient: Stripe;
/**
* Stripe Webhook Secret
*
* @description Stripe webhook secret key
*/
stripeWebhookSecret: string;
/**
* Enable customer creation when a user signs up
*/
createCustomerOnSignUp?: boolean;
/**
* A callback to run after a customer has been created
* @param customer - Customer Data
* @param stripeCustomer - Stripe Customer Data
* @returns
*/
onCustomerCreate?: (data: {
stripeCustomer: Stripe.Customer;
user: User & {
stripeCustomerId: string;
};
}, ctx: GenericEndpointContext) => Promise<void>;
/**
* A custom function to get the customer create
* params
* @param data - data containing user and session
* @returns
*/
getCustomerCreateParams?: (user: User, ctx: GenericEndpointContext) => Promise<Partial<Stripe.CustomerCreateParams>>;
/**
* Subscriptions
*/
subscription?: {
enabled: boolean;
/**
* Subscription Configuration
*/
/**
* List of plan
*/
plans: StripePlan[] | (() => StripePlan[] | Promise<StripePlan[]>);
/**
* Require email verification before a user is allowed to upgrade
* their subscriptions
*
* @default false
*/
requireEmailVerification?: boolean;
/**
* A callback to run after a user has subscribed to a package
* @param event - Stripe Event
* @param subscription - Subscription Data
* @returns
*/
onSubscriptionComplete?: (data: {
event: Stripe.Event;
stripeSubscription: Stripe.Subscription;
subscription: Subscription;
plan: StripePlan;
}, ctx: GenericEndpointContext) => Promise<void>;
/**
* A callback to run after a user is about to cancel their subscription
* @returns
*/
onSubscriptionUpdate?: (data: {
event: Stripe.Event;
subscription: Subscription;
}) => Promise<void>;
/**
* A callback to run after a user is about to cancel their subscription
* @returns
*/
onSubscriptionCancel?: (data: {
event?: Stripe.Event;
subscription: Subscription;
stripeSubscription: Stripe.Subscription;
cancellationDetails?: Stripe.Subscription.CancellationDetails | null;
}) => Promise<void>;
/**
* A function to check if the reference id is valid
* and belongs to the user
*
* @param data - data containing user, session and referenceId
* @param ctx - the context object
* @returns
*/
authorizeReference?: (data: {
user: User & Record<string, any>;
session: Session & Record<string, any>;
referenceId: string;
action: actionType;
}, ctx: GenericEndpointContext) => Promise<boolean>;
/**
* A callback to run after a user has deleted their subscription
* @returns
*/
onSubscriptionDeleted?: (data: {
event: Stripe.Event;
stripeSubscription: Stripe.Subscription;
subscription: Subscription;
}) => Promise<void>;
/**
* parameters for session create params
*
* @param data - data containing user, session and plan
* @param ctx - the context object
*/
getCheckoutSessionParams?: (data: {
user: User & Record<string, any>;
session: Session & Record<string, any>;
plan: StripePlan;
subscription: Subscription;
}, ctx: GenericEndpointContext) => Promise<{
params?: Stripe.Checkout.SessionCreateParams;
options?: Stripe.RequestOptions;
}> | {
params?: Stripe.Checkout.SessionCreateParams;
options?: Stripe.RequestOptions;
};
/**
* Enable organization subscription
*/
organization?: {
enabled: boolean;
};
};
/**
* A callback to run after a stripe event is received
* @param event - Stripe Event
* @returns
*/
onEvent?: (event: Stripe.Event) => Promise<void>;
/**
* A callback to run after a one-time payment checkout is completed
* (e.g., credit top-up)
* @param checkoutSession - Stripe Checkout Session
* @param ctx - Generic Endpoint Context
* @returns
*/
onCheckoutComplete?: (data: {
session: Stripe.Checkout.Session;
}, ctx: GenericEndpointContext) => Promise<void>;
/**
* Schema for the stripe plugin
*/
schema?: InferOptionSchema<typeof subscriptions & typeof user>;
/**
* Pricing table config
*/
pricingTable?: {
id: string;
key: string;
meteredPriceId: string;
};
/**
* Alipay Configuration
*/
alipay?: {
endpoint: string;
/**
* Callback when Alipay sends a webhook notification
* @param data The notification parameters from Alipay
*/
beforeNotify?: (data: AlipayNotifyData, ctx: GenericEndpointContext) => Promise<void>;
};
}
declare const stripe: <O extends StripeOptions>(options: O) => BetterAuthPlugin;
export { stripe };
export type { StripePlan, Subscription };

15
lib/storage.native.ts Normal file
View File

@@ -0,0 +1,15 @@
import * as SecureStore from "expo-secure-store";
export const storage = {
async getItem(key: string): Promise<string | null> {
return await SecureStore.getItemAsync(key);
},
async setItem(key: string, value: string): Promise<void> {
await SecureStore.setItemAsync(key, value);
},
async removeItem(key: string): Promise<void> {
await SecureStore.deleteItemAsync(key);
},
};

17
lib/storage.ts Normal file
View File

@@ -0,0 +1,17 @@
declare const window: any;
export const storage = {
async getItem(key: string): Promise<string | null> {
if (typeof window === "undefined") return null;
return window.localStorage.getItem(key);
},
async setItem(key: string, value: string): Promise<void> {
if (typeof window === "undefined") return;
window.localStorage.setItem(key, value);
},
async removeItem(key: string): Promise<void> {
if (typeof window === "undefined") return;
window.localStorage.removeItem(key);
},
};