Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/v3' into v3
Browse files Browse the repository at this point in the history
  • Loading branch information
AprilNEA committed Dec 17, 2023
2 parents e1869bd + 7ef9764 commit 0ecaecb
Show file tree
Hide file tree
Showing 28 changed files with 564 additions and 675 deletions.
27 changes: 20 additions & 7 deletions packages/backend/src/libs/jwt/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ export type JWTPayload = JWTPayloadDefault & {
export class JwtService {
constructor(private configService: ConfigService) {}

async getKey(type: 'public' | 'private') {
async getKey(type: 'public' | 'private', refresh = false) {
switch (this.configService.get('jwt').algorithm) {
case 'HS256':
if (refresh) {
const refreshSecret = this.configService.get('jwt')?.refreshSecret;
if (refreshSecret) {
return new TextEncoder().encode(refreshSecret);
}
}
return new TextEncoder().encode(this.configService.get('jwt').secret);
case 'ES256':
if (type === 'private') {
return await importJWK(
Expand All @@ -32,23 +40,28 @@ export class JwtService {
JSON.parse(this.configService.get('jwt').publicKey),
'ES256',
);
case 'HS256':
return new TextEncoder().encode(this.configService.get('jwt').secret);
}
}

async sign(payload: JWTPayload): Promise<string> {
async sign(
payload: JWTPayload,
duration = 7 * 24 * 60 * 60,
refresh = false,
): Promise<string> {
const iat = Math.floor(Date.now() / 1000); // Not before: Now
const exp = iat + 7 * 24 * 60 * 60; // One week
return await new SignJWT({ ...payload })
const exp = iat + duration; // One week
return await new SignJWT({
...payload,
...(refresh ? { sub: 'refresh' } : {}),
})
.setProtectedHeader({
alg: this.configService.get('jwt').algorithm,
typ: 'JWT',
})
.setExpirationTime(exp)
.setIssuedAt(iat)
.setNotBefore(iat)
.sign(await this.getKey('private'));
.sign(await this.getKey('private', refresh));
}

async verify(token: string): Promise<JWTPayload> {
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ConfigService } from '@/common/config';
import { BizException } from '@/common/exceptions/biz.exception';
import { Payload, Public } from '@/common/guards/auth.guard';
import { ZodValidationPipe } from '@/common/pipes/zod';
import { JWTPayload } from '@/libs/jwt/jwt.service';
import { WechatService } from '@/modules/auth/wechat.service';

import { AuthDTO, ErrorCodeEnum } from 'shared';
Expand Down Expand Up @@ -185,4 +186,12 @@ export class AuthController {
data: roles,
};
}

@Get('refresh')
async refresh(@Payload() payload: JWTPayload) {
return {
success: true,
...(await this.authService.refresh(payload.id, payload.role)),
};
}
}
15 changes: 10 additions & 5 deletions packages/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,19 @@ export class AuthService {
* 1. 检查是否绑定账户
* 2. 检查是否设置密码
*/
async #signWithCheck(user: any): Promise<{
token: string;
status: IAccountStatus;
}> {
async #signWithCheck(user: any) {
let status: IAccountStatus = 'ok';
if (!user.email && !user.phone) {
status = 'bind';
} else if (!user.newPassword) {
status = 'password';
}
return {
token: await this.jwt.sign({ id: user.id, role: user.role }),
sessionToken: await this.jwt.sign({ id: user.id, role: user.role }),
refreshToken: await this.jwt.sign(
{ id: user.id, role: user.role },
30 * 24 * 60 * 60,
),
status,
};
}
Expand Down Expand Up @@ -342,4 +343,8 @@ export class AuthService {
},
});
}

async refresh(userId: number, userRole: string) {
return this.#signWithCheck({ id: userId, role: userRole });
}
}
2 changes: 1 addition & 1 deletion packages/backend/src/modules/chat/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class ChatController {
modelId: data.modelId,
input: data.content,
messages: chatSession.messages,
// key,
topic: chatSession.topic,
});
}
}
25 changes: 22 additions & 3 deletions packages/backend/src/modules/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,16 +234,15 @@ export class ChatService {

async summarizeTopic(message: string, sessionId: string) {
const result = (await this.#chat({
input: `Give me the topic title about the following text by use as few words as possible.
input: `summarize the text in 4 words.
Text: """
${message}
"""`,
model: 'gpt-3.5-turbo',
histories: [
{
role: 'system',
content:
'You are an assistant who uses a few words to summarize conversations',
content: 'You are an assistant who uses 4 words to summarize text',
},
],
stream: false,
Expand All @@ -263,6 +262,7 @@ ${message}
input,
modelId,
messages, // key,
topic,
}: {
userId: number;
sessionId: string;
Expand All @@ -273,6 +273,7 @@ ${message}
messages: ChatMessage[];
/* Request API Key */
// key: string;
topic?: string;
}) {
const { name: model } = await this.prisma.client.model.findUniqueOrThrow({
where: { id: modelId },
Expand Down Expand Up @@ -338,6 +339,24 @@ ${message}
}),
]);
subscriber.complete();
/* 首次对话自动总结对话,
* First conversation automatically summarizes the conversation
*/
if (!topic) {
await this.summarizeTopic(
[
...histories,
{ role: 'user', content: input },
{
role: 'assistant',
content: generated,
},
]
.map((m) => `${m.role}: ${m.content}`)
.join('\n'),
sessionId,
);
}
}
})();
});
Expand Down
3 changes: 1 addition & 2 deletions packages/frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
},
"rules": {
"@next/next/no-html-link-for-pages": [
"error",
"packages/frontend/src/app"
"off"
]
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/themes": "^2.0.0",
"@svgr/webpack": "^8.1.0",
"@tremor/react": "^3.11.1",
"@types/ramda": "^0.29.7",
"clsx": "^2.0.0",
"emoji-picker-react": "^4.4.7",
Expand Down
5 changes: 2 additions & 3 deletions packages/frontend/src/app/(admin-end)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import useSWR from 'swr';

import { Box, Grid, Text } from '@radix-ui/themes';
import { Card, LineChart, Title } from '@tremor/react';

import { Loading } from '@/components/loading';
import { useStore } from '@/store';
Expand Down Expand Up @@ -32,14 +31,14 @@ export default function DashboardIndex() {
return (
<Grid columns="6" gap="3" width="auto">
{analytics.map((item) => (
<Card key={item.key}>
<Box key={item.key}>
<Text as="p" size="2" weight="medium">
{item.label}
</Text>
<Text as="p" size="6" weight="bold">
{data[item.key]}
</Text>
</Card>
</Box>
))}
</Grid>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/app/(admin-end)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import '@radix-ui/themes/styles.css';
import '@/styles/dashboard.css';

export const metadata = {
title: 'Dashboard | ChatGPT Admin ',
title: 'Dashboard | ChatGPT Admin Web',
description: 'Manage Dashboard for ChatGPT Admin Web',
};

export default function AdminEndLayout({
Expand Down
8 changes: 4 additions & 4 deletions packages/frontend/src/app/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const weChatOauthRedirectUrl =
/* 验证码登录/注册 */
function ValidateCodeLogin() {
const router = useRouter();
const { fetcher, setSessionToken } = useStore();
const { fetcher, setAuthToken } = useStore();
const [identity, setIdentity] = useState('');
const [ifCodeSent, setIfCodeSent] = useState(false);
const [validateCode, setValidateCode] = useState('');
Expand Down Expand Up @@ -65,7 +65,7 @@ function ValidateCodeLogin() {
.then((res) => res.json())
.then((res) => {
if (res.success) {
setSessionToken(res.token);
setAuthToken(res.sessionToken, res.refreshToken);
return router.push('/');
} else {
showToast(res.message);
Expand Down Expand Up @@ -151,7 +151,7 @@ function ValidateCodeLogin() {
/* 密码登录 */
const PasswordLogin: React.FC = () => {
const router = useRouter();
const { fetcher, setSessionToken } = useStore();
const { fetcher, setAuthToken } = useStore();
const [identity, setIdentity] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, handleSubmit] = usePreventFormSubmit();
Expand All @@ -165,7 +165,7 @@ const PasswordLogin: React.FC = () => {
.then((res) => res.json())
.then((res) => {
if (res.success) {
setSessionToken(res.token);
setAuthToken(res.sessionToken, res.refreshToken);
router.push('/');
} else {
router.refresh();
Expand Down
25 changes: 25 additions & 0 deletions packages/frontend/src/app/provider/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useRouter } from 'next/navigation';
import { Middleware, SWRHook } from 'swr';

import { useStore } from '@/store';

import { ErrorCodeEnum } from 'shared';

const authMiddleware: Middleware =
(useSWRNext: SWRHook) => (key, fetcher, config) => {
// Handle the next middleware, or the `useSWR` hook if this is the last one.
const swr = useSWRNext(key, fetcher, config);

// After hook runs...
// @ts-ignore
if (swr.data?.success === false) {
// @ts-ignore
if (swr.data?.code === ErrorCodeEnum.AuthFail) {
useStore().clearAuthToken();
useRouter().push('/auth');
throw new Error('Auth failed');
}
}
return swr;
};
export default authMiddleware;
63 changes: 50 additions & 13 deletions packages/frontend/src/app/provider/client-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { usePathname, useRouter } from 'next/navigation';
import React, { useCallback, useEffect, useState } from 'react';
import { SWRConfig } from 'swr';

import authMiddleware from '@/app/provider/auth-middleware';
import { Loading } from '@/components/loading';
import { useStore } from '@/store';

Expand All @@ -14,7 +15,8 @@ export function AuthProvider({
children: React.ReactNode;
admin?: boolean;
}) {
const { sessionToken, setSessionToken } = useStore();
const { sessionToken, refreshToken, setAuthToken, clearAuthToken } =
useStore();

const pathname = usePathname();
const router = useRouter();
Expand All @@ -25,12 +27,36 @@ export function AuthProvider({
if (!sessionToken) return false;

try {
const payload = JSON.parse(atob(sessionToken.split('.')[1]));
if (admin) {
const payload = JSON.parse(atob(sessionToken.split('.')[1]));
if (payload.role !== 'Admin') {
return false;
}
}
return payload.exp >= Date.now() / 1000;
} catch (e) {
return false;
}
}

function refreshSession() {
if (!refreshToken) return false;

try {
const payload = JSON.parse(atob(refreshToken.split('.')[1]));
if (payload.exp <= Date.now() / 1000) {
return false;
}
fetch('/api/auth/refresh', {
method: 'GET',
headers: {
Authorization: `Bearer ${refreshToken}`,
},
})
.then((res) => res.json())
.then((data) => {
setAuthToken(data.sessionToken, data.refreshSession);
});
return true;
} catch (e) {
return false;
Expand All @@ -40,21 +66,30 @@ export function AuthProvider({
const validate = useCallback(() => {
const isValid = validateSession();
if (!isValid) {
if (admin) {
return router.push('/');
}
return router.push('/auth');
} else {
setIsValidating(false);
if (pathname.startsWith('/auth')) {
return router.push('/');
if (!refreshSession()) {
clearAuthToken();
if (admin) {
return router.push('/');
}
return router.push('/auth');
}
}
}, [pathname, validateSession, router, admin]);
setIsValidating(false);
if (pathname.startsWith('/auth')) {
return router.push('/');
}
}, [
router,
pathname,
clearAuthToken,
validateSession,
refreshSession,
admin,
]);

useEffect(() => {
validate();
}, [pathname, validate]);
}, [pathname, sessionToken, refreshToken, validate]);

if (isValidating) {
return <Loading />;
Expand All @@ -65,6 +100,8 @@ export function AuthProvider({

export function SWRProvider({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig>
<SWRConfig value={{ use: [authMiddleware], provider: () => new Map() }}>
{children}
</SWRConfig>
);
}
Loading

0 comments on commit 0ecaecb

Please sign in to comment.