logout
This commit is contained in:
@@ -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
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user