Compare commits

...
3 Commits
Author SHA1 Message Date
smanylov d419c1c2cb add permission change button 2026-05-05 13:07:52 +07:00
smanylov 48e03b4237 increment version 2026-05-04 20:31:31 +07:00
smanylov 2fad519b7e add file info fro document view 2026-05-04 20:31:15 +07:00
7 changed files with 409 additions and 135 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.105.0", "version": "0.107.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
+84 -25
View File
@@ -4,65 +4,124 @@ import Link from 'next/link';
import PdfViewer from '@/app/components/PdfViewer'; import PdfViewer from '@/app/components/PdfViewer';
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton'; import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton'; import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
interface PageProps { interface PageProps {
searchParams: Promise<{ docid: string; title?: string }>; searchParams: Promise<{ docid: string; title?: string }>;
} }
interface DocumentInfo {
checksCount?: number;
checksum?: string;
createdAt?: string;
downloadUrl?: string;
existsOnDisk?: boolean;
fileExtension?: string;
fileFormat?: string | null;
fileName?: string;
filePath?: string;
fileSize?: number;
fileUploadDate?: string;
formattedSize?: string;
id: string;
mimeType?: string;
monitoring?: string;
originalFileName?: string;
ownerCompany?: string | null;
ownerEmail?: string;
ownerName?: string;
permissions?: {
DOWNLOAD?: boolean;
};
protectStatus?: string;
protectedFilePath?: string;
status?: string;
storedFileName?: string;
supportId?: number;
thumbnailFileUrl?: string;
updatedAt?: string;
uploadSessionId?: string;
userId?: number;
}
export default async function Page({ searchParams }: PageProps) { export default async function Page({ searchParams }: PageProps) {
const { docid, title } = await searchParams; const { docid, title } = await searchParams;
let response: DocumentInfo | null = null;
let error: string | null = null;
try {
response = await fetchDocumentInfo(docid);
} catch (err) {
error = 'Не удалось загрузить информацию о документе';
}
const documentTitle = title || 'FileName'; const documentTitle = title || 'FileName';
const ownerName = "Name Name Name";
const companyName = "companyName"; if (error || !response) {
return (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<LoginUserBage />
</header>
<div className="error-container">
<h2>Ошибка загрузки документа</h2>
<p>{error || 'Документ не найден'}</p>
</div>
</div>
</div>
);
}
return ( return (
<div className="watch-doc-wrapper"> <div className="watch-doc-wrapper">
<div className="watch-doc-page"> <div className="watch-doc-page">
<header className="watch-doc-header"> <header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link> <Link href="#" className="header-logo">NoCopy</Link>
<Link href="#" className="header-user">Name</Link> <LoginUserBage />
</header> </header>
<div className="watch-doc-status"> <div className="watch-doc-status">
<div className="st-title">Защита подтверждена</div> <div className="st-title">Защита подтверждена</div>
<div className="st-sub"> <div className="st-sub">
Документ <strong>{documentTitle}</strong> зарегистрирован и защищён платформой NoCopy. Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
</div> </div>
</div> </div>
<div className="watch-doc-grid"> <div className="watch-doc-grid">
<div> <div
className="flex flex-col justify-between"
>
<div className="watch-doc-card"> <div className="watch-doc-card">
<div className="card-head">Правообладатель</div> <div className="card-head">Правообладатель</div>
<div className="card-body"> <div className="card-body">
<div className="owner-row"> <div className="owner-row">
<div className="owner-av">Н</div> <div className="owner-av">
{response?.fileName ? response?.fileName[0] : ''}
</div>
<div> <div>
<div className="owner-name">{ownerName}</div> {response?.ownerName && (
{/* <div className="owner-meta">{companyName}</div> */} <div className="owner-name">{response?.ownerName}</div>
<div className="owner-meta">Зарегистрирован в NoCopy</div> )}
{response?.ownerCompany && (
<div className="owner-name">{response?.ownerCompany}</div>
)}
{response?.ownerName && (
<div className="owner-meta">Зарегистрирован в NoCopy</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* <div className="watch-doc-card">
<div className="card-head">Скачать документ</div>
<div className="card-body">
<DownloadButton />
<div className="download-description">
Вы вошли как <strong>{ownerName}</strong>.
<br />
Скачивание будет зарегистрировано.
</div>
</div>
</div> */}
<div className="watch-doc-actions"> <div className="watch-doc-actions">
<DownloadButton /> {response?.permissions?.DOWNLOAD && (
<DownloadButton />
)}
<CopyLinkButton /> <CopyLinkButton />
<Link href="#" className="btn btn-out" target="_blank">О NoCopy</Link>
</div> </div>
</div> </div>
@@ -81,8 +140,8 @@ export default async function Page({ searchParams }: PageProps) {
<div className="watch-doc-footer"> <div className="watch-doc-footer">
<Link href="#">NoCopy</Link> <Link href="#">NoCopy</Link>
<Link href="#">Условия</Link> <Link href="/terms-of-use">Условия</Link>
<Link href="#">Конфиденциальность</Link> <Link href="/privacy-policy">Конфиденциальность</Link>
</div> </div>
</div> </div>
</div> </div>
+149 -72
View File
@@ -5,86 +5,163 @@ import { API_BASE_URL } from '@/app/actions/definitions';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
export async function requestFileWithTracking(fileId: string) { export async function requestFileWithTracking(fileId: string) {
const token = await getSessionData('token'); const token = await getSessionData('token');
const headersList = await headers(); const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for'); const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip'); const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown'; const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown'; const userAgent = headersList.get('user-agent') || 'unknown';
try { try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, { const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
file_id: fileId, file_id: fileId,
token: token, token: token,
ip: ip, ip: ip,
user_agent: userAgent user_agent: userAgent
}), }),
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
if (response.ok) { if (response.ok) {
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) { if (contentType?.includes('application/json')) {
const data = await response.json(); const data = await response.json();
return data.message_body; return data.message_body;
} else { } else {
const blob = await response.blob(); const blob = await response.blob();
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
} else { } else {
throw new Error(`${response.status}`); throw new Error(`${response.status}`);
} }
} catch (error) { } catch (error) {
return null; return null;
} }
} }
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> { export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
const token = await getSessionData('token'); const token = await getSessionData('token');
const headersList = await headers(); const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for'); const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip'); const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown'; const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown'; const userAgent = headersList.get('user-agent') || 'unknown';
try { try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, { const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
file_id: fileId, file_id: fileId,
token: token, token: token,
ip: ip, ip: ip,
user_agent: userAgent user_agent: userAgent
}), }),
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`${response.status}`); throw new Error(`${response.status}`);
} }
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) { if (contentType?.includes('application/json')) {
const data = await response.json(); const data = await response.json();
const fileUrl = data.message_body; const fileUrl = data.message_body;
const fileResponse = await fetch(fileUrl); const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) { if (!fileResponse.ok) {
throw new Error(`Failed to fetch file: ${fileResponse.status}`); throw new Error(`Failed to fetch file: ${fileResponse.status}`);
} }
return await fileResponse.arrayBuffer(); return await fileResponse.arrayBuffer();
} }
else { else {
return await response.arrayBuffer(); return await response.arrayBuffer();
} }
} catch (error) { } catch (error) {
return null; return null;
} }
}
export async function fetchDocumentInfo(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
file_id: fileId,
token: token,
action: 'file_info'
}
}),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function filePermisionChange(fileId: string, permissions: boolean) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
file_id: fileId,
token: token,
action: 'change_permission',
permissions: {
DOWNLOAD: permissions
}
}
}),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return true;
} else {
return false;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
} }
@@ -0,0 +1,18 @@
'use client';
import Link from 'next/link';
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
export function LoginUserBage() {
const { data: userData, isLoading, isError, error } = useUserProfile();
if (!userData?.fullName) {
return null;
}
return (
<Link href="#" className="header-user">
{userData?.fullName}
</Link>
);
}
@@ -32,6 +32,7 @@ import { useDebouncedCallback } from 'use-debounce';
import Link from 'next/link'; import Link from 'next/link';
import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions'; import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions';
import Image from 'next/image'; import Image from 'next/image';
import { filePermisionChange } from '@/app/actions/trackingActions';
export type FileItem = { export type FileItem = {
id: string; id: string;
@@ -45,7 +46,10 @@ export type FileItem = {
protectStatus: string; protectStatus: string;
_original?: ApiFile; _original?: ApiFile;
supportId: number; supportId: number;
thumbnailFileUrl: string thumbnailFileUrl: string;
permissions: {
DOWNLOAD: boolean;
}
}; };
type ApiFile = { type ApiFile = {
@@ -61,6 +65,9 @@ type ApiFile = {
supportId: number; supportId: number;
fileName: string; fileName: string;
thumbnailFileUrl: string; thumbnailFileUrl: string;
permissions: {
DOWNLOAD: boolean;
}
}; };
type ApiResponse = { type ApiResponse = {
@@ -190,7 +197,10 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
protectStatus: item.protectStatus, protectStatus: item.protectStatus,
supportId: item.supportId, supportId: item.supportId,
_original: item, _original: item,
thumbnailFileUrl: item.thumbnailFileUrl thumbnailFileUrl: item.thumbnailFileUrl,
permissions: {
DOWNLOAD: item.permissions.DOWNLOAD,
}
})); }));
return { return {
@@ -553,6 +563,33 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
) )
}, },
}, },
{
accessorKey: 'downloadPermision',
header: ({ column }) => (
<div className="column">
<span>
Можно скачать по ссылке
</span>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center font-semibold table-item">
<button
onClick={() => changeDownloadPermition(row.original.id, row.original.permissions.DOWNLOAD)}
disabled={isFileLoading}
className={`toggle-switch ${row.original.permissions?.DOWNLOAD ? 'active' : 'inactive'}`}
title={'change-permission'}
>
<span className="toggle-slider"></span>
<span className="toggle-label">
{row.original.permissions?.DOWNLOAD ? 'Да' : 'Нет'}
</span>
</button>
</div>
)
},
},
{ {
id: 'actions', id: 'actions',
header: () => { header: () => {
@@ -564,15 +601,8 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
}, },
cell: ({ row }) => ( cell: ({ row }) => (
<div className="actions"> <div className="actions">
<div className="actions-group"> {(showFileLink && row.original.fileType === 'document') ? (
<Link <div className="actions-group">
href={`/pages/file/${row.original.id}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
{(showFileLink && row.original.fileType === 'document') && (
<Link <Link
href={`/watch-doc?docid=${row.original.id}`} href={`/watch-doc?docid=${row.original.id}`}
className="bg-indigo-500 hover:bg-indigo-600" className="bg-indigo-500 hover:bg-indigo-600"
@@ -581,30 +611,41 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
> >
<IconLink /> <IconLink />
</Link> </Link>
)} </div>
</div> ) : (
<div className="actions-group"> <>
{row.original.protectStatus === 'PROTECTED' && ( <div className="actions-group">
<button <Link
onClick={() => handleDownload(row.original)} href={`/pages/file/${row.original.id}`}
disabled={isFileLoading} className="bg-violet-500 hover:bg-violet-600"
className="table-action-download" title={t('view')}
title={t('download')} >
> <IconEye />
<IconFileDownload /> </Link>
</button> </div>
)} <div className="actions-group">
<button {row.original.protectStatus === 'PROTECTED' && (
onClick={() => { <button
handleView(row.original) onClick={() => handleDownload(row.original)}
}} disabled={isFileLoading}
className="table-action-view" className="table-action-download"
title={t('make-check')} title={t('download')}
> >
<IconInfo /> <IconFileDownload />
</button> </button>
</div> )}
<button
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
title={t('make-check')}
>
<IconInfo />
</button>
</div>
</>
)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@@ -659,6 +700,16 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
} }
} }
const changeDownloadPermition = async (fileId: string, permission: boolean) => {
const response = await filePermisionChange(fileId, !permission);
if (response) {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.success('Разрешение на скачивание изменено.');
} else {
toast.warning('Не удалось изменить разрешение на скачивание.');
}
}
// Фильтрация по типу файла и дате // Фильтрация по типу файла и дате
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
let result = tableData; let result = tableData;
@@ -735,6 +786,10 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
state: { state: {
sorting, sorting,
pagination, pagination,
columnVisibility: {
monitoring: showFileLink ? false : true,
downloadPermision: showFileLink ? true : false,
}
}, },
pageCount: totalPages, pageCount: totalPages,
manualPagination: true, manualPagination: true,
+61
View File
@@ -1027,6 +1027,67 @@
background-color: #155dfc; background-color: #155dfc;
} }
} }
.toggle-switch {
position: relative;
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 60px;
height: 28px;
padding: 0 6px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 12px;
font-weight: 500;
background-color: #615fff;
padding: 0;
}
.toggle-switch.active {
justify-content: flex-end;
}
.toggle-switch.inactive {
justify-content: flex-start;
}
.toggle-switch .toggle-slider {
position: absolute;
width: 30px;
height: 28px;
background-color: white;
border: 1px solid #615fff;
border-radius: 8px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.toggle-switch.active .toggle-slider {
transform: translateX(0);
}
.toggle-switch.inactive .toggle-slider {
transform: translateX(0);
}
.toggle-switch .toggle-label {
z-index: 1;
color: v.$text-p;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
width: 30px;
height: 26px;
display: flex;
justify-content: center;
align-items: center;
}
.toggle-switch:disabled {
opacity: 0.5;
cursor: not-allowed;
}
} }
.table-item-checkbox { .table-item-checkbox {
+5 -1
View File
@@ -164,6 +164,10 @@
justify-content: center; justify-content: center;
} }
} }
.error-container {
text-align: center;
}
} }
&-page { &-page {
@@ -247,9 +251,9 @@
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
margin-bottom: 14px;
overflow: hidden; overflow: hidden;
box-shadow: var(--shadow); box-shadow: var(--shadow);
height: 100%;
.card-head { .card-head {
padding: 14px 20px; padding: 14px 20px;