forked from wangziqi/gongxue-base
feat: add taro h5 runtime deployment config
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"portal": "platform-admin",
|
||||
"apiBaseUrl": "https://api.gongxue100.com",
|
||||
"supabaseUrl": "https://auth.gongxue100.com",
|
||||
"supabasePublishableKey": "replace-with-supabase-publishable-key",
|
||||
"tenantCode": ""
|
||||
}
|
||||
7
apps/taro/deploy/h5-student.runtime-config.example.json
Normal file
7
apps/taro/deploy/h5-student.runtime-config.example.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"portal": "student",
|
||||
"apiBaseUrl": "https://api.gongxue100.com",
|
||||
"supabaseUrl": "https://auth.gongxue100.com",
|
||||
"supabasePublishableKey": "replace-with-supabase-publishable-key",
|
||||
"tenantCode": ""
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"portal": "tenant-admin",
|
||||
"apiBaseUrl": "https://api.gongxue100.com",
|
||||
"supabaseUrl": "https://auth.gongxue100.com",
|
||||
"supabasePublishableKey": "replace-with-supabase-publishable-key",
|
||||
"tenantCode": ""
|
||||
}
|
||||
@@ -1,10 +1,77 @@
|
||||
export type Portal = 'student' | 'tenant-admin' | 'platform-admin';
|
||||
|
||||
export interface AppEnv {
|
||||
portal: Portal;
|
||||
apiBaseUrl: string;
|
||||
supabaseUrl: string;
|
||||
supabasePublishableKey: string;
|
||||
tenantCode: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfigInput {
|
||||
portal?: string;
|
||||
apiBaseUrl?: string;
|
||||
supabaseUrl?: string;
|
||||
supabasePublishableKey?: string;
|
||||
tenantCode?: string;
|
||||
TARO_APP_PORTAL?: string;
|
||||
TARO_APP_API_BASE_URL?: string;
|
||||
TARO_APP_SUPABASE_URL?: string;
|
||||
TARO_APP_SUPABASE_PUBLISHABLE_KEY?: string;
|
||||
TARO_APP_TENANT_CODE?: string;
|
||||
}
|
||||
|
||||
declare const process: {
|
||||
env: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
export const appEnv = {
|
||||
const forbiddenFrontendKeys = [
|
||||
'SUPABASE_SERVICE_ROLE_KEY',
|
||||
'SUPABASE_SECRET_KEY',
|
||||
'DATABASE_URL',
|
||||
'ALIYUN_OSS_ACCESS_KEY_SECRET',
|
||||
'TENCENT_COS_SECRET_KEY',
|
||||
'WECHAT_PAY_PRIVATE_KEY',
|
||||
'ALIPAY_APP_PRIVATE_KEY',
|
||||
'AUTH_SESSION_SECRET',
|
||||
'PLATFORM_ADMIN_API_KEY',
|
||||
] as const;
|
||||
|
||||
const allowedRuntimeConfigKeys = [
|
||||
'portal',
|
||||
'apiBaseUrl',
|
||||
'supabaseUrl',
|
||||
'supabasePublishableKey',
|
||||
'tenantCode',
|
||||
'TARO_APP_PORTAL',
|
||||
'TARO_APP_API_BASE_URL',
|
||||
'TARO_APP_SUPABASE_URL',
|
||||
'TARO_APP_SUPABASE_PUBLISHABLE_KEY',
|
||||
'TARO_APP_TENANT_CODE',
|
||||
] as const;
|
||||
|
||||
function normalizeString(value: unknown) {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalizePortal(value: unknown): Portal | null {
|
||||
if (value === 'student' || value === 'tenant-admin' || value === 'platform-admin') return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function assertNoForbiddenKeys(input: Record<string, unknown>, source: string) {
|
||||
const leaked = forbiddenFrontendKeys.filter(key => Object.prototype.hasOwnProperty.call(input, key));
|
||||
if (leaked.length) {
|
||||
throw new Error(`Forbidden secret key in ${source}: ${leaked.join(', ')}`);
|
||||
}
|
||||
const allowed = new Set<string>(allowedRuntimeConfigKeys);
|
||||
const unknown = Object.keys(input).filter(key => !allowed.has(key));
|
||||
if (unknown.length) {
|
||||
throw new Error(`Unknown key in ${source}: ${unknown.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const appEnv: AppEnv = {
|
||||
portal: (process.env.TARO_APP_PORTAL || 'student') as Portal,
|
||||
apiBaseUrl: process.env.TARO_APP_API_BASE_URL || 'http://127.0.0.1:8787',
|
||||
supabaseUrl: process.env.TARO_APP_SUPABASE_URL || '',
|
||||
@@ -12,19 +79,66 @@ export const appEnv = {
|
||||
tenantCode: process.env.TARO_APP_TENANT_CODE || '',
|
||||
};
|
||||
|
||||
let runtimeConfigPromise: Promise<AppEnv> | null = null;
|
||||
|
||||
export function applyRuntimeConfig(input: RuntimeConfigInput, source = 'runtime config') {
|
||||
assertNoForbiddenKeys(input as Record<string, unknown>, source);
|
||||
|
||||
const portal = normalizePortal(input.portal || input.TARO_APP_PORTAL);
|
||||
if (portal) appEnv.portal = portal;
|
||||
|
||||
const apiBaseUrl = normalizeString(input.apiBaseUrl || input.TARO_APP_API_BASE_URL);
|
||||
if (apiBaseUrl) appEnv.apiBaseUrl = apiBaseUrl.replace(/\/+$/, '');
|
||||
|
||||
const supabaseUrl = normalizeString(input.supabaseUrl || input.TARO_APP_SUPABASE_URL);
|
||||
if (supabaseUrl) appEnv.supabaseUrl = supabaseUrl.replace(/\/+$/, '');
|
||||
|
||||
const supabasePublishableKey = normalizeString(input.supabasePublishableKey || input.TARO_APP_SUPABASE_PUBLISHABLE_KEY);
|
||||
if (supabasePublishableKey) appEnv.supabasePublishableKey = supabasePublishableKey;
|
||||
|
||||
const tenantCode = normalizeString(input.tenantCode || input.TARO_APP_TENANT_CODE);
|
||||
if (tenantCode) appEnv.tenantCode = tenantCode;
|
||||
|
||||
return appEnv;
|
||||
}
|
||||
|
||||
export async function loadRuntimeConfig() {
|
||||
if (process.env.TARO_ENV !== 'h5' || typeof window === 'undefined' || typeof window.fetch !== 'function') {
|
||||
return appEnv;
|
||||
}
|
||||
|
||||
const runtimeConfigUrl = `${window.location.origin}/runtime-config.json`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await window.fetch(runtimeConfigUrl, {
|
||||
cache: 'no-store',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
} catch {
|
||||
return appEnv;
|
||||
}
|
||||
if (!response.ok) return appEnv;
|
||||
|
||||
const text = (await response.text()).trim();
|
||||
if (!text || !text.startsWith('{')) return appEnv;
|
||||
|
||||
let config: RuntimeConfigInput;
|
||||
try {
|
||||
config = JSON.parse(text) as RuntimeConfigInput;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid Taro runtime-config.json: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
return applyRuntimeConfig(config, 'runtime-config.json');
|
||||
}
|
||||
|
||||
export function ensureRuntimeConfigLoaded() {
|
||||
if (!runtimeConfigPromise) runtimeConfigPromise = loadRuntimeConfig();
|
||||
return runtimeConfigPromise;
|
||||
}
|
||||
|
||||
export function assertFrontendSecretsAreAbsent() {
|
||||
const forbidden = [
|
||||
'SUPABASE_SERVICE_ROLE_KEY',
|
||||
'SUPABASE_SECRET_KEY',
|
||||
'DATABASE_URL',
|
||||
'ALIYUN_OSS_ACCESS_KEY_SECRET',
|
||||
'TENCENT_COS_SECRET_KEY',
|
||||
'WECHAT_PAY_PRIVATE_KEY',
|
||||
'ALIPAY_APP_PRIVATE_KEY',
|
||||
'AUTH_SESSION_SECRET',
|
||||
'PLATFORM_ADMIN_API_KEY',
|
||||
];
|
||||
const leaked = forbidden.filter(key => process.env[key]);
|
||||
const leaked = forbiddenFrontendKeys.filter(key => process.env[key]);
|
||||
if (leaked.length) {
|
||||
throw new Error(`Forbidden secret env in Taro build: ${leaked.join(', ')}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
import { appEnv, assertFrontendSecretsAreAbsent } from '@/env';
|
||||
import { appEnv, assertFrontendSecretsAreAbsent, ensureRuntimeConfigLoaded } from '@/env';
|
||||
import { resolveTenant } from '@/services/api';
|
||||
import './index.css';
|
||||
|
||||
@@ -22,7 +22,8 @@ export default function BootstrapPage() {
|
||||
|
||||
useEffect(() => {
|
||||
assertFrontendSecretsAreAbsent();
|
||||
resolveTenant({ host: hostFromRuntime() })
|
||||
ensureRuntimeConfigLoaded()
|
||||
.then(() => resolveTenant({ host: hostFromRuntime() }))
|
||||
.then(() => {
|
||||
setStatus('租户解析完成');
|
||||
Taro.redirectTo({ url: landingPath() });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { appEnv } from '@/env';
|
||||
import { appEnv, ensureRuntimeConfigLoaded } from '@/env';
|
||||
import type { ApiErrorPayload, ApiSession, TenantContext } from '@/types';
|
||||
import { getStorage, removeStorage, setStorage } from './storage';
|
||||
|
||||
@@ -67,6 +67,7 @@ export async function apiRequest<T>(
|
||||
headers?: Record<string, string>;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
await ensureRuntimeConfigLoaded();
|
||||
const tenant = getTenantContext();
|
||||
const session = getSession();
|
||||
const token = options.token ?? session?.token ?? null;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||
import { appEnv } from '@/env';
|
||||
import { appEnv, ensureRuntimeConfigLoaded } from '@/env';
|
||||
|
||||
let client: SupabaseClient | null = null;
|
||||
let clientKey = '';
|
||||
|
||||
export function getSupabaseClient() {
|
||||
if (!appEnv.supabaseUrl || !appEnv.supabasePublishableKey) return null;
|
||||
if (!client) {
|
||||
const nextClientKey = `${appEnv.supabaseUrl}|${appEnv.supabasePublishableKey}`;
|
||||
if (!client || clientKey !== nextClientKey) {
|
||||
client = createClient(appEnv.supabaseUrl, appEnv.supabasePublishableKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
@@ -13,12 +15,18 @@ export function getSupabaseClient() {
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
clientKey = nextClientKey;
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function ensureSupabaseClient() {
|
||||
await ensureRuntimeConfigLoaded();
|
||||
return getSupabaseClient();
|
||||
}
|
||||
|
||||
export async function getSupabaseAccessToken() {
|
||||
const supabase = getSupabaseClient();
|
||||
const supabase = await ensureSupabaseClient();
|
||||
if (!supabase) return null;
|
||||
const { data } = await supabase.auth.getSession();
|
||||
return data.session?.access_token || null;
|
||||
|
||||
Reference in New Issue
Block a user