mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[feat] Add jwt for forgotten; Fix styles; Refactor
This commit is contained in:
@ -187,12 +187,12 @@ export const resetConfiguration = async () => {
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const generateDocServerToken = async (document, editorConfig) => {
|
||||
export const generateDocServerToken = async (document, editorConfig, command) => {
|
||||
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/generate-docserver-token`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({document, editorConfig})
|
||||
body: JSON.stringify({document, editorConfig, command})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to generate Document Server token');
|
||||
@ -200,53 +200,50 @@ export const generateDocServerToken = async (document, editorConfig) => {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export const getForgottenList = async () => {
|
||||
// Call Document Server directly
|
||||
const docServiceUrl = process.env.REACT_APP_DOCSERVICE_URL || 'http://localhost:8000';
|
||||
const response = await safeFetch(`${docServiceUrl}/coauthoring/CommandService.ashx`, {
|
||||
const callDocumentServer = async (command, key = null) => {
|
||||
const {token} = await generateDocServerToken(
|
||||
{key: key || 'forgotten-list', fileType: 'docx', title: 'Document', url: ''},
|
||||
{user: {id: 'admin', name: 'admin'}, lang: 'en', mode: 'view'},
|
||||
command
|
||||
);
|
||||
|
||||
const response = await safeFetch(`${process.env.REACT_APP_DOCSERVICE_URL || window.location.origin}/coauthoring/CommandService.ashx`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
c: 'getForgottenList'
|
||||
c: command,
|
||||
...(key && {key}),
|
||||
token: token
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch forgotten files list');
|
||||
if (response.status === 404) throw new Error('File not found');
|
||||
throw new Error(`Failed to execute ${command}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
// Format the response to match our component expectations
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const getForgottenList = async () => {
|
||||
const result = await callDocumentServer('getForgottenList');
|
||||
const files = result.keys || [];
|
||||
return files.map(fileKey => {
|
||||
const fileName = fileKey.split('/').pop() || fileKey;
|
||||
return {
|
||||
key: fileKey,
|
||||
name: fileName,
|
||||
size: null, // Size not available from getForgottenList
|
||||
modified: null // Modified date not available from getForgottenList
|
||||
size: null,
|
||||
modified: null
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getForgotten = async (docId) => {
|
||||
// Call Document Server directly
|
||||
const docServiceUrl = process.env.REACT_APP_DOCSERVICE_URL || 'http://localhost:8000';
|
||||
const response = await safeFetch(`${docServiceUrl}/coauthoring/CommandService.ashx`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
c: 'getForgotten',
|
||||
key: docId
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) throw new Error('File not found');
|
||||
throw new Error('Failed to fetch forgotten file');
|
||||
}
|
||||
const result = await response.json();
|
||||
const result = await callDocumentServer('getForgotten', docId);
|
||||
return {
|
||||
docId: docId,
|
||||
url: result.url,
|
||||
|
||||
4
AdminPanel/client/src/assets/Download.svg
Normal file
4
AdminPanel/client/src/assets/Download.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 16V13H7.2V11H5C4.44772 11 4 11.4477 4 12V17C4 17.5523 4.44772 18 5 18H19C19.5523 18 20 17.5523 20 17V12C20 11.4477 19.5523 11 19 11H16.8V13H18V16H6Z" fill="#444444"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9996 8.5L15.2025 8.5C15.7911 8.5 16 8.49063 16 8.75503C16 8.94763 15.8986 9.29489 15.6987 9.4874L12.6667 12.6478C12.179 13.1174 11.821 13.1174 11.3333 12.6478L8.30133 9.4874C8.10142 9.29489 8 8.94763 8 8.75503C8 8.49056 8.20889 8.5 8.79749 8.5C8.79749 8.5 9.4909 8.5 9.99708 8.5C9.99708 8.28638 9.99708 5 9.99708 5L13.9996 5C13.9996 5 13.9996 8.29916 13.9996 8.5Z" fill="#444444"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 757 B |
@ -1,5 +1,7 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {getForgottenList, getForgotten} from '../../api';
|
||||
import DownloadIcon from '../../assets/Download.svg';
|
||||
import styles from './Forgotten.module.scss';
|
||||
|
||||
const Forgotten = () => {
|
||||
const [forgottenFiles, setForgottenFiles] = useState([]);
|
||||
@ -41,16 +43,13 @@ const Forgotten = () => {
|
||||
try {
|
||||
console.log('Downloading file:', file.name);
|
||||
|
||||
// Add file to downloading set
|
||||
setDownloadingFiles(prev => new Set(prev).add(file.key));
|
||||
|
||||
const result = await getForgotten(file.key);
|
||||
|
||||
if (result.url) {
|
||||
// Create a temporary link element and trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = result.url;
|
||||
// Use "output" as the filename with the proper extension
|
||||
const fileExtension = file.name.split('.').pop() || 'docx';
|
||||
link.download = 'output.docx';
|
||||
document.body.appendChild(link);
|
||||
@ -67,7 +66,6 @@ const Forgotten = () => {
|
||||
console.error('Error downloading file:', err);
|
||||
setError(`Failed to download ${file.name}: ${err.message}`);
|
||||
} finally {
|
||||
// Remove file from downloading set
|
||||
setDownloadingFiles(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(file.key);
|
||||
@ -76,79 +74,48 @@ const Forgotten = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="forgotten-page">
|
||||
<div className="page-header">
|
||||
<h1>Forgotten Files</h1>
|
||||
</div>
|
||||
<div className="loading">
|
||||
<div className="spinner"></div>
|
||||
<p>Loading forgotten files...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="forgotten-page">
|
||||
<div className="page-header">
|
||||
<div className={styles.forgottenPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1>Forgotten Files</h1>
|
||||
</div>
|
||||
<div className="error">
|
||||
<p>{error}</p>
|
||||
<button onClick={loadForgottenFiles} className="retry-btn">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
Failed to load forgotten files
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="forgotten-page">
|
||||
<div className="page-header">
|
||||
<div className={styles.forgottenPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1>Forgotten Files</h1>
|
||||
<button onClick={loadForgottenFiles} className="refresh-btn">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="forgotten-content">
|
||||
<div className={styles.forgottenContent}>
|
||||
{forgottenFiles.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className={styles.emptyState}>
|
||||
<p>No forgotten files found.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="files-list">
|
||||
<div className="files-header">
|
||||
<span>File Name</span>
|
||||
<span>Size</span>
|
||||
<span>Modified</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
<div className={styles.filesList}>
|
||||
{forgottenFiles.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<span className="file-name" title={file.name}>
|
||||
<div key={index} className={styles.fileRow}>
|
||||
<span className={styles.fileName} title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="file-size">
|
||||
{file.size ? formatFileSize(file.size) : '-'}
|
||||
</span>
|
||||
<span className="file-date">
|
||||
{file.modified ? formatDate(file.modified) : '-'}
|
||||
</span>
|
||||
<div className="file-actions">
|
||||
<button
|
||||
className="download-btn"
|
||||
onClick={() => handleDownload(file)}
|
||||
disabled={downloadingFiles.has(file.key)}
|
||||
title="Download file"
|
||||
>
|
||||
{downloadingFiles.has(file.key) ? 'Downloading...' : 'Download'}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={styles.downloadBtn}
|
||||
onClick={() => handleDownload(file)}
|
||||
disabled={downloadingFiles.has(file.key)}
|
||||
title="Download file"
|
||||
>
|
||||
<img
|
||||
src={DownloadIcon}
|
||||
alt="Download"
|
||||
className={styles.downloadIcon}
|
||||
style={{opacity: downloadingFiles.has(file.key) ? 0.5 : 1}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
165
AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss
Normal file
165
AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss
Normal file
@ -0,0 +1,165 @@
|
||||
.forgottenPage {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: 32px;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
p {
|
||||
color: #dc3545;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retryBtn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filesList {
|
||||
.fileRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid rgb(226, 226, 226);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.fileName {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.downloadIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.forgottenPage {
|
||||
// padding: 15px;
|
||||
|
||||
.pageHeader {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.filesList {
|
||||
.fileRow {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
.fileName {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.downloadBtn {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
.forgotten-page {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
p {
|
||||
color: #dc3545;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.files-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e0e0e0;
|
||||
overflow: hidden;
|
||||
|
||||
.files-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1.5fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 15px 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1.5fr 1fr;
|
||||
gap: 20px;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-date {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.download-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Responsive design
|
||||
@media (max-width: 768px) {
|
||||
.forgotten-page {
|
||||
padding: 15px;
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.files-list {
|
||||
.files-header,
|
||||
.file-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.file-size,
|
||||
.file-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -236,7 +236,7 @@ router.post('/generate-docserver-token', requireAuth, async (req, res) => {
|
||||
try {
|
||||
ctx.initFromRequest(req);
|
||||
|
||||
const {document, editorConfig} = req.body;
|
||||
const {document, editorConfig, command} = req.body;
|
||||
|
||||
if (!document || !editorConfig) {
|
||||
return res.status(400).json({error: 'Document and editorConfig are required'});
|
||||
@ -275,6 +275,15 @@ router.post('/generate-docserver-token', requireAuth, async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Add command parameter if provided (required for forgotten files operations)
|
||||
if (command) {
|
||||
payload.c = command;
|
||||
// For forgotten files operations, also add the key at root level
|
||||
if (command === 'getForgotten' || command === 'getForgottenList') {
|
||||
payload.key = document.key;
|
||||
}
|
||||
}
|
||||
|
||||
const tenTokenOutboxAlgorithm = ctx.getCfg('services.CoAuthoring.token.outbox.algorithm', 'HS256');
|
||||
const tenTokenOutboxExpires = ctx.getCfg('services.CoAuthoring.token.outbox.expires', '5m');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user