This commit is contained in:
2025-10-11 13:36:27 +02:00
parent a21853c874
commit 6f6abef5d2
2 changed files with 134 additions and 25 deletions
+19 -3
View File
@@ -171,11 +171,13 @@ pub async fn logout(
jar: Option<TypedHeader<Cookie>>,
) -> AppResult<(HeaderMap, StatusCode)> {
let mut conn = state.db()?;
let now = Utc::now().naive_utc();
let mut rows_affected = 0;
if let Some(cookies) = jar {
if let Some(value) = cookies.get(REFRESH_COOKIE_NAME) {
let hashed = hash_refresh_token(value);
let now = Utc::now().naive_utc();
let _ = diesel::update(
rows_affected = diesel::update(
refresh_dsl::refresh_tokens
.filter(refresh_dsl::token_hash.eq(hashed))
.filter(refresh_dsl::user_id.eq(user.user_id))
@@ -185,10 +187,24 @@ pub async fn logout(
refresh_dsl::revoked_at.eq(now),
refresh_dsl::updated_at.eq(now),
))
.execute(&mut conn);
.execute(&mut conn)
.unwrap_or(0);
}
}
if rows_affected == 0 {
let _ = diesel::update(
refresh_dsl::refresh_tokens
.filter(refresh_dsl::user_id.eq(user.user_id))
.filter(refresh_dsl::revoked_at.is_null()),
)
.set((
refresh_dsl::revoked_at.eq(now),
refresh_dsl::updated_at.eq(now),
))
.execute(&mut conn);
}
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, build_clear_refresh_cookie(&state));
Ok((headers, StatusCode::NO_CONTENT))
+115 -22
View File
@@ -1149,6 +1149,9 @@ const Sidebar = ({
function App({ routeFolderId = null, routeDocumentId = null, navigate }) {
const [token, setToken] = useState(() => window.localStorage.getItem('paperless_token') || '');
const [status, setStatus] = useState(null);
const setStatusMessage = useCallback((message, variant = 'info') => {
setStatus(message ? { message, variant } : null);
}, []);
const [loading, setLoading] = useState(false);
const [folderNodes, setFolderNodes] = useState(() => {
const rootNode = createRootNode();
@@ -1165,6 +1168,23 @@ function App({ routeFolderId = null, routeDocumentId = null, navigate }) {
const [focusedDocumentId, setFocusedDocumentId] = useState(routeDocumentId);
const [documentDetails, setDocumentDetails] = useState(() => new Map());
const documentDetailsRef = useRef(documentDetails);
const tokenRef = useRef(token);
const refreshPromiseRef = useRef(null);
const refreshAccessToken = useCallback(async () => {
console.log('[Auth] Attempting to refresh access token…');
try {
const { data } = await api.post('/auth/refresh');
if (data?.access_token) {
setToken(data.access_token);
console.log('[Auth] Access token refreshed');
return data.access_token;
}
throw new Error('Missing access token in refresh response');
} catch (error) {
console.warn('[Auth] Failed to refresh access token', error);
throw error;
}
}, []);
const [searchResults, setSearchResults] = useState(null);
const [tags, setTags] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
@@ -1219,6 +1239,10 @@ function App({ routeFolderId = null, routeDocumentId = null, navigate }) {
documentDetailsRef.current = documentDetails;
}, [documentDetails]);
useEffect(() => {
tokenRef.current = token;
}, [token]);
useEffect(() => {
if (token) {
api.defaults.headers.common.Authorization = `Bearer ${token}`;
@@ -1229,9 +1253,70 @@ function App({ routeFolderId = null, routeDocumentId = null, navigate }) {
}
}, [token]);
const setStatusMessage = useCallback((message, variant = 'info') => {
setStatus(message ? { message, variant } : null);
}, []);
useEffect(() => {
const requestInterceptor = api.interceptors.request.use((config) => {
const currentToken = tokenRef.current;
if (currentToken) {
config.headers = config.headers || {};
if (!config.headers.Authorization) {
config.headers.Authorization = `Bearer ${currentToken}`;
}
}
return config;
});
const responseInterceptor = api.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error;
if (!response || !config) {
return Promise.reject(error);
}
const status = response.status;
const url = typeof config.url === 'string' ? config.url : '';
const isAuthRoute = url.includes('/auth/login') || url.includes('/auth/refresh');
if (status === 401 && !config._retry && !isAuthRoute) {
console.warn('[Auth] 401 received for', url, '- attempting token refresh');
if (!refreshPromiseRef.current) {
refreshPromiseRef.current = (async () => {
try {
return await refreshAccessToken();
} finally {
refreshPromiseRef.current = null;
}
})();
}
try {
const newToken = await refreshPromiseRef.current;
if (!newToken) {
throw new Error('No token returned from refresh');
}
config._retry = true;
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
console.log('[Auth] Retrying original request', url);
return api(config);
} catch (refreshError) {
console.warn('[Auth] Refresh failed, clearing session');
setToken('');
setStatusMessage('Session expired. Please log in again.', 'error');
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
},
);
return () => {
api.interceptors.request.eject(requestInterceptor);
api.interceptors.response.eject(responseInterceptor);
};
}, [refreshAccessToken, setStatusMessage]);
useEffect(() => {
if (!selectedDocumentIds.length) {
@@ -2877,25 +2962,33 @@ const handleDownload = useCallback(
[setStatusMessage],
);
const handleLogout = useCallback(() => {
setToken('');
setStatusMessage('Logged out.', 'info');
setFolderNodes(new Map([[createRootNode().id, createRootNode()]]));
setFolderContents(new Map());
setSelectedFolder('root');
setCurrentFolder(null);
setCurrentSubfolders([]);
setDocuments([]);
setSelectedDocumentIds([]);
setFocusedDocumentId(null);
selectionAnchorRef.current = null;
setDraggedDocumentIds([]);
setDocumentDetails(new Map());
setSearchResults(null);
setTags([]);
setSearchQuery('');
setActiveTagFilters([]);
initializedRef.current = false;
const handleLogout = useCallback(async () => {
try {
setLoading(true);
await api.post('/auth/logout');
} catch (error) {
console.warn('[Auth] Failed to revoke refresh token during logout', error);
} finally {
setLoading(false);
setToken('');
setStatusMessage('Logged out.', 'info');
setFolderNodes(new Map([[createRootNode().id, createRootNode()]]));
setFolderContents(new Map());
setSelectedFolder('root');
setCurrentFolder(null);
setCurrentSubfolders([]);
setDocuments([]);
setSelectedDocumentIds([]);
setFocusedDocumentId(null);
selectionAnchorRef.current = null;
setDraggedDocumentIds([]);
setDocumentDetails(new Map());
setSearchResults(null);
setTags([]);
setSearchQuery('');
setActiveTagFilters([]);
initializedRef.current = false;
}
}, [setStatusMessage]);
const handleBulkReanalyze = useCallback(async () => {