Skip to content

Commit 4406426

Browse files
committed
feat: implement session_jwt and enhance auth
1 parent 7fd377b commit 4406426

File tree

9 files changed

+147
-58
lines changed

9 files changed

+147
-58
lines changed

application/app.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import os
12
import platform
3+
import uuid
24

35
import dotenv
46
from flask import Flask, jsonify, redirect, request
57
from jose import jwt
68

7-
from application.auth import get_or_create_user_id, handle_auth
9+
from application.auth import handle_auth
810

911
from application.core.logging_config import setup_logging
1012

@@ -38,10 +40,22 @@
3840
celery.config_from_object("application.celeryconfig")
3941
api.init_app(app)
4042

43+
if settings.AUTH_TYPE in ("simple_jwt", "session_jwt") and not settings.JWT_SECRET_KEY:
44+
key_file = ".jwt_secret_key"
45+
try:
46+
with open(key_file, "r") as f:
47+
settings.JWT_SECRET_KEY = f.read().strip()
48+
except FileNotFoundError:
49+
new_key = os.urandom(32).hex()
50+
with open(key_file, "w") as f:
51+
f.write(new_key)
52+
settings.JWT_SECRET_KEY = new_key
53+
except Exception as e:
54+
raise RuntimeError(f"Failed to setup JWT_SECRET_KEY: {e}")
55+
4156
SIMPLE_JWT_TOKEN = None
4257
if settings.AUTH_TYPE == "simple_jwt":
43-
user_id = get_or_create_user_id()
44-
payload = {"sub": user_id}
58+
payload = {"sub": "local"}
4559
SIMPLE_JWT_TOKEN = jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm="HS256")
4660
print(f"Generated Simple JWT Token: {SIMPLE_JWT_TOKEN}")
4761

@@ -54,13 +68,33 @@ def home():
5468
return "Welcome to DocsGPT Backend!"
5569

5670

71+
@app.route("/api/config")
72+
def get_config():
73+
response = {
74+
"auth_type": settings.AUTH_TYPE,
75+
"requires_auth": settings.AUTH_TYPE in ["simple_jwt", "session_jwt"],
76+
}
77+
return jsonify(response)
78+
79+
80+
@app.route("/api/generate_token")
81+
def generate_token():
82+
if settings.AUTH_TYPE == "session_jwt":
83+
new_user_id = str(uuid.uuid4())
84+
token = jwt.encode(
85+
{"sub": new_user_id}, settings.JWT_SECRET_KEY, algorithm="HS256"
86+
)
87+
return jsonify({"token": token})
88+
return jsonify({"error": "Token generation not allowed in current auth mode"}), 400
89+
90+
5791
@app.before_request
5892
def authenticate_request():
5993
if request.method == "OPTIONS":
6094
return "", 200
6195

6296
decoded_token = handle_auth(request)
63-
if "message" in decoded_token:
97+
if not decoded_token:
6498
request.decoded_token = None
6599
elif "error" in decoded_token:
66100
return jsonify(decoded_token), 401

application/auth.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import uuid
2-
31
from jose import jwt
42

53
from application.core.settings import settings
64

75

86
def handle_auth(request, data={}):
9-
if settings.AUTH_TYPE == "simple_jwt":
7+
if settings.AUTH_TYPE in ["simple_jwt", "session_jwt"]:
108
jwt_token = request.headers.get("Authorization")
119
if not jwt_token:
12-
return {"message": "Missing Authorization header"}
10+
return None
1311

1412
jwt_token = jwt_token.replace("Bearer ", "")
1513

@@ -22,18 +20,9 @@ def handle_auth(request, data={}):
2220
)
2321
return decoded_token
2422
except Exception as e:
25-
return {"message": f"Authentication error: {str(e)}"}
23+
return {
24+
"message": f"Authentication error: {str(e)}",
25+
"error": "invalid_token",
26+
}
2627
else:
2728
return {"sub": "local"}
28-
29-
30-
def get_or_create_user_id():
31-
try:
32-
with open(settings.USER_ID_FILE, "r") as f:
33-
user_id = f.read().strip()
34-
return user_id
35-
except FileNotFoundError:
36-
user_id = str(uuid.uuid4())
37-
with open(settings.USER_ID_FILE, "w") as f:
38-
f.write(user_id)
39-
return user_id

application/core/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ class Settings(BaseSettings):
100100
FLASK_DEBUG_MODE: bool = False
101101

