Skip to content

Commit

Permalink
🐛 fix: rewrite to local container in docker deployment mode (#5910)
Browse files Browse the repository at this point in the history
* make rewrite with local

* try longitude

* refactor the isMobile condition

* support use longitude as theme detector

* improve dockerfile

* improve geo

* improve geo

* improve geo

* vercel functions

* clean log

* skip api request at first

* turn back status in rewrite
  • Loading branch information
arvinxx authored Feb 9, 2025
1 parent b71206d commit f399197
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 35 deletions.
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID

ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}"

# Make the middleware rewrite through local as default
# refs: https://github.com/lobehub/lobe-chat/issues/5876
ENV MIDDLEWARE_REWRITE_THROUGH_LOCAL="1"

# Sentry
ENV NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \
SENTRY_ORG="" \
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile.database
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID

ENV NEXT_PUBLIC_BASE_PATH="${NEXT_PUBLIC_BASE_PATH}"

# Make the middleware rewrite through local as default
# refs: https://github.com/lobehub/lobe-chat/issues/5876
ENV MIDDLEWARE_REWRITE_THROUGH_LOCAL="1"

ENV NEXT_PUBLIC_SERVICE_MODE="${NEXT_PUBLIC_SERVICE_MODE:-server}" \
NEXT_PUBLIC_ENABLE_NEXT_AUTH="${NEXT_PUBLIC_ENABLE_NEXT_AUTH:-1}" \
APP_URL="http://app.com" \
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,15 @@
"@trpc/server": "next",
"@vercel/analytics": "^1.4.1",
"@vercel/edge-config": "^1.4.0",
"@vercel/functions": "^2",
"@vercel/speed-insights": "^1.1.0",
"ahooks": "^3.8.4",
"ai": "^3.4.33",
"antd": "^5.23.0",
"antd-style": "^3.7.1",
"brotli-wasm": "^3.0.1",
"chroma-js": "^3.1.2",
"countries-and-timezones": "^3.7.2",
"dayjs": "^1.11.13",
"debug": "^4.4.0",
"dexie": "^3.2.7",
Expand Down
5 changes: 2 additions & 3 deletions src/app/[variants]/(main)/(mobile)/me/data/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { redirect } from 'next/navigation';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { isMobileDevice } from '@/utils/server/responsive';
import { RouteVariants } from '@/utils/server/routeVariants';

import Category from './features/Category';
Expand All @@ -17,8 +16,8 @@ export const generateMetadata = async (props: DynamicLayoutProps) => {
});
};

const Page = async () => {
const mobile = await isMobileDevice();
const Page = async (props: DynamicLayoutProps) => {
const mobile = await RouteVariants.getIsMobile(props);

if (!mobile) return redirect('/chat');

Expand Down
5 changes: 2 additions & 3 deletions src/app/[variants]/(main)/settings/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { isMobileDevice } from '@/utils/server/responsive';
import { RouteVariants } from '@/utils/server/routeVariants';

import Page from './index';
Expand All @@ -16,8 +15,8 @@ export const generateMetadata = async (props: DynamicLayoutProps) => {
});
};

