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",
"version": "0.105.0",
"version": "0.107.0",
"private": true,
"scripts": {
"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 { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
interface PageProps {
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) {
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 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 (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<Link href="#" className="header-user">Name</Link>
<LoginUserBage />
</header>
<div className="watch-doc-status">
<div className="st-title">Защита подтверждена</div>
<div className="st-sub">
Документ <strong>{documentTitle}</strong> зарегистрирован и защищён платформой NoCopy.
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
</div>
</div>
<div className="watch-doc-grid">
<div>
<div
className="flex flex-col justify-between"
>
<div className="watch-doc-card">
<div className="card-head">Правообладатель</div>
<div className="card-body">
<div className="owner-row">
<div className="owner-av">Н</div>
<div className="owner-av">
{response?.fileName ? response?.fileName[0] : ''}
</div>
<div>
<div className="owner-name">{ownerName}</div>
{/* <div className="owner-meta">{companyName}</div> */}
<div className="owner-meta">Зарегистрирован в NoCopy</div>
{response?.ownerName && (
<div className="owner-name">{response?.ownerName}</div>
)}
{response?.ownerCompany && (
<div className="owner-name">{response?.ownerCompany}</div>
)}
{response?.ownerName && (
<div className="owner-meta">Зарегистрирован в NoCopy</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">
<DownloadButton />
{response?.permissions?.DOWNLOAD && (
<DownloadButton />
)}
<CopyLinkButton />
<Link href="#" className="btn btn-out" target="_blank">О NoCopy</Link>
</div>
</div>
@@ -81,8 +140,8 @@ export default async function Page({ searchParams }: PageProps) {
<div className="watch-doc-footer">
<Link href="#">NoCopy</Link>
<Link href="#">Условия</Link>
<Link href="#">Конфиденциальность</Link>
<Link href="/terms-of-use">Условия</Link>
<Link href="/privacy-policy">Конфиденциальность</Link>
</div>
</div>
</div>
+150 -73
View File
@@ -5,86 +5,163 @@ import { API_BASE_URL } from '@/app/actions/definitions';
import { headers } from 'next/headers';
export async function requestFileWithTracking(fileId: string) {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
token: token,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json'
}
});
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
token: token,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const contentType = response.headers.get('content-type');
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
return data.message_body;
} else {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
if (contentType?.includes('application/json')) {
const data = await response.json();
return data.message_body;
} else {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
token: token,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json'
}
});
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
token: token,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`${response.status}`);
}
if (!response.ok) {
throw new Error(`${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
const fileUrl = data.message_body;
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
}
return await fileResponse.arrayBuffer();
}
else {
return await response.arrayBuffer();
}
} catch (error) {
return null;
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
const fileUrl = data.message_body;
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
}
return await fileResponse.arrayBuffer();
}
else {
return await response.arrayBuffer();
}
} catch (error) {
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 { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions';
import Image from 'next/image';
import { filePermisionChange } from '@/app/actions/trackingActions';
export type FileItem = {
id: string;
@@ -45,7 +46,10 @@ export type FileItem = {
protectStatus: string;
_original?: ApiFile;
supportId: number;
thumbnailFileUrl: string
thumbnailFileUrl: string;
permissions: {
DOWNLOAD: boolean;
}
};
type ApiFile = {
@@ -61,6 +65,9 @@ type ApiFile = {
supportId: number;
fileName: string;
thumbnailFileUrl: string;
permissions: {
DOWNLOAD: boolean;
}
};
type ApiResponse = {
@@ -190,7 +197,10 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
protectStatus: item.protectStatus,
supportId: item.supportId,
_original: item,
thumbnailFileUrl: item.thumbnailFileUrl
thumbnailFileUrl: item.thumbnailFileUrl,
permissions: {
DOWNLOAD: item.permissions.DOWNLOAD,
}
}));
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',
header: () => {
@@ -564,15 +601,8 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
},
cell: ({ row }) => (
<div className="actions">
<div className="actions-group">
<Link
href={`/pages/file/${row.original.id}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
{(showFileLink && row.original.fileType === 'document') && (
{(showFileLink && row.original.fileType === 'document') ? (
<div className="actions-group">
<Link
href={`/watch-doc?docid=${row.original.id}`}
className="bg-indigo-500 hover:bg-indigo-600"
@@ -581,30 +611,41 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
>
<IconLink />
</Link>
)}
</div>
<div className="actions-group">
{row.original.protectStatus === 'PROTECTED' && (
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="table-action-download"
title={t('download')}
>
<IconFileDownload />
</button>
)}
<button
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
title={t('make-check')}
>
<IconInfo />
</button>
</div>
</div>
) : (
<>
<div className="actions-group">
<Link
href={`/pages/file/${row.original.id}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
</div>
<div className="actions-group">
{row.original.protectStatus === 'PROTECTED' && (
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="table-action-download"
title={t('download')}
>
<IconFileDownload />
</button>
)}
<button
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
title={t('make-check')}
>
<IconInfo />
</button>
</div>
</>
)}
</div>
),
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(() => {
let result = tableData;
@@ -735,6 +786,10 @@ export default function TanstakFilesTable({ fileType, showFileLink }: { fileType
state: {
sorting,
pagination,
columnVisibility: {
monitoring: showFileLink ? false : true,
downloadPermision: showFileLink ? true : false,
}
},
pageCount: totalPages,
manualPagination: true,
+61
View File
@@ -1027,6 +1027,67 @@
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 {
+5 -1
View File
@@ -164,6 +164,10 @@
justify-content: center;
}
}
.error-container {
text-align: center;
}
}
&-page {
@@ -247,9 +251,9 @@
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
margin-bottom: 14px;
overflow: hidden;
box-shadow: var(--shadow);
height: 100%;
.card-head {
padding: 14px 20px;