Skip to content

Commit 10403df

Browse files
rdmclin2sudongyuer
authored andcommitted
server: sync from feat/mobile-app (exclude apps/mobile)
1 parent 509619b commit 10403df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+7442
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"desktop:prepare-dist": "tsx scripts/electronWorkflow/moveNextStandalone.ts",
5252
"dev": "next dev --turbopack -p 3010",
5353
"dev:desktop": "next dev --turbopack -p 3015",
54+
"dev:mobile": "next dev --turbopack -p 3020",
5455
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx && prettier -c --write locales/**/*",
5556
"docs:seo": "lobe-seo && npm run lint:mdx",
5657
"e2e": "playwright test",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
2+
import type { NextRequest } from 'next/server';
3+
4+
import { pino } from '@/libs/logger';
5+
import { createLambdaContext } from '@/libs/trpc/lambda/context';
6+
import { mobileRouter } from '@/server/routers/mobile';
7+
8+
const handler = (req: NextRequest) =>
9+
fetchRequestHandler({
10+
/**
11+
* @link https://trpc.io/docs/v11/context
12+
*/
13+
createContext: () => createLambdaContext(req),
14+
15+
endpoint: '/trpc/mobile',
16+
17+
onError: ({ error, path, type }) => {
18+
pino.info(`Error in tRPC handler (mobile) on path: ${path}, type: ${type}`);
19+
console.error(error);
20+
},
21+
22+
req,
23+
responseMeta({ ctx }) {
24+
const headers = ctx?.resHeaders;
25+
26+
return { headers };
27+
},
28+
router: mobileRouter,
29+
});
30+
31+
export { handler as GET, handler as POST };

src/app/[variants]/oauth/consent/[uid]/components/OAuthApplicationLogo.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,20 @@ const useStyles = createStyles(({ css, token }) => ({
1111
connector: css`
1212
width: 40px;
1313
height: 40px;
14+
15+
@media (max-width: 768px) {
16+
width: 32px;
17+
height: 32px;
18+
}
1419
`,
1520
connectorLine: css`
1621
width: 32px;
1722
height: 1px;
1823
background-color: ${token.colorBorderSecondary};
24+
25+
@media (max-width: 768px) {
26+
width: 24px;
27+
}
1928
`,
2029
icon: css`
2130
overflow: hidden;
@@ -29,6 +38,12 @@ const useStyles = createStyles(({ css, token }) => ({
2938
border-radius: 16px;
3039
3140
background-color: ${token.colorBgElevated};
41+
42+
@media (max-width: 768px) {
43+
width: 48px;
44+
height: 48px;
45+
border-radius: 12px;
46+
}
3247
`,
3348
lobeIcon: css`
3449
overflow: hidden;
@@ -41,6 +56,11 @@ const useStyles = createStyles(({ css, token }) => ({
4156
border-radius: 50%;
4257
4358
background-color: ${token.colorBgElevated};
59+
60+
@media (max-width: 768px) {
61+
width: 48px;
62+
height: 48px;
63+
}
4464
`,
4565
}));
4666