102102
JWT_SECRET_KEY: str = ""
103-
USER_ID_FILE: str = os.path.join(current_dir, "user_id.txt")
104103

105104

106105
path = Path(__file__).parent.parent.absolute()

frontend/src/App.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1-
import { Routes, Route } from 'react-router-dom';
2-
import Navigation from './Navigation';
3-
import Conversation from './conversation/Conversation';
1+
import './locale/i18n';
2+
3+
import { useState } from 'react';
4+
import { Outlet, Route, Routes } from 'react-router-dom';
5+
46
import About from './About';
7+
import Spinner from './components/Spinner';
8+
import Conversation from './conversation/Conversation';
9+
import { SharedConversation } from './conversation/SharedConversation';
10+
import { useDarkTheme, useMediaQuery } from './hooks';
11+
import useTokenAuth from './hooks/useTokenAuth';
12+
import Navigation from './Navigation';
513
import PageNotFound from './PageNotFound';
6-
import { useMediaQuery } from './hooks';
7-
import { useState } from 'react';
814
import Setting from './settings';
9-
import './locale/i18n';
10-
import { Outlet } from 'react-router-dom';
11-
import { SharedConversation } from './conversation/SharedConversation';
12-
import { useDarkTheme } from './hooks';
15+
16+
function AuthWrapper({ children }: { children: React.ReactNode }) {
17+
const { isAuthLoading } = useTokenAuth();
18+
19+
if (isAuthLoading) {
20+
return (
21+
<div className="h-screen flex items-center justify-center">
22+
<Spinner />
23+
</div>
24+
);
25+
}
26+
return <>{children}</>;
27+
}
1328