export default async () => {
const isMobile = await isMobileDevice();
export default async (props: DynamicLayoutProps) => {
const isMobile = await RouteVariants.getIsMobile(props);

return <Page mobile={isMobile} />;
};
6 changes: 3 additions & 3 deletions src/app/[variants]/(main)/settings/sync/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { serverFeatureFlags } from '@/config/featureFlags';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { gerServerDeviceInfo, isMobileDevice } from '@/utils/server/responsive';
import { gerServerDeviceInfo } from '@/utils/server/responsive';
import { RouteVariants } from '@/utils/server/routeVariants';

import Page from './index';
Expand All @@ -18,11 +18,11 @@ export const generateMetadata = async (props: DynamicLayoutProps) => {
url: '/settings/sync',
});
};
export default async () => {
export default async (props: DynamicLayoutProps) => {
const enableWebrtc = serverFeatureFlags().enableWebrtc;
if (!enableWebrtc) return notFound();

const isMobile = await isMobileDevice();
const isMobile = await RouteVariants.getIsMobile(props);
const { os, browser } = await gerServerDeviceInfo();

return <Page browser={browser} mobile={isMobile} os={os} />;
Expand Down
3 changes: 3 additions & 0 deletions src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const getAppConfig = () => {

APP_URL: z.string().optional(),
VERCEL_EDGE_CONFIG: z.string().optional(),
MIDDLEWARE_REWRITE_THROUGH_LOCAL: z.boolean().optional(),

CDN_USE_GLOBAL: z.boolean().optional(),
CUSTOM_FONT_FAMILY: z.string().optional(),
Expand Down Expand Up @@ -80,6 +81,8 @@ export const getAppConfig = () => {
VERCEL_EDGE_CONFIG: process.env.VERCEL_EDGE_CONFIG,

APP_URL,
MIDDLEWARE_REWRITE_THROUGH_LOCAL: process.env.MIDDLEWARE_REWRITE_THROUGH_LOCAL === '1',

CUSTOM_FONT_FAMILY: process.env.CUSTOM_FONT_FAMILY,
CUSTOM_FONT_URL: process.env.CUSTOM_FONT_URL,
CDN_USE_GLOBAL: process.env.CDN_USE_GLOBAL === '1',
Expand Down
45 changes: 20 additions & 25 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextRequest, NextResponse } from 'next/server';
import { UAParser } from 'ua-parser-js';
import urlJoin from 'url-join';

import { appEnv } from '@/config/app';
import { authEnv } from '@/config/auth';
import { LOBE_THEME_APPEARANCE } from '@/const/theme';
import NextAuthEdge from '@/libs/next-auth/edge';
import { Locales } from '@/locales/resources';
import { parseBrowserLanguage } from '@/utils/locale';
import { parseDefaultThemeFromCountry } from '@/utils/server/geo';
import { RouteVariants } from '@/utils/server/routeVariants';

import { OAUTH_AUTHORIZED } from './const/auth';
Expand Down Expand Up @@ -37,30 +40,17 @@ export const config = {
],
};

const parseDefaultThemeFromTime = (request: NextRequest) => {
// 获取经度信息,Next.js 会自动解析 geo 信息到请求对象中
const longitude = 'geo' in request && (request.geo as any)?.longitude;

if (typeof longitude === 'number') {
// 计算时区偏移(每15度经度对应1小时)
// 东经为正,西经为负
const offsetHours = Math.round(longitude / 15);

// 计算当地时间
const localHour = (new Date().getUTCHours() + offsetHours + 24) % 24;
console.log(`[theme] localHour: ${localHour}`);
const defaultMiddleware = (request: NextRequest) => {
const url = new URL(request.url);

// 6点到18点之间返回 light 主题
return localHour >= 6 && localHour < 18 ? 'light' : 'dark';
// skip all api requests
if (['/api', '/trpc', '/webapi'].some((path) => url.pathname.startsWith(path))) {
return NextResponse.next();
}

return 'light';
};

const defaultMiddleware = (request: NextRequest) => {
// 1. 从 cookie 中读取用户偏好
const theme =
request.cookies.get(LOBE_THEME_APPEARANCE)?.value || parseDefaultThemeFromTime(request);
request.cookies.get(LOBE_THEME_APPEARANCE)?.value || parseDefaultThemeFromCountry(request);

// if it's a new user, there's no cookie
// So we need to use the fallback language parsed by accept-language
Expand All @@ -80,19 +70,24 @@ const defaultMiddleware = (request: NextRequest) => {
theme,
});

const url = new URL(request.url);

// skip all api requests
if (['/api', '/trpc', '/webapi'].some((path) => url.pathname.startsWith(path))) {
return NextResponse.next();
// if app is in docker, rewrite to self container
// https://github.com/lobehub/lobe-chat/issues/5876
if (appEnv.MIDDLEWARE_REWRITE_THROUGH_LOCAL) {
url.protocol = 'http';
url.host = '127.0.0.1';
url.port = process.env.PORT || '3210';
}

// refs: https://github.com/lobehub/lobe-chat/pull/5866
// new handle segment rewrite: /${route}${originalPathname}
// / -> /zh-CN__0__dark
// /discover -> /zh-CN__0__dark/discover
const nextPathname = `/${route}` + (url.pathname === '/' ? '' : url.pathname);
console.log(`[rewrite] ${url.pathname} -> ${nextPathname}`);
const nextURL = appEnv.MIDDLEWARE_REWRITE_THROUGH_LOCAL
? urlJoin(url.origin, nextPathname)
: nextPathname;

console.log(`[rewrite] ${url.pathname} -> ${nextURL}`);

url.pathname = nextPathname;

Expand Down
39 changes: 39 additions & 0 deletions src/utils/server/geo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { geolocation } from '@vercel/functions';
import { getCountry } from 'countries-and-timezones';
import { NextRequest } from 'next/server';

export const parseDefaultThemeFromCountry = (request: NextRequest) => {
// 1. 从请求头中获取国家代码
const geo = geolocation(request);

const countryCode =
geo?.country ||
request.headers.get('x-vercel-ip-country') || // Vercel
request.headers.get('cf-ipcountry') || // Cloudflare
request.headers.get('x-zeabur-ip-country') || // Zeabur
request.headers.get('x-country-code'); // Netlify

// 如果没有获取到国家代码,直接返回 light 主题
if (!countryCode) return 'light';

// 2. 获取国家的时区信息
const country = getCountry(countryCode);

// 如果找不到国家信息或该国家没有时区信息,返回 light 主题
if (!country?.timezones?.length) return 'light';

// 3. 获取该国家的第一个 时区下的当前时间
const localTime = new Date().toLocaleString('en-US', {
hour: 'numeric',
hour12: false,
timeZone: country.timezones[0],
});

// 4. 解析小时数并确定主题
const localHour = parseInt(localTime);
// console.log(
// `[theme] Country: ${countryCode}, Timezone: ${country.timezones[0]}, LocalHour: ${localHour}`,
// );

return localHour >= 6 && localHour < 18 ? 'light' : 'dark';
};
2 changes: 1 addition & 1 deletion src/utils/server/responsive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { UAParser } from 'ua-parser-js';
/**
* check mobile device in server
*/
export const isMobileDevice = async () => {
const isMobileDevice = async () => {
if (typeof process === 'undefined') {
throw new Error('[Server method] you are importing a server-only module outside of server');
}
Expand Down

0 comments on commit f399197

Please sign in to comment.