[feat] Add jwt for forgotten; Fix styles; Refactor

This commit is contained in:
PauI Ostrovckij
2025-10-08 20:17:53 +03:00
parent 97e821ad8d
commit 06d14ce680
6 changed files with 231 additions and 314 deletions

View File

@ -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,

View 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

View File

@ -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>

View 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;
}
}
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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');