1429
function MainLayout() {
1530
const { isMobile } = useMediaQuery();
@@ -39,7 +54,13 @@ export default function App() {
3954
return (
4055
<div className="h-full relative overflow-auto">
4156
<Routes>
42-
<Route element={<MainLayout />}>
57+
<Route
58+
element={
59+
<AuthWrapper>
60+
<MainLayout />
61+
</AuthWrapper>
62+
}
63+
>
4364
<Route index element={<Conversation />} />
4465
<Route path="/about" element={<About />} />
4566
<Route path="/settings" element={<Setting />} />

frontend/src/Navigation.tsx

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import ConversationTile from './conversation/ConversationTile';
2929
import { useDarkTheme, useMediaQuery } from './hooks';
3030
import useDefaultDocument from './hooks/useDefaultDocument';
31+
import useTokenAuth from './hooks/useTokenAuth';
3132
import DeleteConvModal from './modals/DeleteConvModal';
3233
import JWTModal from './modals/JWTModal';
3334
import { ActiveState, Doc } from './models/misc';
@@ -72,10 +73,10 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
7273
const { t } = useTranslation();
7374
const isApiKeySet = useSelector(selectApiKeyStatus);
7475

76+
const { showTokenModal, handleTokenSubmit } = useTokenAuth();
77+
7578
const [uploadModalState, setUploadModalState] =
7679
useState<ActiveState>('INACTIVE');
77-
const [authKeyModalState, setAuthKeyModalState] =
78-
useState<ActiveState>('INACTIVE');
7980

8081
const navRef = useRef(null);
8182

@@ -204,15 +205,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
204205
setNavOpen(!isMobile);
205206
}, [isMobile]);
206207

207-
useEffect(() => {
208-
const authToken = localStorage.getItem('authToken');
209-
if (!authToken) {
210-
setAuthKeyModalState('ACTIVE');
211-
}
212-
}, []);
213-
214208
useDefaultDocument();
215-
216209
return (
217210
<>
218211
{!navOpen && (
@@ -485,8 +478,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
485478
></Upload>
486479
)}
487480
<JWTModal
488-
modalState={authKeyModalState}
489-
setModalState={setAuthKeyModalState}
481+
modalState={showTokenModal ? 'ACTIVE' : 'INACTIVE'}
482+
handleTokenSubmit={handleTokenSubmit}
490483
/>
491484
</>
492485
);

frontend/src/api/endpoints.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const endpoints = {
22
USER: {
3+
CONFIG: '/api/config',
4+
NEW_TOKEN: '/api/generate_token',
35
DOCS: '/api/sources',
46
DOCS_CHECK: '/api/docs_check',
57
DOCS_PAGINATED: '/api/sources/paginated',

frontend/src/api/services/userService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import apiClient from '../client';
22
import endpoints from '../endpoints';
33

44
const userService = {
5+
getConfig: (): Promise<any> => apiClient.get(endpoints.USER.CONFIG, null),
6+
getNewToken: (): Promise<any> =>
7+
apiClient.get(endpoints.USER.NEW_TOKEN, null),
58
getDocs: (token: string | null): Promise<any> =>
69
apiClient.get(`${endpoints.USER.DOCS}`, token),
710
getDocsWithPagination: (query: string, token: string | null): Promise<any> =>

frontend/src/hooks/useTokenAuth.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { useDispatch, useSelector } from 'react-redux';
3+
4+
import userService from '../api/services/userService';
5+
import { selectToken, setToken } from '../preferences/preferenceSlice';
6+
7+
export default function useAuth() {
8+
const dispatch = useDispatch();
9+
const token = useSelector(selectToken);
10+
const [authType, setAuthType] = useState(null);
11+
const [showTokenModal, setShowTokenModal] = useState(false);
12+
const [isAuthLoading, setIsAuthLoading] = useState(true);
13+
const isGeneratingToken = useRef(false);
14+
15+
const generateNewToken = async () => {
16+
if (isGeneratingToken.current) return;
17+
isGeneratingToken.current = true;
18+
const response = await userService.getNewToken();
19+
const { token: newToken } = await response.json();
20+
localStorage.setItem('authToken', newToken);
21+
dispatch(setToken(newToken));
22+
setIsAuthLoading(false);
23+
return newToken;
24+
};
25+
26+
useEffect(() => {
27+
const initializeAuth = async () => {
28+
try {
29+
const configRes = await userService.getConfig();
30+
const config = await configRes.json();
31+
setAuthType(config.auth_type);
32+
33+
if (config.auth_type === 'session_jwt' && !token) {
34+
await generateNewToken();
35+
} else if (config.auth_type === 'simple_jwt' && !token) {
36+
setShowTokenModal(true);
37+
setIsAuthLoading(false);
38+
} else {
39+
setIsAuthLoading(false);
40+
}
41+
} catch (error) {
42+
console.error('Auth initialization failed:', error);
43+
setIsAuthLoading(false);
44+
}
45+
};
46+
initializeAuth();
47+
}, []);
48+
49+
const handleTokenSubmit = (enteredToken: string) => {
50+
localStorage.setItem('authToken', enteredToken);
51+
dispatch(setToken(enteredToken));
52+
setShowTokenModal(false);
53+
};
54+
return { authType, showTokenModal, isAuthLoading, token, handleTokenSubmit };
55+
}

frontend/src/modals/JWTModal.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,23 @@ import { useDispatch } from 'react-redux';
33

44
import Input from '../components/Input';
55
import { ActiveState } from '../models/misc';
6-
import { setToken } from '../preferences/preferenceSlice';
76
import WrapperModal from './WrapperModal';
87

98
type JWTModalProps = {
109
modalState: ActiveState;
11-
setModalState: (state: ActiveState) => void;
10+
handleTokenSubmit: (enteredToken: string) => void;
1211
};
1312

14-
export default function JWTModal({ modalState, setModalState }: JWTModalProps) {
15-
const dispatch = useDispatch();
13+
export default function JWTModal({
14+
modalState,
15+
handleTokenSubmit,
16+
}: JWTModalProps) {
1617
const [jwtToken, setJwtToken] = useState<string>('');
1718

18-
const handleSaveToken = () => {
19-
if (jwtToken) {
20-
localStorage.setItem('authToken', jwtToken);
21-
dispatch(setToken(jwtToken));
22-
setModalState('INACTIVE');
23-
}
24-
};
25-
2619
if (modalState !== 'ACTIVE') return null;
2720

2821
return (
29-
<WrapperModal close={() => setModalState('INACTIVE')} className="p-4">
22+
<WrapperModal className="p-4" isPerformingTask={true} close={() => {}}>
3023
<div className="mb-6">
3124
<span className="text-lg text-jet dark:text-bright-gray">
3225
Add JWT Token
@@ -44,7 +37,7 @@ export default function JWTModal({ modalState, setModalState }: JWTModalProps) {
4437
</div>
4538
<button
4639
disabled={jwtToken.length === 0}
47-
onClick={handleSaveToken}
40+
onClick={handleTokenSubmit.bind(null, jwtToken)}
4841
className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
4942
>
5043
Save Token

0 commit comments

Comments
 (0)