From 06d14ce680763a23ebad1fb5de87443424d6ef46 Mon Sep 17 00:00:00 2001 From: PauI Ostrovckij Date: Wed, 8 Oct 2025 20:17:53 +0300 Subject: [PATCH] [feat] Add jwt for forgotten; Fix styles; Refactor --- AdminPanel/client/src/api/index.js | 57 +++-- AdminPanel/client/src/assets/Download.svg | 4 + .../client/src/pages/Forgotten/Forgotten.js | 83 ++----- .../src/pages/Forgotten/Forgotten.module.scss | 165 +++++++++++++ .../client/src/pages/Forgotten/Forgotten.scss | 225 ------------------ .../sources/routes/adminpanel/router.js | 11 +- 6 files changed, 231 insertions(+), 314 deletions(-) create mode 100644 AdminPanel/client/src/assets/Download.svg create mode 100644 AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss delete mode 100644 AdminPanel/client/src/pages/Forgotten/Forgotten.scss diff --git a/AdminPanel/client/src/api/index.js b/AdminPanel/client/src/api/index.js index 8cbdfcd8..cce9acf6 100644 --- a/AdminPanel/client/src/api/index.js +++ b/AdminPanel/client/src/api/index.js @@ -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, diff --git a/AdminPanel/client/src/assets/Download.svg b/AdminPanel/client/src/assets/Download.svg new file mode 100644 index 00000000..f1dbc5e8 --- /dev/null +++ b/AdminPanel/client/src/assets/Download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/AdminPanel/client/src/pages/Forgotten/Forgotten.js b/AdminPanel/client/src/pages/Forgotten/Forgotten.js index ad34514e..7efdf1f9 100644 --- a/AdminPanel/client/src/pages/Forgotten/Forgotten.js +++ b/AdminPanel/client/src/pages/Forgotten/Forgotten.js @@ -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 ( -
-
-

Forgotten Files

-
-
-
-

Loading forgotten files...

-
-
- ); - } - if (error) { return ( -
-
+
+

Forgotten Files

-
-

{error}

- -
+ Failed to load forgotten files
); } return ( -
-
+
+

Forgotten Files

-
-
+
{forgottenFiles.length === 0 ? ( -
+

No forgotten files found.

) : ( -
-
- File Name - Size - Modified - Actions -
+
{forgottenFiles.map((file, index) => ( -
- +
+ {file.name} - - {file.size ? formatFileSize(file.size) : '-'} - - - {file.modified ? formatDate(file.modified) : '-'} - -
- -
+
))}
diff --git a/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss b/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss new file mode 100644 index 00000000..0984c710 --- /dev/null +++ b/AdminPanel/client/src/pages/Forgotten/Forgotten.module.scss @@ -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; + } + } + } + } +} diff --git a/AdminPanel/client/src/pages/Forgotten/Forgotten.scss b/AdminPanel/client/src/pages/Forgotten/Forgotten.scss deleted file mode 100644 index 2f47fd05..00000000 --- a/AdminPanel/client/src/pages/Forgotten/Forgotten.scss +++ /dev/null @@ -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; - } - } - } - } -} diff --git a/AdminPanel/server/sources/routes/adminpanel/router.js b/AdminPanel/server/sources/routes/adminpanel/router.js index 5c79cf64..e579343c 100644 --- a/AdminPanel/server/sources/routes/adminpanel/router.js +++ b/AdminPanel/server/sources/routes/adminpanel/router.js @@ -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');