everything works!
This commit is contained in:
parent
01017bb36e
commit
a32992b17b
@ -5,6 +5,7 @@
|
|||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||||
|
|
||||||
DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public"
|
DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public"
|
||||||
|
JWT_SECRET=some-secret
|
||||||
AWS_REGION=EU2
|
AWS_REGION=EU2
|
||||||
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
|
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
|
||||||
AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
|
AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `content` on the `Document` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `sharedWithId` on the `Document` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `uploadedById` to the `Document` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `documentId` to the `Notification` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Document" DROP CONSTRAINT "Document_sharedWithId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Document" DROP COLUMN "content",
|
||||||
|
DROP COLUMN "sharedWithId",
|
||||||
|
ADD COLUMN "uploadedById" INTEGER NOT NULL,
|
||||||
|
ALTER COLUMN "status" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Notification" ADD COLUMN "documentId" INTEGER NOT NULL;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_SharedDocuments" (
|
||||||
|
"A" INTEGER NOT NULL,
|
||||||
|
"B" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_SharedDocuments_AB_unique" ON "_SharedDocuments"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_SharedDocuments_B_index" ON "_SharedDocuments"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_A_fkey" FOREIGN KEY ("A") REFERENCES "Document"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_SharedDocuments" ADD CONSTRAINT "_SharedDocuments_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -8,32 +8,36 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Document {
|
model Document {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
content String?
|
|
||||||
s3Key String
|
s3Key String
|
||||||
status String @default("pending")
|
status String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
sharedWithId Int
|
uploadedById Int
|
||||||
sharedWith User @relation("SharedDocuments", fields: [sharedWithId], references: [id])
|
uploadedBy User @relation("UploadedDocuments", fields: [uploadedById], references: [id])
|
||||||
|
Notification Notification[]
|
||||||
|
sharedWith User[] @relation("SharedDocuments")
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
email String @unique
|
email String @unique
|
||||||
name String?
|
name String?
|
||||||
password String
|
password String
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
sharedDocuments Document[] @relation("SharedDocuments")
|
uploadedDocuments Document[] @relation("UploadedDocuments")
|
||||||
notifications Notification[]
|
Notification Notification[]
|
||||||
|
sharedDocuments Document[] @relation("SharedDocuments")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Notification {
|
model Notification {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
message String
|
message String
|
||||||
read Boolean @default(false)
|
read Boolean @default(false)
|
||||||
userId Int
|
userId Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
user User @relation(fields: [userId], references: [id])
|
documentId Int
|
||||||
|
document Document @relation(fields: [documentId], references: [id])
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,118 @@
|
|||||||
|
// import {
|
||||||
|
// Controller,
|
||||||
|
// Get,
|
||||||
|
// Post,
|
||||||
|
// Body,
|
||||||
|
// Param,
|
||||||
|
// Put,
|
||||||
|
// UseInterceptors,
|
||||||
|
// UploadedFile,
|
||||||
|
// ParseIntPipe,
|
||||||
|
// UseGuards,
|
||||||
|
// } from '@nestjs/common';
|
||||||
|
// import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
// import { AdminService } from './admin.service';
|
||||||
|
// //import { CreateDocumentDto } from '../dto/create-document.dto';
|
||||||
|
// import { UpdateDocumentDto } from '../dto/update-document.dto';
|
||||||
|
// import { AdminGuard } from '../auth/admin.guard';
|
||||||
|
// import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
// import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
|
// import { S3Service } from 'src/s3/s3.service';
|
||||||
|
// import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
||||||
|
// @Controller('admin')
|
||||||
|
// @UseGuards(JwtAuthGuard, AdminGuard)
|
||||||
|
// export class AdminController {
|
||||||
|
// constructor(
|
||||||
|
// private readonly adminService: AdminService,
|
||||||
|
// private readonly s3Service: S3Service,
|
||||||
|
// private readonly prisma: PrismaService,
|
||||||
|
// ) {}
|
||||||
|
|
||||||
|
// @Get('documents')
|
||||||
|
// getAllDocuments() {
|
||||||
|
// return this.adminService.getAllDocuments();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Put('documents/:id')
|
||||||
|
// @UseInterceptors(FileInterceptor('file'))
|
||||||
|
// updateDocument(
|
||||||
|
// @Param('id', ParseIntPipe) id: number,
|
||||||
|
// @Body() updateDocumentDto: UpdateDocumentDto,
|
||||||
|
// @UploadedFile() file?: Express.Multer.File,
|
||||||
|
// ) {
|
||||||
|
// return this.adminService.updateDocument(id, updateDocumentDto, file);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Get('users')
|
||||||
|
// getAllUsers() {
|
||||||
|
// return this.adminService.getAllUsers();
|
||||||
|
// }
|
||||||
|
// @Post('test-document')
|
||||||
|
// async testDocumentCreation() {
|
||||||
|
// try {
|
||||||
|
// const document = await this.prisma.document.create({
|
||||||
|
// data: {
|
||||||
|
// title: 'Test Document',
|
||||||
|
// s3Key: 'test-key',
|
||||||
|
// status: 'completed',
|
||||||
|
// sharedWithId: 2, // ID of 'pero' user
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// return document;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Test document creation error:', error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Post('users')
|
||||||
|
// async createUser(@Body() createUserDto: CreateUserDto) {
|
||||||
|
// return this.adminService.createUser(createUserDto);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Post('documents/:id/share')
|
||||||
|
// async shareDocument(
|
||||||
|
// @Param('id') id: string,
|
||||||
|
// @Body() { userId }: { userId: number },
|
||||||
|
// ) {
|
||||||
|
// return this.adminService.shareDocument(+id, userId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Put('documents/:id/status')
|
||||||
|
// async updateDocumentStatus(
|
||||||
|
// @Param('id') id: string,
|
||||||
|
// @Body() { status }: { status: string },
|
||||||
|
// ) {
|
||||||
|
// return this.adminService.updateDocumentStatus(+id, status);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Post('documents')
|
||||||
|
// @UseInterceptors(FileInterceptor('file'))
|
||||||
|
// async uploadDocument(
|
||||||
|
// @UploadedFile() file: Express.Multer.File,
|
||||||
|
// @Body('title') title: string,
|
||||||
|
// @Body('sharedWithId') sharedWithId: number,
|
||||||
|
// @Body('uploadedById') uploadedById: number
|
||||||
|
// ) {
|
||||||
|
// const document = await this.adminService.uploadDocument(
|
||||||
|
// file,
|
||||||
|
// title,
|
||||||
|
// sharedWithId,
|
||||||
|
// uploadedById // Add this missing parameter
|
||||||
|
// );
|
||||||
|
// return document;
|
||||||
|
// }
|
||||||
|
// @Get('test-s3-connection')
|
||||||
|
// async testS3Connection() {
|
||||||
|
// const isConnected = await this.s3Service.testConnection();
|
||||||
|
// if (isConnected) {
|
||||||
|
// return { message: 'Successfully connected to S3' };
|
||||||
|
// } else {
|
||||||
|
// return { message: 'Failed to connect to S3' };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
@ -9,10 +124,10 @@ import {
|
|||||||
UploadedFile,
|
UploadedFile,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
//import { CreateDocumentDto } from '../dto/create-document.dto';
|
|
||||||
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
||||||
import { AdminGuard } from '../auth/admin.guard';
|
import { AdminGuard } from '../auth/admin.guard';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
@ -44,27 +159,60 @@ export class AdminController {
|
|||||||
return this.adminService.updateDocument(id, updateDocumentDto, file);
|
return this.adminService.updateDocument(id, updateDocumentDto, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('documents')
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
async uploadDocument(
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Body('title') title: string,
|
||||||
|
@Body('sharedWithId') sharedWithId: string, // Accept as string first
|
||||||
|
@Body('uploadedById') uploadedById: string // Accept as string first
|
||||||
|
) {
|
||||||
|
if (!sharedWithId || !uploadedById) {
|
||||||
|
throw new BadRequestException('sharedWithId and uploadedById are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the string values to numbers
|
||||||
|
const parsedSharedWithId = parseInt(sharedWithId, 10);
|
||||||
|
const parsedUploadedById = parseInt(uploadedById, 10);
|
||||||
|
|
||||||
|
// Validate that the parsing was successful
|
||||||
|
if (isNaN(parsedSharedWithId) || isNaN(parsedUploadedById)) {
|
||||||
|
throw new BadRequestException('sharedWithId and uploadedById must be valid numbers');
|
||||||
|
}
|
||||||
|
|
||||||
|
const document = await this.adminService.uploadDocument(
|
||||||
|
file,
|
||||||
|
title,
|
||||||
|
parsedSharedWithId,
|
||||||
|
parsedUploadedById
|
||||||
|
);
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
@Get('users')
|
@Get('users')
|
||||||
getAllUsers() {
|
getAllUsers() {
|
||||||
return this.adminService.getAllUsers();
|
return this.adminService.getAllUsers();
|
||||||
}
|
}
|
||||||
@Post('test-document')
|
|
||||||
async testDocumentCreation() {
|
// @Post('test-document')
|
||||||
try {
|
// async testDocumentCreation() {
|
||||||
const document = await this.prisma.document.create({
|
// try {
|
||||||
data: {
|
// const document = await this.prisma.document.create({
|
||||||
title: 'Test Document',
|
// data: {
|
||||||
s3Key: 'test-key',
|
// title: 'Test Document',
|
||||||
status: 'completed',
|
// s3Key: 'test-key',
|
||||||
sharedWithId: 2, // ID of 'pero' user
|
// status: 'completed',
|
||||||
},
|
// sharedWith: {
|
||||||
});
|
// connect: { id: 2 } // ID of 'pero' user
|
||||||
return document;
|
// }
|
||||||
} catch (error) {
|
// },
|
||||||
console.error('Test document creation error:', error);
|
// });
|
||||||
throw error;
|
// return document;
|
||||||
}
|
// } catch (error) {
|
||||||
}
|
// console.error('Test document creation error:', error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@Post('users')
|
@Post('users')
|
||||||
async createUser(@Body() createUserDto: CreateUserDto) {
|
async createUser(@Body() createUserDto: CreateUserDto) {
|
||||||
@ -87,41 +235,14 @@ export class AdminController {
|
|||||||
return this.adminService.updateDocumentStatus(+id, status);
|
return this.adminService.updateDocumentStatus(+id, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('documents')
|
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
|
||||||
async uploadDocument(
|
|
||||||
@UploadedFile() file: Express.Multer.File,
|
|
||||||
@Body('title') title: string,
|
|
||||||
@Body('sharedWith') sharedWithId: string,
|
|
||||||
) {
|
|
||||||
console.log('Received upload request:', {
|
|
||||||
fileExists: !!file,
|
|
||||||
fileSize: file?.size,
|
|
||||||
title,
|
|
||||||
sharedWithId,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
// @Get('test-s3-connection')
|
||||||
const document = await this.adminService.uploadDocument(
|
// async testS3Connection() {
|
||||||
file,
|
// const isConnected = await this.s3Service.testConnection();
|
||||||
title,
|
// if (isConnected) {
|
||||||
Number(sharedWithId),
|
// return { message: 'Successfully connected to S3' };
|
||||||
);
|
// } else {
|
||||||
|
// return { message: 'Failed to connect to S3' };
|
||||||
console.log('Document created:', document);
|
// }
|
||||||
return document;
|
// }
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Upload error:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Get('test-s3-connection')
|
|
||||||
async testS3Connection() {
|
|
||||||
const isConnected = await this.s3Service.testConnection();
|
|
||||||
if (isConnected) {
|
|
||||||
return { message: 'Successfully connected to S3' };
|
|
||||||
} else {
|
|
||||||
return { message: 'Failed to connect to S3' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { S3Service } from '../s3/s3.service';
|
import { S3Service } from '../s3/s3.service';
|
||||||
//import { CreateDocumentDto } from '../dto/create-document.dto';
|
|
||||||
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
@ -9,93 +8,44 @@ import * as bcrypt from 'bcrypt';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private s3Service: S3Service,
|
private readonly s3Service: S3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async uploadDocument(
|
|
||||||
file: Express.Multer.File,
|
|
||||||
title: string,
|
|
||||||
sharedWithId: number,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// First verify the user exists
|
|
||||||
console.log('Verifying user:', sharedWithId);
|
|
||||||
const user = await this.prisma.user.findUnique({
|
|
||||||
where: { id: sharedWithId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new NotFoundException(`User with ID ${sharedWithId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found user:', user);
|
|
||||||
|
|
||||||
// Upload file to S3
|
|
||||||
console.log('Uploading file to S3...');
|
|
||||||
const s3Key = await this.s3Service.uploadFile(file, 'documents');
|
|
||||||
console.log('File uploaded to S3:', s3Key);
|
|
||||||
|
|
||||||
console.log('Creating document record with data:', {
|
|
||||||
title,
|
|
||||||
s3Key,
|
|
||||||
sharedWithId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const document = await this.prisma.document.create({
|
|
||||||
data: {
|
|
||||||
title,
|
|
||||||
s3Key,
|
|
||||||
status: 'completed',
|
|
||||||
sharedWithId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
sharedWith: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Created document:', document);
|
|
||||||
return document;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in uploadDocument:', error);
|
|
||||||
if (error.code === 'P2002') {
|
|
||||||
console.error('Unique constraint violation');
|
|
||||||
}
|
|
||||||
if (error.code === 'P2003') {
|
|
||||||
console.error('Foreign key constraint violation');
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getAllDocuments() {
|
async getAllDocuments() {
|
||||||
try {
|
return this.prisma.document.findMany({
|
||||||
const documents = await this.prisma.document.findMany({
|
include: {
|
||||||
include: {
|
uploadedBy: {
|
||||||
sharedWith: {
|
select: {
|
||||||
select: {
|
id: true,
|
||||||
id: true,
|
name: true,
|
||||||
name: true,
|
email: true,
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
sharedWith: {
|
||||||
createdAt: 'desc',
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Retrieved documents with shared users:', documents);
|
async getAllUsers() {
|
||||||
return documents;
|
return this.prisma.user.findMany();
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Error fetching documents:', error);
|
|
||||||
throw error;
|
async createUser(createUserDto: CreateUserDto) {
|
||||||
}
|
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
||||||
|
|
||||||
|
return this.prisma.user.create({
|
||||||
|
data: {
|
||||||
|
...createUserDto,
|
||||||
|
password: hashedPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDocument(
|
async updateDocument(
|
||||||
@ -103,76 +53,26 @@ export class AdminService {
|
|||||||
updateDocumentDto: UpdateDocumentDto,
|
updateDocumentDto: UpdateDocumentDto,
|
||||||
file?: Express.Multer.File,
|
file?: Express.Multer.File,
|
||||||
) {
|
) {
|
||||||
const { ...documentData } = updateDocumentDto;
|
let s3Key = undefined;
|
||||||
let s3Key: string | undefined;
|
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const oldDocument = await this.prisma.document.findUnique({
|
|
||||||
where: { id },
|
|
||||||
select: { s3Key: true },
|
|
||||||
});
|
|
||||||
if (oldDocument?.s3Key) {
|
|
||||||
await this.s3Service.deleteFile(oldDocument.s3Key);
|
|
||||||
}
|
|
||||||
s3Key = await this.s3Service.uploadFile(file, 'documents');
|
s3Key = await this.s3Service.uploadFile(file, 'documents');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.document.update({
|
return this.prisma.document.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
...documentData,
|
...updateDocumentDto,
|
||||||
...(s3Key && { s3Key }),
|
...(s3Key && { s3Key }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// async createUser(createUserDto: CreateUserDto) {
|
|
||||||
// const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
|
||||||
// return this.prisma.user.create({
|
|
||||||
// data: {
|
|
||||||
// ...createUserDto,
|
|
||||||
// password: hashedPassword,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
async createUser(userData: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
}) {
|
|
||||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
|
||||||
|
|
||||||
return this.prisma.user.create({
|
|
||||||
data: {
|
|
||||||
name: userData.name,
|
|
||||||
email: userData.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
isAdmin: userData.isAdmin,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
isAdmin: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async shareDocument(documentId: number, userId: number) {
|
async shareDocument(documentId: number, userId: number) {
|
||||||
return this.prisma.document.update({
|
return this.prisma.document.update({
|
||||||
where: { id: documentId },
|
where: { id: documentId },
|
||||||
data: {
|
data: {
|
||||||
sharedWithId: userId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
sharedWith: {
|
sharedWith: {
|
||||||
select: {
|
connect: { id: userId },
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -185,43 +85,42 @@ export class AdminService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDocumentUrl(documentId: number) {
|
async uploadDocument(
|
||||||
const document = await this.prisma.document.findUnique({
|
file: Express.Multer.File,
|
||||||
where: { id: documentId },
|
title: string,
|
||||||
});
|
sharedWithId: number,
|
||||||
if (!document) {
|
uploadedById: number
|
||||||
throw new NotFoundException('Document not found');
|
) {
|
||||||
}
|
const s3Key = await this.s3Service.uploadFile(file, 'documents');
|
||||||
return this.s3Service.getFileUrl(document.s3Key);
|
|
||||||
}
|
return this.prisma.document.create({
|
||||||
async getUserWithDocuments(userId: number) {
|
data: {
|
||||||
return this.prisma.user.findUnique({
|
title,
|
||||||
where: { id: userId },
|
s3Key,
|
||||||
select: {
|
status: 'pending',
|
||||||
id: true,
|
sharedWith: {
|
||||||
name: true,
|
connect: { id: sharedWithId }
|
||||||
email: true,
|
},
|
||||||
sharedDocuments: true,
|
uploadedBy: {
|
||||||
|
connect: { id: uploadedById }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
uploadedBy: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sharedWith: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
async getAllUsers() {
|
|
||||||
try {
|
|
||||||
const users = await this.prisma.user.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
isAdmin: true,
|
|
||||||
sharedDocuments: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log('All users:', users);
|
|
||||||
return users;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching users:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,6 +14,7 @@ import { PrismaModule } from './prisma/prisma.module';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AuthController } from './auth/auth.controller';
|
import { AuthController } from './auth/auth.controller';
|
||||||
import { DocumentsController } from './documents/documents.controller';
|
import { DocumentsController } from './documents/documents.controller';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -26,7 +27,13 @@ import { DocumentsController } from './documents/documents.controller';
|
|||||||
// database: 'imk',
|
// database: 'imk',
|
||||||
// synchronize: true,
|
// synchronize: true,
|
||||||
// }),
|
// }),
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET,
|
||||||
|
signOptions: { expiresIn: '1h' },
|
||||||
|
}),
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
|
|||||||
@ -3,12 +3,14 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateUser(username: string, password: string): Promise<any> {
|
async validateUser(username: string, password: string): Promise<any> {
|
||||||
@ -24,9 +26,16 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(user: any) {
|
async login(user: any) {
|
||||||
const payload = { username: user.email, sub: user.id };
|
// const payload = { username: user.email, sub: user.id };
|
||||||
|
// return {
|
||||||
|
// access_token: this.jwtService.sign(payload),
|
||||||
|
// };
|
||||||
|
const payload = { username: user.username, sub: user.id };
|
||||||
|
console.log(payload);
|
||||||
return {
|
return {
|
||||||
access_token: this.jwtService.sign(payload),
|
access_token: this.jwtService.sign(payload, {
|
||||||
|
secret: this.configService.get<string>('JWT_SECRET'),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,15 +68,32 @@ export class AuthService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// async getUserInfo(userId: number) {
|
||||||
|
// return this.prisma.user.findUnique({
|
||||||
|
// where: { id: userId },
|
||||||
|
// select: {
|
||||||
|
// id: true,
|
||||||
|
// name: true,
|
||||||
|
// email: true,
|
||||||
|
// isAdmin: true,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
async getUserInfo(userId: number) {
|
async getUserInfo(userId: number) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error('User ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
return this.prisma.user.findUnique({
|
return this.prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: {
|
||||||
|
id: userId, // Make sure userId is properly passed and converted to number if needed
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
email: true,
|
email: true,
|
||||||
isAdmin: true,
|
isAdmin: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { jwtConstants } from './constants';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
constructor() {
|
constructor(configService: ConfigService) {
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: jwtConstants.secret,
|
secretOrKey: configService.get('JWT_SECRET'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,11 @@ export class ClientService {
|
|||||||
async getDocuments(userId: string) {
|
async getDocuments(userId: string) {
|
||||||
return this.prisma.document.findMany({
|
return this.prisma.document.findMany({
|
||||||
where: {
|
where: {
|
||||||
sharedWithId: Number(userId),
|
sharedWith: {
|
||||||
|
some: {
|
||||||
|
id: Number(userId),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
sharedWith: {
|
sharedWith: {
|
||||||
|
|||||||
@ -1,33 +1,81 @@
|
|||||||
|
import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common';
|
||||||
import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common';
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { DocumentsService } from './documents.service';
|
import { DocumentsService } from './documents.service';
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
|
import { S3Service } from '../s3/s3.service';
|
||||||
|
|
||||||
|
interface S3File {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
contentLength: number;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('documents')
|
@Controller('documents')
|
||||||
export class DocumentsController {
|
export class DocumentsController {
|
||||||
constructor(private readonly documentsService: DocumentsService) {}
|
private readonly logger = new Logger(DocumentsController.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly documentsService: DocumentsService,
|
||||||
|
private readonly s3Service: S3Service
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get('shared/:userId')
|
@Get('shared/:userId')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async getSharedDocuments(@Param('userId') userId: string) {
|
async getSharedDocuments(@Param('userId') userId: string) {
|
||||||
return this.documentsService.getClientDocuments(parseInt(userId));
|
return this.documentsService.getClientDocuments(Number(userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('shared/download/:key')
|
@Get('download/:key')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
async downloadDocument(
|
async downloadDocument(
|
||||||
@Param('key') key: string,
|
@Param('key') key: string,
|
||||||
@Req() req,
|
@Request() req,
|
||||||
@Res() res: Response
|
@Res() res: Response
|
||||||
) {
|
) {
|
||||||
const file = await this.documentsService.downloadDocument(key, req.user.id);
|
try {
|
||||||
|
this.logger.debug(`Download request for key: ${key}`);
|
||||||
res.set({
|
|
||||||
'Content-Type': file.contentType,
|
const decodedKey = decodeURIComponent(key);
|
||||||
'Content-Length': file.contentLength,
|
this.logger.debug(`Decoded key: ${decodedKey}`);
|
||||||
'Content-Disposition': `attachment; filename="${file.fileName}"`,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.send(file.buffer);
|
// Get document from database first to verify access
|
||||||
|
const document = await this.documentsService.findDocumentByS3Key(decodedKey);
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
return res.status(404).json({ message: 'Document not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user has access to this document
|
||||||
|
const hasAccess = await this.documentsService.verifyDocumentAccess(
|
||||||
|
document.id,
|
||||||
|
req.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
return res.status(403).json({ message: 'Access denied' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file from S3
|
||||||
|
const file = await this.s3Service.getFile(decodedKey);
|
||||||
|
|
||||||
|
if (!file || !file.buffer) {
|
||||||
|
return res.status(404).json({ message: 'File not found in storage' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set({
|
||||||
|
'Content-Type': file.contentType || 'application/octet-stream',
|
||||||
|
'Content-Length': file.contentLength,
|
||||||
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(file.fileName)}"`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send(file.buffer);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Download error:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
message: 'Failed to download file',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,236 +1,120 @@
|
|||||||
// // import { Injectable } from '@nestjs/common';
|
import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
// // import { PrismaService } from '../prisma/prisma.service';
|
|
||||||
// // //import { Document } from '@prisma/client';
|
|
||||||
|
|
||||||
// // @Injectable()
|
|
||||||
// // export class DocumentsService {
|
|
||||||
// // downloadDocument(key: string, id: any) {
|
|
||||||
// // throw new Error('Method not implemented.');
|
|
||||||
// // }
|
|
||||||
// // constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
// // async getClientDocuments(clientId: number) {
|
|
||||||
// // // return this.prisma.document.findMany({
|
|
||||||
// // // where: {
|
|
||||||
// // // sharedWithId: clientId,
|
|
||||||
// // // },
|
|
||||||
// // // include: {
|
|
||||||
// // // sharedWith: {
|
|
||||||
// // // select: {
|
|
||||||
// // // id: true,
|
|
||||||
// // // name: true,
|
|
||||||
// // // email: true,
|
|
||||||
// // // },
|
|
||||||
// // // },
|
|
||||||
// // // },
|
|
||||||
// // // });
|
|
||||||
// // return this.prisma.document.findMany({
|
|
||||||
// // where: {
|
|
||||||
// // sharedWithId: clientId,
|
|
||||||
// // },
|
|
||||||
// // orderBy: {
|
|
||||||
// // createdAt: 'desc',
|
|
||||||
// // },
|
|
||||||
// // });
|
|
||||||
// // }
|
|
||||||
// // }
|
|
||||||
// import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
|
||||||
// import { PrismaService } from '../prisma/prisma.service';
|
|
||||||
// import { S3Service } from '../s3/s3.service';
|
|
||||||
// import { Document, User } from '@prisma/client';
|
|
||||||
|
|
||||||
// @Injectable()
|
|
||||||
// export class DocumentsService {
|
|
||||||
// constructor(
|
|
||||||
// private prisma: PrismaService,
|
|
||||||
// private s3Service: S3Service,
|
|
||||||
// ) {}
|
|
||||||
|
|
||||||
// async findAllByClient(userId: number): Promise<Document[]> {
|
|
||||||
// const documents = await this.prisma.document.findMany({
|
|
||||||
// where: {
|
|
||||||
// sharedWithId: userId,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// sharedWith: {
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// name: true,
|
|
||||||
// email: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// orderBy: {
|
|
||||||
// createdAt: 'desc',
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return documents;
|
|
||||||
// }
|
|
||||||
// async getClientDocuments(clientId: number) {
|
|
||||||
// // return this.prisma.document.findMany({
|
|
||||||
// // where: {
|
|
||||||
// // sharedWithId: clientId,
|
|
||||||
// // },
|
|
||||||
// // include: {
|
|
||||||
// // sharedWith: {
|
|
||||||
// // select: {
|
|
||||||
// // id: true,
|
|
||||||
// // name: true,
|
|
||||||
// // email: true,
|
|
||||||
// // },
|
|
||||||
// // },
|
|
||||||
// // },
|
|
||||||
// // });
|
|
||||||
// return this.prisma.document.findMany({
|
|
||||||
// where: {
|
|
||||||
// sharedWithId: clientId,
|
|
||||||
// },
|
|
||||||
// orderBy: {
|
|
||||||
// createdAt: 'desc',
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async downloadDocument(s3Key: string, userId: number): Promise<any> {
|
|
||||||
// // Verify document exists and user has access
|
|
||||||
// const document = await this.prisma.document.findFirst({
|
|
||||||
// where: {
|
|
||||||
// s3Key: s3Key,
|
|
||||||
// sharedWithId: userId,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!document) {
|
|
||||||
// throw new NotFoundException('Document not found or access denied');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Get document stream from S3
|
|
||||||
// const fileStream = await this.s3Service.getObject(s3Key).createReadStream();
|
|
||||||
// return fileStream;
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error downloading document:', error);
|
|
||||||
// throw new NotFoundException('Document file not found in storage');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async getAllDocuments(): Promise<Document[]> {
|
|
||||||
// return this.prisma.document.findMany({
|
|
||||||
// include: {
|
|
||||||
// sharedWith: {
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// name: true,
|
|
||||||
// email: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// orderBy: {
|
|
||||||
// createdAt: 'desc',
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async shareDocument(documentId: number, userId: number): Promise<Document> {
|
|
||||||
// const document = await this.prisma.document.update({
|
|
||||||
// where: { id: documentId },
|
|
||||||
// data: {
|
|
||||||
// sharedWithId: userId,
|
|
||||||
// },
|
|
||||||
// include: {
|
|
||||||
// sharedWith: {
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// name: true,
|
|
||||||
// email: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return document;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async updateStatus(documentId: number, status: string): Promise<Document> {
|
|
||||||
// return this.prisma.document.update({
|
|
||||||
// where: { id: documentId },
|
|
||||||
// data: { status },
|
|
||||||
// include: {
|
|
||||||
// sharedWith: {
|
|
||||||
// select: {
|
|
||||||
// id: true,
|
|
||||||
// name: true,
|
|
||||||
// email: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { S3Service } from '../s3/s3.service';
|
import { S3Service } from '../s3/s3.service';
|
||||||
import { Document, User } from '@prisma/client';
|
import { Document, User, Prisma } from '@prisma/client';
|
||||||
import { Readable } from 'stream';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocumentsService {
|
export class DocumentsService {
|
||||||
|
private readonly logger = new Logger(DocumentsService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly s3Service: S3Service,
|
private readonly s3Service: S3Service
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getClientDocuments(clientId: number) {
|
async findDocumentByS3Key(s3Key: string) {
|
||||||
return this.prisma.document.findMany({
|
return this.prisma.document.findFirst({
|
||||||
where: {
|
where: { s3Key },
|
||||||
sharedWithId: clientId,
|
include: {
|
||||||
},
|
uploadedBy: true,
|
||||||
orderBy: {
|
sharedWith: true,
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadDocument(s3Key: string, userId: number) {
|
async verifyDocumentAccess(documentId: number, userId: number): Promise<boolean> {
|
||||||
// Verify document access
|
const document = await this.prisma.document.findUnique({
|
||||||
const document = await this.prisma.document.findFirst({
|
where: { id: documentId },
|
||||||
where: {
|
include: {
|
||||||
s3Key: s3Key,
|
uploadedBy: true,
|
||||||
sharedWithId: userId,
|
sharedWith: {
|
||||||
|
where: {
|
||||||
|
id: userId
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
throw new NotFoundException('Document not found or access denied');
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// User has access if they uploaded the document or it's shared with them
|
||||||
const s3Response = await this.s3Service.getObject(s3Key);
|
return document.uploadedBy.id === userId || document.sharedWith.length > 0;
|
||||||
|
|
||||||
if (!s3Response.Body) {
|
|
||||||
throw new Error('No file content received from S3');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the response body to a buffer
|
|
||||||
const streamBody = s3Response.Body as Readable;
|
|
||||||
const chunks: Buffer[] = [];
|
|
||||||
|
|
||||||
for await (const chunk of streamBody) {
|
|
||||||
chunks.push(Buffer.from(chunk));
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileBuffer = Buffer.concat(chunks);
|
|
||||||
|
|
||||||
return {
|
|
||||||
buffer: fileBuffer,
|
|
||||||
contentType: s3Response.ContentType || 'application/octet-stream',
|
|
||||||
contentLength: s3Response.ContentLength,
|
|
||||||
fileName: s3Key.split('/').pop() || 'download'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error downloading from S3:', error);
|
|
||||||
throw new NotFoundException('Failed to download file');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async getClientDocuments(clientId: number) {
|
||||||
|
return this.prisma.document.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ uploadedById: clientId },
|
||||||
|
{
|
||||||
|
sharedWith: {
|
||||||
|
some: {
|
||||||
|
id: clientId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
uploadedBy: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sharedWith: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadDocument(
|
||||||
|
file: Express.Multer.File,
|
||||||
|
title: string,
|
||||||
|
sharedWithId: number,
|
||||||
|
uploadedById: number
|
||||||
|
) {
|
||||||
|
const s3Key = await this.s3Service.uploadFile(file, 'documents');
|
||||||
|
|
||||||
|
return this.prisma.document.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
s3Key,
|
||||||
|
status: 'pending',
|
||||||
|
uploadedBy: {
|
||||||
|
connect: { id: uploadedById }
|
||||||
|
},
|
||||||
|
sharedWith: {
|
||||||
|
connect: { id: sharedWithId }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
uploadedBy: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sharedWith: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
export interface FileResponse {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
contentLength: number;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
6
backend/imk-backend/src/interfaces/s3-file.interface.ts
Normal file
6
backend/imk-backend/src/interfaces/s3-file.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface S3File {
|
||||||
|
buffer: Buffer;
|
||||||
|
contentType: string;
|
||||||
|
contentLength: number;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
@ -1,101 +1,70 @@
|
|||||||
|
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
S3Client,
|
|
||||||
DeleteObjectCommand,
|
|
||||||
GetObjectCommand,
|
|
||||||
ListObjectsCommand,
|
|
||||||
PutObjectCommand,
|
|
||||||
} from '@aws-sdk/client-s3';
|
|
||||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
||||||
import { Upload } from '@aws-sdk/lib-storage';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
import { S3File } from '../interfaces/s3-file.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class S3Service {
|
export class S3Service {
|
||||||
private s3Client: S3Client;
|
private readonly s3Client: S3Client;
|
||||||
private readonly logger = new Logger(S3Service.name);
|
private readonly logger = new Logger(S3Service.name);
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
this.s3Client = new S3Client({
|
this.s3Client = new S3Client({
|
||||||
region: this.configService.get('AWS_REGION'),
|
region: this.configService.get<string>('AWS_REGION'),
|
||||||
endpoint: this.configService.get('AWS_ENDPOINT_URL'),
|
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
|
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
|
||||||
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
|
secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
|
||||||
},
|
},
|
||||||
forcePathStyle: true, // Needed for non-AWS S3 compatible services
|
endpoint: this.configService.get<string>('AWS_ENDPOINT_URL'),
|
||||||
|
forcePathStyle: true, // Required for Contabo Object Storage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getObject(key: string) {
|
|
||||||
try {
|
|
||||||
const command = new GetObjectCommand({
|
|
||||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
|
||||||
Key: key,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.s3Client.send(command);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error getting object from S3: ${error.message}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async uploadFile(file: Express.Multer.File, folder: string): Promise<string> {
|
async uploadFile(file: Express.Multer.File, folder: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const key = `${folder}/${Date.now()}-${file.originalname}`;
|
const key = `${folder}/${Date.now()}-${file.originalname}`;
|
||||||
|
|
||||||
const command = new PutObjectCommand({
|
const command = new PutObjectCommand({
|
||||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
|
||||||
Key: key,
|
Key: key,
|
||||||
Body: file.buffer,
|
Body: file.buffer,
|
||||||
ContentType: file.mimetype,
|
ContentType: file.mimetype,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.s3Client.send(command);
|
await this.s3Client.send(command);
|
||||||
|
this.logger.debug(`File uploaded successfully: ${key}`);
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error uploading file to S3: ${error.message}`);
|
this.logger.error(`Error uploading file to S3: ${error.message}`);
|
||||||
throw error;
|
throw new InternalServerErrorException('Failed to upload file to storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteFile(key: string): Promise<void> {
|
async getFile(key: string): Promise<S3File> {
|
||||||
try {
|
try {
|
||||||
const command = new DeleteObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
|
||||||
Key: key,
|
Key: key,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.s3Client.send(command);
|
const response = await this.s3Client.send(command);
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error deleting file from S3: ${error.message}`);
|
const chunks = [];
|
||||||
throw error;
|
for await (const chunk of response.Body as any) {
|
||||||
}
|
chunks.push(chunk);
|
||||||
}
|
|
||||||
|
|
||||||
async getFileUrl(key: string): Promise<string> {
|
|
||||||
const command = new GetObjectCommand({
|
|
||||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
|
||||||
Key: key,
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
async testConnection(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.getObject('test-connection');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'NoSuchKey') {
|
|
||||||
// This is expected as we're just testing connection
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
this.logger.error('Failed to connect to S3', error);
|
const buffer = Buffer.concat(chunks);
|
||||||
return false;
|
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
contentType: response.ContentType || 'application/octet-stream',
|
||||||
|
contentLength: response.ContentLength || buffer.length,
|
||||||
|
fileName: key.split('/').pop() || 'download',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error getting file from S3: ${error.message}`);
|
||||||
|
throw new InternalServerErrorException('Failed to retrieve file from storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,5 +271,714 @@ function AdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default AdminPanel;
|
||||||
|
|
||||||
export default AdminPanel;
|
// import { useState, useEffect } from 'react';
|
||||||
|
// import { useAuth } from '../../hooks/useAuth';
|
||||||
|
// import { format } from 'date-fns';
|
||||||
|
// import { FiUpload, FiUsers, FiFile, FiTrash2, FiShare2, FiLoader, FiSearch, FiChevronRight } from 'react-icons/fi';
|
||||||
|
// import api from '../../services/api';
|
||||||
|
|
||||||
|
// function AdminPanel() {
|
||||||
|
// const [documents, setDocuments] = useState([]);
|
||||||
|
// const [users, setUsers] = useState([]);
|
||||||
|
// const [loading, setLoading] = useState(true);
|
||||||
|
// const [error, setError] = useState('');
|
||||||
|
// const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
// const [uploadTitle, setUploadTitle] = useState('');
|
||||||
|
// const [selectedUser, setSelectedUser] = useState('');
|
||||||
|
// const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
// const [expandedUsers, setExpandedUsers] = useState({});
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetchData();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// const fetchData = async () => {
|
||||||
|
// try {
|
||||||
|
// setLoading(true);
|
||||||
|
// const [documentsRes, usersRes] = await Promise.all([
|
||||||
|
// api.get('/admin/documents'),
|
||||||
|
// api.get('/admin/users')
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// console.log('Documents response:', documentsRes.data);
|
||||||
|
// console.log('Users response:', usersRes.data);
|
||||||
|
|
||||||
|
// // Group documents by user
|
||||||
|
// const groupedDocs = groupDocumentsByUser(documentsRes.data || []);
|
||||||
|
// setDocuments(groupedDocs);
|
||||||
|
// setUsers(usersRes.data || []);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError('Failed to fetch data');
|
||||||
|
// console.error('Error:', err);
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// const groupDocumentsByUser = (docs) => {
|
||||||
|
// return docs.reduce((acc, doc) => {
|
||||||
|
// const userName = doc.sharedWith?.name || 'Unassigned';
|
||||||
|
// if (!acc[userName]) {
|
||||||
|
// acc[userName] = [];
|
||||||
|
// }
|
||||||
|
// acc[userName].push(doc);
|
||||||
|
// return acc;
|
||||||
|
// }, {});
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const toggleUser = (userName) => {
|
||||||
|
// setExpandedUsers(prev => ({
|
||||||
|
// ...prev,
|
||||||
|
// [userName]: !prev[userName]
|
||||||
|
// }));
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleFileUpload = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// if (!selectedFile || !uploadTitle || !selectedUser) {
|
||||||
|
// alert('Please fill in all fields');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const formData = new FormData();
|
||||||
|
// formData.append('file', selectedFile);
|
||||||
|
// formData.append('title', uploadTitle);
|
||||||
|
// formData.append('sharedWith', selectedUser);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.post('/admin/documents', formData, {
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'multipart/form-data',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// setSelectedFile(null);
|
||||||
|
// setUploadTitle('');
|
||||||
|
// setSelectedUser('');
|
||||||
|
// fetchData();
|
||||||
|
// alert('Document uploaded successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Upload error:', err);
|
||||||
|
// alert('Failed to upload document');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleDelete = async (documentId) => {
|
||||||
|
// if (!window.confirm('Are you sure you want to delete this document?')) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.delete(`/admin/documents/${documentId}`);
|
||||||
|
// fetchData();
|
||||||
|
// alert('Document deleted successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Delete error:', err);
|
||||||
|
// alert('Failed to delete document');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleShare = async (documentId) => {
|
||||||
|
// const userId = prompt('Enter user ID to share with:');
|
||||||
|
// if (!userId) return;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.post(`/admin/documents/${documentId}/share`, { userId });
|
||||||
|
// fetchData();
|
||||||
|
// alert('Document shared successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Share error:', err);
|
||||||
|
// alert('Failed to share document');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (loading) {
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
// <div className="flex items-center space-x-3 text-blue-500">
|
||||||
|
// <FiLoader className="w-6 h-6 animate-spin" />
|
||||||
|
// <span className="text-lg font-medium">Loading admin panel...</span>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (error) {
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
// <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
|
||||||
|
// {error}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
|
||||||
|
// <div className="max-w-7xl mx-auto">
|
||||||
|
// <header className="mb-8">
|
||||||
|
// <h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||||
|
// <p className="text-gray-400">Manage documents and users</p>
|
||||||
|
// </header>
|
||||||
|
|
||||||
|
// {/* Upload Section - remains the same */}
|
||||||
|
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 mb-8">
|
||||||
|
// {/* ... upload form content remains the same ... */}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Search Bar */}
|
||||||
|
// <div className="relative mb-6">
|
||||||
|
// <FiSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// placeholder="Search documents..."
|
||||||
|
// value={searchTerm}
|
||||||
|
// onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
// className="w-full pl-10 pr-4 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Documents List - Grouped by User */}
|
||||||
|
// <div className="space-y-4">
|
||||||
|
// {Object.entries(documents).map(([userName, userDocs]) => {
|
||||||
|
// const filteredDocs = userDocs.filter(doc =>
|
||||||
|
// doc.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (filteredDocs.length === 0) return null;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div
|
||||||
|
// key={userName}
|
||||||
|
// className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden"
|
||||||
|
// >
|
||||||
|
// <button
|
||||||
|
// onClick={() => toggleUser(userName)}
|
||||||
|
// className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center space-x-3">
|
||||||
|
// <FiUsers className="w-5 h-5 text-blue-400" />
|
||||||
|
// <span className="font-medium">{userName}</span>
|
||||||
|
// <span className="text-sm text-gray-400">({filteredDocs.length} documents)</span>
|
||||||
|
// </div>
|
||||||
|
// <FiChevronRight
|
||||||
|
// className={`w-5 h-5 transition-transform duration-200 ${
|
||||||
|
// expandedUsers[userName] ? 'rotate-90' : ''
|
||||||
|
// }`}
|
||||||
|
// />
|
||||||
|
// </button>
|
||||||
|
|
||||||
|
// {expandedUsers[userName] && (
|
||||||
|
// <div className="border-t border-gray-700 divide-y divide-gray-700">
|
||||||
|
// {filteredDocs.map(doc => (
|
||||||
|
// <div
|
||||||
|
// key={doc.id}
|
||||||
|
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center space-x-4">
|
||||||
|
// <FiFile className="w-5 h-5 text-gray-400" />
|
||||||
|
// <div>
|
||||||
|
// <h3 className="text-white font-medium">{doc.title}</h3>
|
||||||
|
// <p className="text-sm text-gray-400">
|
||||||
|
// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')}
|
||||||
|
// </p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="flex items-center space-x-2">
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleShare(doc.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
// title="Share document"
|
||||||
|
// >
|
||||||
|
// <FiShare2 className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleDelete(doc.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
|
||||||
|
// title="Delete document"
|
||||||
|
// >
|
||||||
|
// <FiTrash2 className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
|
||||||
|
// {Object.keys(documents).length === 0 && (
|
||||||
|
// <div className="text-center py-12 text-gray-400">
|
||||||
|
// No documents available
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default AdminPanel;
|
||||||
|
// import { useState, useEffect } from 'react';
|
||||||
|
// import { useAuth } from '../../hooks/useAuth';
|
||||||
|
// import { format } from 'date-fns';
|
||||||
|
// import {
|
||||||
|
// FiUpload,
|
||||||
|
// FiDownload,
|
||||||
|
// FiUsers,
|
||||||
|
// FiFile,
|
||||||
|
// FiTrash2,
|
||||||
|
// FiShare2,
|
||||||
|
// FiLoader,
|
||||||
|
// FiSearch,
|
||||||
|
// FiChevronRight,
|
||||||
|
// FiUserPlus,
|
||||||
|
// FiEdit2,
|
||||||
|
// FiKey
|
||||||
|
// } from 'react-icons/fi';
|
||||||
|
// import api from '../../services/api';
|
||||||
|
|
||||||
|
// function AdminPanel() {
|
||||||
|
// const [documents, setDocuments] = useState({});
|
||||||
|
// const [users, setUsers] = useState([]);
|
||||||
|
// const [expandedUsers, setExpandedUsers] = useState({});
|
||||||
|
// const [loading, setLoading] = useState(true);
|
||||||
|
// const [error, setError] = useState('');
|
||||||
|
// const [selectedFile, setSelectedFile] = useState(null);
|
||||||
|
// const [uploadTitle, setUploadTitle] = useState('');
|
||||||
|
// const [selectedUser, setSelectedUser] = useState('');
|
||||||
|
// const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
// const [activeTab, setActiveTab] = useState('documents'); // 'documents' or 'users'
|
||||||
|
// const [newUser, setNewUser] = useState({ username: '', password: '', name: '', isAdmin: false });
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// fetchData();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// const fetchData = async () => {
|
||||||
|
// try {
|
||||||
|
// setLoading(true);
|
||||||
|
// const [documentsRes, usersRes] = await Promise.all([
|
||||||
|
// api.get('/admin/documents'),
|
||||||
|
// api.get('/admin/users')
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// const groupedDocs = groupDocumentsByUser(documentsRes.data || []);
|
||||||
|
// const initialExpandedState = Object.keys(groupedDocs).reduce((acc, userName) => {
|
||||||
|
// acc[userName] = false;
|
||||||
|
// return acc;
|
||||||
|
// }, {});
|
||||||
|
|
||||||
|
// setDocuments(groupedDocs);
|
||||||
|
// setUsers(usersRes.data || []);
|
||||||
|
// setExpandedUsers(initialExpandedState);
|
||||||
|
// } catch (err) {
|
||||||
|
// setError('Failed to fetch data');
|
||||||
|
// console.error('Error:', err);
|
||||||
|
// } finally {
|
||||||
|
// setLoading(false);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const groupDocumentsByUser = (docs) => {
|
||||||
|
// return docs.reduce((acc, doc) => {
|
||||||
|
// const userName = doc.sharedWith?.name || 'Unassigned';
|
||||||
|
// if (!acc[userName]) {
|
||||||
|
// acc[userName] = [];
|
||||||
|
// }
|
||||||
|
// acc[userName].push(doc);
|
||||||
|
// return acc;
|
||||||
|
// }, {});
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleAddUser = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// try {
|
||||||
|
// await api.post('/admin/users', newUser);
|
||||||
|
// setNewUser({ username: '', password: '', name: '', isAdmin: false });
|
||||||
|
// fetchData();
|
||||||
|
// alert('User added successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Error adding user:', err);
|
||||||
|
// alert('Failed to add user');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleDeleteUser = async (userId) => {
|
||||||
|
// if (!window.confirm('Are you sure you want to delete this user?')) return;
|
||||||
|
// try {
|
||||||
|
// await api.delete(`/admin/users/${userId}`);
|
||||||
|
// fetchData();
|
||||||
|
// alert('User deleted successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Error deleting user:', err);
|
||||||
|
// alert('Failed to delete user');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleResetPassword = async (userId) => {
|
||||||
|
// const newPassword = prompt('Enter new password:');
|
||||||
|
// if (!newPassword) return;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.post(`/admin/users/${userId}/reset-password`, { password: newPassword });
|
||||||
|
// alert('Password reset successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Error resetting password:', err);
|
||||||
|
// alert('Failed to reset password');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const handleFileUpload = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// if (!selectedFile || !uploadTitle || !selectedUser) {
|
||||||
|
// alert('Please fill in all fields');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const formData = new FormData();
|
||||||
|
// formData.append('file', selectedFile);
|
||||||
|
// formData.append('title', uploadTitle);
|
||||||
|
// formData.append('sharedWith', selectedUser);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await api.post('/admin/documents', formData, {
|
||||||
|
// headers: {
|
||||||
|
// 'Content-Type': 'multipart/form-data',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// setSelectedFile(null);
|
||||||
|
// setUploadTitle('');
|
||||||
|
// setSelectedUser('');
|
||||||
|
// fetchData();
|
||||||
|
// alert('Document uploaded successfully');
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Upload error:', err);
|
||||||
|
// alert('Failed to upload document');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// const toggleUser = (userName) => {
|
||||||
|
// console.log('Toggling user:', userName); // Debug log
|
||||||
|
// setExpandedUsers(prev => ({
|
||||||
|
// ...prev,
|
||||||
|
// [userName]: !prev[userName]
|
||||||
|
// }));
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const handleDownload = async (s3Key, fileName) => {
|
||||||
|
// try {
|
||||||
|
// const token = localStorage.getItem('token');
|
||||||
|
// // Remove 'documents/' from the s3Key as it's already in the path
|
||||||
|
// const cleanS3Key = s3Key.replace('documents/', '');
|
||||||
|
|
||||||
|
// const response = await fetch(`http://localhost:3000/admin/documents/download/${cleanS3Key}`, {
|
||||||
|
// headers: {
|
||||||
|
// 'Authorization': `Bearer ${token}`
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error('Download failed');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const blob = await response.blob();
|
||||||
|
// const url = window.URL.createObjectURL(blob);
|
||||||
|
// const link = document.createElement('a');
|
||||||
|
// link.href = url;
|
||||||
|
// link.download = fileName;
|
||||||
|
// document.body.appendChild(link);
|
||||||
|
// link.click();
|
||||||
|
// document.body.removeChild(link);
|
||||||
|
// window.URL.revokeObjectURL(url);
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('Error downloading document:', err);
|
||||||
|
// alert('Failed to download document. Please try again.');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
// if (loading) {
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
|
// <div className="flex items-center space-x-3 text-blue-500">
|
||||||
|
// <FiLoader className="w-6 h-6 animate-spin" />
|
||||||
|
// <span className="text-lg font-medium">Loading admin panel...</span>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
|
||||||
|
// <div className="max-w-7xl mx-auto mt-20 text-center">
|
||||||
|
// <header className="mb-8">
|
||||||
|
// <h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
|
||||||
|
// <p className="text-gray-400">Manage documents and users</p>
|
||||||
|
// </header>
|
||||||
|
|
||||||
|
// {/* Tab Navigation */}
|
||||||
|
// <div className="flex space-x-4 mb-8">
|
||||||
|
// <button
|
||||||
|
// onClick={() => setActiveTab('documents')}
|
||||||
|
// className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
// activeTab === 'documents'
|
||||||
|
// ? 'bg-blue-600 text-white'
|
||||||
|
// : 'text-gray-400 hover:text-white'
|
||||||
|
// }`}
|
||||||
|
// >
|
||||||
|
// Documents
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={() => setActiveTab('users')}
|
||||||
|
// className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
// activeTab === 'users'
|
||||||
|
// ? 'bg-blue-600 text-white'
|
||||||
|
// : 'text-gray-400 hover:text-white'
|
||||||
|
// }`}
|
||||||
|
// >
|
||||||
|
// Users
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {activeTab === 'documents' ? (
|
||||||
|
// <>
|
||||||
|
// {/* Upload Section */}
|
||||||
|
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 mb-8">
|
||||||
|
// <h2 className="text-xl font-semibold text-white mb-4">Upload Document</h2>
|
||||||
|
// <form onSubmit={handleFileUpload} className="space-y-4">
|
||||||
|
// <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">Document Title</label>
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// value={uploadTitle}
|
||||||
|
// onChange={(e) => setUploadTitle(e.target.value)}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400"
|
||||||
|
// placeholder="Enter document title"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">Share With User</label>
|
||||||
|
// <select
|
||||||
|
// value={selectedUser}
|
||||||
|
// onChange={(e) => setSelectedUser(e.target.value)}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||||
|
// >
|
||||||
|
// <option value="">Select user</option>
|
||||||
|
// {Array.isArray(users) && users.map(user => (
|
||||||
|
// <option key={user.id} value={user.id}>{user.name}</option>
|
||||||
|
// ))}
|
||||||
|
// </select>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">File</label>
|
||||||
|
// <input
|
||||||
|
// type="file"
|
||||||
|
// onChange={(e) => setSelectedFile(e.target.files[0])}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-500 file:text-white hover:file:bg-blue-600"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <button
|
||||||
|
// type="submit"
|
||||||
|
// className="w-full md:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2"
|
||||||
|
// >
|
||||||
|
// <FiUpload className="w-4 h-4" />
|
||||||
|
// <span>Upload Document</span>
|
||||||
|
// </button>
|
||||||
|
// </form>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Documents List */}
|
||||||
|
// <div className="space-y-4">
|
||||||
|
// {Object.entries(documents).map(([userName, userDocs]) => (
|
||||||
|
// <div
|
||||||
|
// key={userName}
|
||||||
|
// className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden"
|
||||||
|
// >
|
||||||
|
// <button
|
||||||
|
// onClick={() => toggleUser(userName)}
|
||||||
|
// className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center space-x-3">
|
||||||
|
// <FiUsers className="w-5 h-5 text-blue-400" />
|
||||||
|
// <span className="font-medium">{userName}</span>
|
||||||
|
// <span className="text-sm text-gray-400">({userDocs.length} documents)</span>
|
||||||
|
// </div>
|
||||||
|
// <FiChevronRight
|
||||||
|
// className={`w-5 h-5 transition-transform duration-200 ${
|
||||||
|
// expandedUsers[userName] ? 'rotate-90' : ''
|
||||||
|
// }`}
|
||||||
|
// />
|
||||||
|
// </button>
|
||||||
|
// {expandedUsers[userName] && (
|
||||||
|
// <div className="border-t border-gray-700 divide-y divide-gray-700">
|
||||||
|
// {userDocs.map(doc => (
|
||||||
|
// <div
|
||||||
|
// key={doc.id}
|
||||||
|
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center space-x-4">
|
||||||
|
// <FiFile className="w-5 h-5 text-gray-400" />
|
||||||
|
// <div>
|
||||||
|
// <h3 className="text-white font-medium">{doc.title}</h3>
|
||||||
|
// <p className="text-sm text-gray-400">
|
||||||
|
// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')}
|
||||||
|
// </p>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="flex items-center space-x-2">
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleDownload(doc.s3Key, doc.title)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
// title="Download document"
|
||||||
|
// >
|
||||||
|
// <FiDownload className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleShare(doc.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
// title="Share document"
|
||||||
|
// >
|
||||||
|
// <FiShare2 className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleDelete(doc.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
|
||||||
|
// title="Delete document"
|
||||||
|
// >
|
||||||
|
// <FiTrash2 className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
|
// ) : (
|
||||||
|
// // Users Management Section
|
||||||
|
// <div className="space-y-8">
|
||||||
|
// {/* Add New User Form */}
|
||||||
|
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6">
|
||||||
|
// <h2 className="text-xl font-semibold text-white mb-4">Add New User</h2>
|
||||||
|
// <form onSubmit={handleAddUser} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">Username</label>
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// value={newUser.username}
|
||||||
|
// onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||||
|
// required
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">Password</label>
|
||||||
|
// <input
|
||||||
|
// type="password"
|
||||||
|
// value={newUser.password}
|
||||||
|
// onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||||
|
// required
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <label className="block text-sm font-medium text-gray-400 mb-1">Full Name</label>
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// value={newUser.name}
|
||||||
|
// onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
|
||||||
|
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
|
||||||
|
// required
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <div className="flex items-center space-x-2">
|
||||||
|
// <input
|
||||||
|
// type="checkbox"
|
||||||
|
// id="isAdmin"
|
||||||
|
// checked={newUser.isAdmin}
|
||||||
|
// onChange={(e) => setNewUser({ ...newUser, isAdmin: e.target.checked })}
|
||||||
|
// className="w-4 h-4 rounded border-gray-700 text-blue-600 focus:ring-blue-500"
|
||||||
|
// />
|
||||||
|
// <label htmlFor="isAdmin" className="text-sm font-medium text-gray-400">
|
||||||
|
// Admin User
|
||||||
|
// </label>
|
||||||
|
// </div>
|
||||||
|
// <div className="md:col-span-2">
|
||||||
|
// <button
|
||||||
|
// type="submit"
|
||||||
|
// className="w-full md:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2"
|
||||||
|
// >
|
||||||
|
// <FiUserPlus className="w-4 h-4" />
|
||||||
|
// <span>Add User</span>
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// {/* Users List */}
|
||||||
|
// <div className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden">
|
||||||
|
// <div className="px-6 py-4 border-b border-gray-700">
|
||||||
|
// <h2 className="text-xl font-semibold text-white">Users</h2>
|
||||||
|
// </div>
|
||||||
|
// <div className="divide-y divide-gray-700">
|
||||||
|
// {Array.isArray(users) && users.map(user => (
|
||||||
|
// <div
|
||||||
|
// key={user.id}
|
||||||
|
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
|
||||||
|
// >
|
||||||
|
// <div className="flex items-center space-x-4">
|
||||||
|
// <FiUsers className="w-5 h-5 text-gray-400" />
|
||||||
|
// <div>
|
||||||
|
// <h3 className="text-white font-medium">{user.name}</h3>
|
||||||
|
// <p className="text-sm text-gray-400">@{user.username}</p>
|
||||||
|
// </div>
|
||||||
|
// {user.isAdmin && (
|
||||||
|
// <span className="px-2 py-1 text-xs font-medium text-blue-400 bg-blue-400/10 rounded-full">
|
||||||
|
// Admin
|
||||||
|
// </span>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="flex items-center space-x-2">
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleResetPassword(user.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
// title="Reset password"
|
||||||
|
// >
|
||||||
|
// <FiKey className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// <button
|
||||||
|
// onClick={() => handleDeleteUser(user.id)}
|
||||||
|
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
|
||||||
|
// title="Delete user"
|
||||||
|
// >
|
||||||
|
// <FiTrash2 className="w-4 h-4" />
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default AdminPanel;
|
||||||
@ -1,13 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
|
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { getSharedDocuments } from '../../services/api';
|
import { getSharedDocuments } from '../../services/api';
|
||||||
import api from '../../services/api';
|
import { format } from 'date-fns';
|
||||||
|
import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi';
|
||||||
|
import { downloadDocument } from '../../services/api';
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const [documents, setDocuments] = useState([]);
|
const [documents, setDocuments] = useState({});
|
||||||
const [expandedFolders, setExpandedFolders] = useState({});
|
const [expandedFolders, setExpandedFolders] = useState({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@ -16,23 +15,29 @@ function Dashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDocuments = async () => {
|
const fetchDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
const response = await getSharedDocuments(user.id);
|
const response = await getSharedDocuments(user.id);
|
||||||
|
// Group the documents by company and date
|
||||||
const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
|
const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
|
||||||
setDocuments(groupedDocs);
|
setDocuments(groupedDocs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to fetch documents');
|
|
||||||
console.error('Error fetching documents:', err);
|
console.error('Error fetching documents:', err);
|
||||||
|
setError('Failed to fetch documents');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (user) {
|
fetchDocuments();
|
||||||
fetchDocuments();
|
|
||||||
}
|
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const groupDocumentsByCompanyAndDate = (docs) => {
|
const groupDocumentsByCompanyAndDate = (docs) => {
|
||||||
|
if (!Array.isArray(docs)) {
|
||||||
|
console.error('Expected array of documents, received:', docs);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
return docs.reduce((acc, doc) => {
|
return docs.reduce((acc, doc) => {
|
||||||
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
||||||
if (!acc[folderName]) {
|
if (!acc[folderName]) {
|
||||||
@ -43,52 +48,16 @@ function Dashboard() {
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleDownload = async (s3Key, fileName) => {
|
const handleDownload = async (s3Key, fileName) => {
|
||||||
// try {
|
try {
|
||||||
// const response = await api.get(`/documents/shared/download/${s3Key}`, {
|
await downloadDocument(s3Key);
|
||||||
// responseType: 'blob',
|
} catch (err) {
|
||||||
// });
|
console.error('Error downloading document:', err);
|
||||||
// const url = window.URL.createObjectURL(new Blob([response.data]));
|
alert('Failed to download document. Please try again.');
|
||||||
// const link = document.createElement('a');
|
}
|
||||||
// link.href = url;
|
};
|
||||||
// link.setAttribute('download', fileName); // or use the actual filename
|
|
||||||
// document.body.appendChild(link);
|
|
||||||
// link.click();
|
|
||||||
// link.parentNode.removeChild(link);
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error('Error downloading document:', err);
|
|
||||||
// alert('Failed to download document. Please try again.');
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// const groupDocumentsByCompanyAndDate = (docs) => {
|
|
||||||
// return docs.reduce((acc, doc) => {
|
|
||||||
// const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
|
||||||
// if (!acc[folderName]) {
|
|
||||||
// acc[folderName] = [];
|
|
||||||
// }
|
|
||||||
// acc[folderName].push(doc);
|
|
||||||
// return acc;
|
|
||||||
// }, {});
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleDownload = async (s3Key, fileName) => {
|
const toggleFolder = (folderName) => {
|
||||||
try {
|
|
||||||
const response = await api.get(`/documents/shared/download/${s3Key}`, {
|
|
||||||
responseType: 'blob',
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute('download', fileName); // or use the actual filename
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
link.parentNode.removeChild(link);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error downloading document:', err);
|
|
||||||
alert('Failed to download document. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const toggleFolder = (folderName) => {
|
|
||||||
setExpandedFolders(prev => ({
|
setExpandedFolders(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
[folderName]: !prev[folderName]
|
[folderName]: !prev[folderName]
|
||||||
@ -97,94 +66,87 @@ const toggleFolder = (folderName) => {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
<div className="flex items-center space-x-3 text-blue-500">
|
||||||
|
<FiLoader className="w-6 h-6 animate-spin" />
|
||||||
|
<span className="text-lg font-medium">Loading documents...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-red-600 p-4">
|
<div className="min-h-screen flex items-center justify-center bg-gray-900">
|
||||||
{error}
|
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
|
||||||
<h1 className="text-2xl font-bold mb-6">Your Laboratory Reports</h1>
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<header className="mb-8 mt-20">
|
||||||
{Object.entries(documents).length === 0 ? (
|
<h1 className="text-3xl font-bold text-white mb-2">Your Documents</h1>
|
||||||
<p className="text-gray-600">No documents have been shared with you yet.</p>
|
<p className="text-gray-400">Access and manage your shared documents</p>
|
||||||
) : (
|
</header>
|
||||||
<div className="grid gap-4">
|
|
||||||
{Object.entries(documents).map(([folderName, folderDocs]) => (
|
|
||||||
<motion.div
|
|
||||||
key={folderName}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="bg-white p-4 rounded-lg shadow-md"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => toggleFolder(folderName)}
|
|
||||||
className="w-full flex items-center text-left p-2 hover:bg-gray-50 rounded-md"
|
|
||||||
>
|
|
||||||
<FiFolder className="text-blue-600 mr-2" />
|
|
||||||
{expandedFolders[folderName] ? (
|
|
||||||
<FiChevronDown className="mr-2" />
|
|
||||||
) : (
|
|
||||||
<FiChevronRight className="mr-2" />
|
|
||||||
)}
|
|
||||||
<span className="font-medium">{folderName}</span>
|
|
||||||
<span className="ml-2 text-sm text-gray-500">
|
|
||||||
({folderDocs.length} {folderDocs.length === 1 ? 'report' : 'reports'})
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{expandedFolders[folderName] && (
|
<div className="grid gap-6">
|
||||||
<motion.div
|
{Object.entries(documents).length > 0 ? (
|
||||||
initial={{ opacity: 0, height: 0 }}
|
Object.entries(documents).map(([folderName, docs]) => (
|
||||||
animate={{ opacity: 1, height: 'auto' }}
|
<div
|
||||||
className="mt-2 space-y-2 pl-8"
|
key={folderName}
|
||||||
|
className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden transition-all duration-200 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFolder(folderName)}
|
||||||
|
className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
|
||||||
>
|
>
|
||||||
{folderDocs.map((doc) => (
|
<div className="flex items-center space-x-3">
|
||||||
<div
|
<FiFolder className="w-5 h-5 text-blue-400" />
|
||||||
key={doc.id}
|
<span className="font-medium">{folderName}</span>
|
||||||
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
<span className="text-sm text-gray-400">({docs.length} files)</span>
|
||||||
>
|
</div>
|
||||||
<div className="flex justify-between items-start mb-4">
|
<FiChevronRight
|
||||||
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
className={`w-5 h-5 transition-transform duration-200 ${
|
||||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
expandedFolders[folderName] ? 'rotate-90' : ''
|
||||||
doc.status === 'completed'
|
}`}
|
||||||
? 'bg-green-100 text-green-800'
|
/>
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
</button>
|
||||||
}`}>
|
|
||||||
{doc.status}
|
{expandedFolders[folderName] && (
|
||||||
</span>
|
<div className="border-t border-gray-700">
|
||||||
</div>
|
{docs.map((doc) => (
|
||||||
|
<div
|
||||||
<div className="text-sm text-gray-600">
|
key={doc.id}
|
||||||
<p>Created: {new Date(doc.createdAt).toLocaleDateString()}</p>
|
className="px-6 py-3 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||||
</div>
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
<div className="mt-4 flex justify-end">
|
<FiFile className="w-4 h-4 text-gray-400" />
|
||||||
<button
|
<span className="text-gray-200">{doc.title}</span>
|
||||||
onClick={() => handleDownload(doc.s3Key, doc.title)}
|
</div>
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
<button
|
||||||
|
onClick={() => handleDownload(doc.s3Key)}
|
||||||
|
className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
|
||||||
|
title="Download document"
|
||||||
>
|
>
|
||||||
<FiDownload className="inline-block mr-2" />
|
<FiDownload className="w-4 h-4" />
|
||||||
Download
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
</motion.div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</motion.div>
|
))
|
||||||
))}
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-400">No documents available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { uploadDocument, getAllUsers } from '../../services/api';
|
import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api';
|
||||||
|
|
||||||
function DocumentUpload() {
|
function DocumentUpload() {
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
@ -8,9 +8,11 @@ function DocumentUpload() {
|
|||||||
const [availableUsers, setAvailableUsers] = useState([]);
|
const [availableUsers, setAvailableUsers] = useState([]);
|
||||||
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
|
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
|
getCurrentUser();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
@ -22,18 +24,22 @@ function DocumentUpload() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (event) => {
|
const getCurrentUser = async () => {
|
||||||
const selectedFile = event.target.files[0];
|
try {
|
||||||
setFile(selectedFile);
|
const response = await getUserInfo();
|
||||||
|
console.log('Current user data:', response.data); // Debug log
|
||||||
|
setCurrentUser(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get current user:', error);
|
||||||
|
setErrorMessage('Failed to get current user info');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
console.log('Form submission:', { file, title, selectedUsers });
|
if (!file || !title || selectedUsers.length === 0 || !currentUser) {
|
||||||
|
setErrorMessage('Please provide all required information');
|
||||||
if (!file || !title || selectedUsers.length === 0) {
|
|
||||||
setErrorMessage('Please provide a title, file, and select at least one user');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +49,18 @@ const handleSubmit = async (event) => {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('title', title);
|
formData.append('title', title);
|
||||||
formData.append('sharedWith', selectedUsers[0]);
|
formData.append('sharedWithId', selectedUsers[0]); // Remove toString()
|
||||||
|
formData.append('uploadedById', currentUser.id); // Remove toString()
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
console.log('Form Data Contents:', {
|
||||||
|
file: formData.get('file'),
|
||||||
|
title: formData.get('title'),
|
||||||
|
sharedWithId: formData.get('sharedWithId'),
|
||||||
|
uploadedById: formData.get('uploadedById')
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Sending request with:', {
|
|
||||||
title,
|
|
||||||
sharedWith: selectedUsers[0],
|
|
||||||
fileSize: file.size
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await uploadDocument(formData);
|
const response = await uploadDocument(formData);
|
||||||
console.log('Upload response:', response);
|
console.log('Upload response:', response);
|
||||||
|
|
||||||
@ -66,6 +75,17 @@ const handleSubmit = async (event) => {
|
|||||||
setErrorMessage(error.response?.data?.message || 'Upload failed');
|
setErrorMessage(error.response?.data?.message || 'Upload failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleFileChange(e) {
|
||||||
|
setFile(e.target.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserSelect = (e) => {
|
||||||
|
const values = Array.from(e.target.selectedOptions, option => option.value);
|
||||||
|
console.log('Selected user values:', values);
|
||||||
|
setSelectedUsers(values);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
|
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
|
||||||
<h2 className="text-2xl font-bold mb-6">Upload Document</h2>
|
<h2 className="text-2xl font-bold mb-6">Upload Document</h2>
|
||||||
@ -103,7 +123,7 @@ const handleSubmit = async (event) => {
|
|||||||
<select
|
<select
|
||||||
multiple
|
multiple
|
||||||
value={selectedUsers}
|
value={selectedUsers}
|
||||||
onChange={(e) => setSelectedUsers(Array.from(e.target.selectedOptions, option => option.value))}
|
onChange={handleUserSelect}
|
||||||
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,9 +1,90 @@
|
|||||||
|
// import { useState } from 'react';
|
||||||
|
// import { useAuth } from '../../hooks/useAuth';
|
||||||
|
// import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// const Login = () => {
|
||||||
|
// const [username, setUsername] = useState(''); // Changed from email to username
|
||||||
|
// const [password, setPassword] = useState('');
|
||||||
|
// const [error, setError] = useState('');
|
||||||
|
// const { login } = useAuth();
|
||||||
|
// const navigate = useNavigate();
|
||||||
|
|
||||||
|
// const handleSubmit = async (e) => {
|
||||||
|
// e.preventDefault();
|
||||||
|
// setError('');
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const userData = await login(username, password);
|
||||||
|
// console.log('Login result:', userData);
|
||||||
|
// if (userData?.isAdmin) {
|
||||||
|
// navigate('/admin');
|
||||||
|
// } else {
|
||||||
|
// navigate('/dashboard');
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// setError('Invalid credentials');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
// <div className="max-w-md w-full space-y-8">
|
||||||
|
// <div>
|
||||||
|
// <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
// Sign in to your account
|
||||||
|
// </h2>
|
||||||
|
// </div>
|
||||||
|
// <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
|
// {error && (
|
||||||
|
// <div className="text-red-500 text-center text-sm">
|
||||||
|
// {error}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// <div className="rounded-md shadow-sm -space-y-px">
|
||||||
|
// <div>
|
||||||
|
// <input
|
||||||
|
// type="text"
|
||||||
|
// required
|
||||||
|
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
// placeholder="Username"
|
||||||
|
// value={username}
|
||||||
|
// onChange={(e) => setUsername(e.target.value)}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// <div>
|
||||||
|
// <input
|
||||||
|
// type="password"
|
||||||
|
// required
|
||||||
|
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
|
// placeholder="Password"
|
||||||
|
// value={password}
|
||||||
|
// onChange={(e) => setPassword(e.target.value)}
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div>
|
||||||
|
// <button
|
||||||
|
// type="submit"
|
||||||
|
// className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
// >
|
||||||
|
// Sign in
|
||||||
|
// </button>
|
||||||
|
// </div>
|
||||||
|
// </form>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default Login;
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { FiUser, FiLock } from 'react-icons/fi'; // Import icons
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [username, setUsername] = useState(''); // Changed from email to username
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
@ -15,8 +96,7 @@ const Login = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const userData = await login(username, password);
|
const userData = await login(username, password);
|
||||||
console.log('Login result:', userData);
|
if (userData?.isAdmin) {
|
||||||
if (userData?.isAdmin) {
|
|
||||||
navigate('/admin');
|
navigate('/admin');
|
||||||
} else {
|
} else {
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
@ -27,35 +107,39 @@ const Login = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
||||||
<div className="max-w-md w-full space-y-8">
|
<div className="w-full max-w-md px-8 py-10 bg-white/5 backdrop-blur-lg rounded-2xl shadow-2xl">
|
||||||
<div>
|
<div className="mb-10 text-center">
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
<h1 className="text-3xl font-bold text-white mb-2">Welcome Back</h1>
|
||||||
Sign in to your account
|
<p className="text-gray-400">Please sign in to continue</p>
|
||||||
</h2>
|
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-red-500 text-center text-sm">
|
<div className="p-3 text-sm text-red-200 bg-red-500/10 border border-red-500/20 rounded-lg">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="rounded-md shadow-sm -space-y-px">
|
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
|
<div className="relative">
|
||||||
|
<FiUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
<div className="relative">
|
||||||
|
<FiLock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
@ -63,13 +147,17 @@ const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<button
|
||||||
<button
|
type="submit"
|
||||||
type="submit"
|
className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
|
||||||
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
>
|
||||||
>
|
Sign In
|
||||||
Sign in
|
</button>
|
||||||
</button>
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Need help? Contact your administrator
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,27 +22,50 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const downloadDocument = async (documentId) => {
|
export const getSharedDocuments = (userId) => {
|
||||||
|
return api.get(`/documents/shared/${userId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getToken = () => localStorage.getItem('token');
|
||||||
|
export const downloadDocument = async (key) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/documents/download/${documentId}`, {
|
const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
'Accept': 'application/octet-stream',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return response.data;
|
|
||||||
|
// Create blob URL and trigger download
|
||||||
|
const blob = new Blob([response.data]);
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
// Extract filename from key
|
||||||
|
const fileName = key.split('/').pop();
|
||||||
|
link.download = fileName;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Download error:', error);
|
console.error('Download error:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const createUser = (userData) => {
|
export const createUser = (userData) => {
|
||||||
return api.post('/admin/users', {
|
return api.post('/admin/users', {
|
||||||
name: userData.name,
|
name: userData.name,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
password: userData.password,
|
password: userData.password,
|
||||||
isAdmin: userData.isAdmin // Add this line
|
isAdmin: userData.isAdmin
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const login = (username, password) => api.post('/auth/login', { username, password });
|
export const login = (username, password) => api.post('/auth/login', { username, password });
|
||||||
@ -51,15 +74,21 @@ export const updateDocumentStatus = (documentId, status) => api.put(`/admin/docu
|
|||||||
|
|
||||||
export const uploadDocument = async (formData) => {
|
export const uploadDocument = async (formData) => {
|
||||||
try {
|
try {
|
||||||
|
// Debug log
|
||||||
|
console.log('Sending to server:', {
|
||||||
|
title: formData.get('title'),
|
||||||
|
sharedWithId: formData.get('sharedWithId'),
|
||||||
|
uploadedById: formData.get('uploadedById')
|
||||||
|
});
|
||||||
|
|
||||||
const response = await api.post('/admin/documents', formData, {
|
const response = await api.post('/admin/documents', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log('Upload API response:', response.data);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload API error:', error);
|
console.error('API Error:', error.response?.data);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -67,7 +96,7 @@ export const getUserInfo = () => api.get('/auth/user-info');
|
|||||||
|
|
||||||
|
|
||||||
export const getAllDocuments = () => api.get('/admin/documents');
|
export const getAllDocuments = () => api.get('/admin/documents');
|
||||||
export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
|
// export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
|
||||||
export const getAllUsers = () => api.get('/admin/users');
|
export const getAllUsers = () => api.get('/admin/users');
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user