@@ -55,13 +75,27 @@ const OAuthApplicationLogo = memo<OAuthApplicationLogoProps>(
5575
const { styles, theme } = useStyles();
5676
return isFirstParty ? (
5777
<Flexbox align={'center'} gap={12} horizontal justify={'center'}>
58-
<Image alt={clientDisplayName} height={64} src={logoUrl!} unoptimized width={64} />
78+
<Image
79+
alt={clientDisplayName}
80+
height={64}
81+
src={logoUrl!}
82+
style={{ height: 'auto', maxWidth: '100%' }}
83+
unoptimized
84+
width={64}
85+
/>
5986
</Flexbox>
6087
) : (
6188
<Flexbox align={'center'} gap={12} horizontal justify={'center'}>
6289
<div className={styles.icon}>
6390
{logoUrl ? (
64-
<Image alt={clientDisplayName} height={56} src={logoUrl} unoptimized width={56} />
91+
<Image
92+
alt={clientDisplayName}
93+
height={56}
94+
src={logoUrl}
95+
style={{ height: 'auto', maxWidth: '100%' }}
96+
unoptimized
97+
width={56}
98+
/>
6599
) : (
66100
<Icon icon={ServerIcon} />
67101
)}
@@ -72,7 +106,11 @@ const OAuthApplicationLogo = memo<OAuthApplicationLogoProps>(
72106
</Center>
73107
<div className={styles.connectorLine} />
74108
<div className={styles.lobeIcon}>
75-
<ProductLogo height={48} style={{ objectFit: 'cover' }} width={48} />
109+
<ProductLogo
110+
height={48}
111+
style={{ height: 'auto', maxWidth: '100%', objectFit: 'cover' }}
112+
width={48}
113+
/>
76114
</div>
77115
</Flexbox>
78116
);

src/features/ModelSelect/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ const ModelSelect = memo<ModelSelectProps>(({ value, onChange, showAbility = tru
4646
provider: provider.id,
4747
value: `${provider.id}/${model.id}`,
4848
}));
49-
5049
if (enabledList.length === 1) {
5150
const provider = enabledList[0];
5251

src/libs/oidc-provider/config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ export const defaultClients: ClientMetadata[] = [
3535
// 标记为公共客户端客户端,无密钥
3636
token_endpoint_auth_method: 'none',
3737
},
38+
{
39+
application_type: 'native', // 移动端使用 native 类型
40+
client_id: 'lobehub-mobile',
41+
client_name: 'LobeHub Mobile',
42+
// 支持授权码流程和刷新令牌
43+
grant_types: ['authorization_code', 'refresh_token'],
44+
logo_uri: 'https://hub-apac-1.lobeobjects.space/docs/73f69adfa1b802a0e250f6ff9d62f70b.png',
45+
// 移动端不需要 post_logout_redirect_uris,因为注销通常在应用内处理
46+
post_logout_redirect_uris: [],
47+
// 移动端使用自定义 URL Scheme
48+
redirect_uris: ['com.lobehub.app://auth/callback'],
49+
response_types: ['code'],
50+
// 公共客户端,无密钥
51+
token_endpoint_auth_method: 'none',
52+
},
3853
];
3954

4055
/**

src/libs/oidc-provider/jwt.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export const validateOIDCJWT = async (token: string) => {
106106

107107
// 提取用户信息
108108
const userId = payload.sub;
109-
const clientId = payload.aud;
109+
const clientId = payload.client_id;
110+
const aud = payload.aud;
110111

111112
if (!userId) {
112113
throw new TRPCError({
@@ -119,7 +120,7 @@ export const validateOIDCJWT = async (token: string) => {
119120
clientId,
120121
payload,
121122
tokenData: {
122-
aud: clientId,
123+
aud: aud,
123124
client_id: clientId,
124125
exp: payload.exp,
125126
iat: payload.iat,

src/libs/oidc-provider/provider.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { serverDBEnv } from '@/config/db';
77
import { UserModel } from '@/database/models/user';
88
import { appEnv } from '@/envs/app';
99
import { getJWKS } from '@/libs/oidc-provider/jwt';
10+
import { normalizeLocale } from '@/locales/resources';
1011

1112
import { DrizzleAdapter } from './adapter';
1213
import { defaultClaims, defaultClients, defaultScopes } from './config';
@@ -76,6 +77,9 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
7677
// 1. 客户端配置
7778
clients: defaultClients,
7879

80+
// 新增:确保 ID Token 包含所有 scope 对应的 claims,而不仅仅是 openid scope
81+
conformIdTokenClaims: false,
82+
7983
// 7. Cookie 配置
8084
cookies: {
8185
keys: cookieKeys,
@@ -93,6 +97,7 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
9397
resourceIndicators: {
9498
defaultResource: () => API_AUDIENCE,
9599
enabled: true,
100+
96101
getResourceServerInfo: (ctx, resourceIndicator) => {
97102
logProvider('getResourceServerInfo called with indicator: %s', resourceIndicator); // <-- 添加这行日志
98103
if (resourceIndicator === API_AUDIENCE) {
@@ -107,6 +112,8 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
107112
logProvider('Indicator does not match API_AUDIENCE, throwing InvalidTarget.'); // <-- 添加这行日志
108113
throw new errors.InvalidTarget();
109114
},
115+
// 当客户端使用刷新令牌请求新的访问令牌但没有指定资源时,授权服务器会检查原始授权中包含的所有资源,并将这些资源用于新的访问令牌。这提供了一种便捷的方式来维持授权一致性,而不需要客户端在每次刷新时重新指定所有资源
116+
useGrantedResource: () => true,
110117
},
111118
revocation: { enabled: true },
112119
rpInitiatedLogout: { enabled: true },
@@ -195,7 +202,25 @@ export const createOIDCProvider = async (db: LobeChatDatabase): Promise<Provider
195202
// ---> 添加日志 <---
196203
logProvider('interactions.url function called');
197204
logProvider('Interaction details: %O', interaction);
198-
const interactionUrl = `/oauth/consent/${interaction.uid}`;
205+
206+
// 读取 OIDC 请求中的 ui_locales 参数(空格分隔的语言优先级)
207+
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
208+
const uiLocalesRaw = (interaction.params?.ui_locales || ctx.oidc?.params?.ui_locales) as
209+
| string
210+
| undefined;
211+
212+
let query = '';
213+
if (uiLocalesRaw) {
214+
// 取第一个优先语言,规范化到站点支持的标签
215+
const first = uiLocalesRaw.split(/[\s,]+/).find(Boolean);
216+
const hl = normalizeLocale(first);
217+
query = `?hl=${encodeURIComponent(hl)}`;
218+
logProvider('Detected ui_locales=%s -> using hl=%s', uiLocalesRaw, hl);
219+
} else {
220+
logProvider('No ui_locales provided in authorization request');
221+
}
222+
223+
const interactionUrl = `/oauth/consent/${interaction.uid}${query}`;
199224
logProvider('Generated interaction URL: %s', interactionUrl);
200225
// ---> 添加日志结束 <---
201226
return interactionUrl;

src/middleware.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,32 @@ const defaultMiddleware = (request: NextRequest) => {
143143

144144
url.pathname = nextPathname;
145145

146-
return NextResponse.rewrite(url, { status: 200 });
146+
// build rewrite response first
147+
const rewrite = NextResponse.rewrite(url, { status: 200 });
148+
149+
// If locale explicitly provided via query (?hl=), persist it in cookie when user has no prior preference
150+
if (explicitlyLocale) {
151+
const existingLocale = request.cookies.get(LOBE_LOCALE_COOKIE)?.value as Locales | undefined;
152+
if (!existingLocale) {
153+
rewrite.cookies.set(LOBE_LOCALE_COOKIE, explicitlyLocale, {
154+
// 90 days is a balanced persistence for locale preference
155+
maxAge: 60 * 60 * 24 * 90,
156+
157+
path: '/',
158+
sameSite: 'lax',
159+
secure: process.env.NODE_ENV === 'production',
160+
});
161+
logDefault('Persisted explicit locale to cookie (no prior cookie): %s', explicitlyLocale);
162+
} else {
163+
logDefault(
164+
'Locale cookie exists (%s), skip overwrite with %s',
165+
existingLocale,
166+
explicitlyLocale,
167+
);
168+
}
169+
}
170+
171+
return rewrite;
147172
};
148173

149174
const isPublicRoute = createRouteMatcher([
@@ -158,6 +183,8 @@ const isPublicRoute = createRouteMatcher([
158183
'/login',
159184
'/signup',
160185
// oauth
186+
// Make only the consent view public (GET page), not other oauth paths
187+
'/oauth/consent/(.*)',
161188
'/oidc/handoff',
162189
'/oidc/token',
163190
]);
@@ -212,6 +239,11 @@ const nextAuthMiddleware = NextAuth.auth((req) => {
212239
logNextAuth('Request a protected route, redirecting to sign-in page');
213240
const nextLoginUrl = new URL('/next-auth/signin', req.nextUrl.origin);
214241
nextLoginUrl.searchParams.set('callbackUrl', req.nextUrl.href);
242+
const hl = req.nextUrl.searchParams.get('hl');
243+
if (hl) {
244+
nextLoginUrl.searchParams.set('hl', hl);
245+
logNextAuth('Preserving locale to sign-in: hl=%s', hl);
246+
}
215247
return Response.redirect(nextLoginUrl);
216248
}
217249
logNextAuth('Request a free route but not login, allow visit without auth header');

0 commit comments

Comments
 (0)