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
|
||||
|
||||
DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public"
|
||||
JWT_SECRET=some-secret
|
||||
AWS_REGION=EU2
|
||||
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
|
||||
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 {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String?
|
||||
s3Key String
|
||||
status String @default("pending")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sharedWithId Int
|
||||
sharedWith User @relation("SharedDocuments", fields: [sharedWithId], references: [id])
|
||||
status String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
uploadedById Int
|
||||
uploadedBy User @relation("UploadedDocuments", fields: [uploadedById], references: [id])
|
||||
Notification Notification[]
|
||||
sharedWith User[] @relation("SharedDocuments")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
password String
|
||||
isAdmin Boolean @default(false)
|
||||
sharedDocuments Document[] @relation("SharedDocuments")
|
||||
notifications Notification[]
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
password String
|
||||
isAdmin Boolean @default(false)
|
||||
uploadedDocuments Document[] @relation("UploadedDocuments")
|
||||
Notification Notification[]
|
||||
sharedDocuments Document[] @relation("SharedDocuments")
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
message String
|
||||
read Boolean @default(false)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
id Int @id @default(autoincrement())
|
||||
message String
|
||||
read Boolean @default(false)
|
||||
userId Int
|
||||
createdAt DateTime @default(now())
|
||||
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 {
|
||||
Controller,
|
||||
Get,
|
||||
@ -9,10 +124,10 @@ import {
|
||||
UploadedFile,
|
||||
ParseIntPipe,
|
||||
UseGuards,
|
||||
BadRequestException,
|
||||
} 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';
|
||||
@ -44,27 +159,60 @@ export class AdminController {
|
||||
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')
|
||||
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('test-document')
|
||||
// async testDocumentCreation() {
|
||||
// try {
|
||||
// const document = await this.prisma.document.create({
|
||||
// data: {
|
||||
// title: 'Test Document',
|
||||
// s3Key: 'test-key',
|
||||
// status: 'completed',
|
||||
// sharedWith: {
|
||||
// connect: { id: 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) {
|
||||
@ -87,41 +235,14 @@ export class AdminController {
|
||||
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 {
|
||||
const document = await this.adminService.uploadDocument(
|
||||
file,
|
||||
title,
|
||||
Number(sharedWithId),
|
||||
);
|
||||
|
||||
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' };
|
||||
}
|
||||
}
|
||||
}
|
||||
// @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 { S3Service } from '../s3/s3.service';
|
||||
//import { CreateDocumentDto } from '../dto/create-document.dto';
|
||||
import { UpdateDocumentDto } from '../dto/update-document.dto';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
@ -9,93 +8,44 @@ import * as bcrypt from 'bcrypt';
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private s3Service: S3Service,
|
||||
private readonly prisma: PrismaService,
|
||||
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() {
|
||||
try {
|
||||
const documents = await this.prisma.document.findMany({
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
return this.prisma.document.findMany({
|
||||
include: {
|
||||
uploadedBy: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Retrieved documents with shared users:', documents);
|
||||
return documents;
|
||||
} catch (error) {
|
||||
console.error('Error fetching documents:', error);
|
||||
throw error;
|
||||
}
|
||||
async getAllUsers() {
|
||||
return this.prisma.user.findMany();
|
||||
}
|
||||
|
||||
async createUser(createUserDto: CreateUserDto) {
|
||||
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
|
||||
|
||||
return this.prisma.user.create({
|
||||
data: {
|
||||
...createUserDto,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateDocument(
|
||||
@ -103,76 +53,26 @@ export class AdminService {
|
||||
updateDocumentDto: UpdateDocumentDto,
|
||||
file?: Express.Multer.File,
|
||||
) {
|
||||
const { ...documentData } = updateDocumentDto;
|
||||
let s3Key: string | undefined;
|
||||
|
||||
let s3Key = undefined;
|
||||
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');
|
||||
}
|
||||
|
||||
return this.prisma.document.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...documentData,
|
||||
...updateDocumentDto,
|
||||
...(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) {
|
||||
return this.prisma.document.update({
|
||||
where: { id: documentId },
|
||||
data: {
|
||||
sharedWithId: userId,
|
||||
},
|
||||
include: {
|
||||
sharedWith: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
connect: { id: userId },
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -185,43 +85,42 @@ export class AdminService {
|
||||
});
|
||||
}
|
||||
|
||||
async getDocumentUrl(documentId: number) {
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: documentId },
|
||||
});
|
||||
if (!document) {
|
||||
throw new NotFoundException('Document not found');
|
||||
}
|
||||
return this.s3Service.getFileUrl(document.s3Key);
|
||||
}
|
||||
async getUserWithDocuments(userId: number) {
|
||||
return this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
sharedDocuments: true,
|
||||
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',
|
||||
sharedWith: {
|
||||
connect: { id: sharedWithId }
|
||||
},
|
||||
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 { AuthController } from './auth/auth.controller';
|
||||
import { DocumentsController } from './documents/documents.controller';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -26,7 +27,13 @@ import { DocumentsController } from './documents/documents.controller';
|
||||
// database: 'imk',
|
||||
// synchronize: true,
|
||||
// }),
|
||||
ConfigModule.forRoot(),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '1h' },
|
||||
}),
|
||||
AuthModule,
|
||||
AdminModule,
|
||||
ClientModule,
|
||||
|
||||
@ -3,12 +3,14 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async validateUser(username: string, password: string): Promise<any> {
|
||||
@ -24,9 +26,16 @@ export class AuthService {
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (!userId) {
|
||||
throw new Error('User ID is required');
|
||||
}
|
||||
|
||||
return this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
where: {
|
||||
id: userId, // Make sure userId is properly passed and converted to number if needed
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
isAdmin: true,
|
||||
},
|
||||
isAdmin: true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { jwtConstants } from './constants';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
constructor(configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,11 @@ export class ClientService {
|
||||
async getDocuments(userId: string) {
|
||||
return this.prisma.document.findMany({
|
||||
where: {
|
||||
sharedWithId: Number(userId),
|
||||
sharedWith: {
|
||||
some: {
|
||||
id: Number(userId),
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
sharedWith: {
|
||||
|
||||
@ -1,33 +1,81 @@
|
||||
|
||||
import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
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')
|
||||
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')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
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)
|
||||
async downloadDocument(
|
||||
@Param('key') key: string,
|
||||
@Req() req,
|
||||
@Request() req,
|
||||
@Res() res: Response
|
||||
) {
|
||||
const file = await this.documentsService.downloadDocument(key, req.user.id);
|
||||
|
||||
res.set({
|
||||
'Content-Type': file.contentType,
|
||||
'Content-Length': file.contentLength,
|
||||
'Content-Disposition': `attachment; filename="${file.fileName}"`,
|
||||
});
|
||||
try {
|
||||
this.logger.debug(`Download request for key: ${key}`);
|
||||
|
||||
const decodedKey = decodeURIComponent(key);
|
||||
this.logger.debug(`Decoded key: ${decodedKey}`);
|
||||
|
||||
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 { 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 { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { S3Service } from '../s3/s3.service';
|
||||
import { Document, User } from '@prisma/client';
|
||||
import { Readable } from 'stream';
|
||||
import { Document, User, Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentsService {
|
||||
private readonly logger = new Logger(DocumentsService.name);
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3Service: S3Service,
|
||||
private readonly s3Service: S3Service
|
||||
) {}
|
||||
|
||||
async getClientDocuments(clientId: number) {
|
||||
return this.prisma.document.findMany({
|
||||
where: {
|
||||
sharedWithId: clientId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
async findDocumentByS3Key(s3Key: string) {
|
||||
return this.prisma.document.findFirst({
|
||||
where: { s3Key },
|
||||
include: {
|
||||
uploadedBy: true,
|
||||
sharedWith: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async downloadDocument(s3Key: string, userId: number) {
|
||||
// Verify document access
|
||||
const document = await this.prisma.document.findFirst({
|
||||
where: {
|
||||
s3Key: s3Key,
|
||||
sharedWithId: userId,
|
||||
async verifyDocumentAccess(documentId: number, userId: number): Promise<boolean> {
|
||||
const document = await this.prisma.document.findUnique({
|
||||
where: { id: documentId },
|
||||
include: {
|
||||
uploadedBy: true,
|
||||
sharedWith: {
|
||||
where: {
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundException('Document not found or access denied');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const s3Response = await this.s3Service.getObject(s3Key);
|
||||
|
||||
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');
|
||||
}
|
||||
// User has access if they uploaded the document or it's shared with them
|
||||
return document.uploadedBy.id === userId || document.sharedWith.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 } 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 { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { S3File } from '../interfaces/s3-file.interface';
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
private s3Client: S3Client;
|
||||
private readonly s3Client: S3Client;
|
||||
private readonly logger = new Logger(S3Service.name);
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.s3Client = new S3Client({
|
||||
region: this.configService.get('AWS_REGION'),
|
||||
endpoint: this.configService.get('AWS_ENDPOINT_URL'),
|
||||
region: this.configService.get<string>('AWS_REGION'),
|
||||
credentials: {
|
||||
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
|
||||
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
|
||||
accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
|
||||
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> {
|
||||
try {
|
||||
const key = `${folder}/${Date.now()}-${file.originalname}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
Body: file.buffer,
|
||||
ContentType: file.mimetype,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
this.logger.debug(`File uploaded successfully: ${key}`);
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
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 {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
|
||||
Key: key,
|
||||
});
|
||||
|
||||
await this.s3Client.send(command);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting file from S3: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
const response = await this.s3Client.send(command);
|
||||
|
||||
const chunks = [];
|
||||
for await (const chunk of response.Body as any) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
this.logger.error('Failed to connect to S3', error);
|
||||
return false;
|
||||
const buffer = Buffer.concat(chunks);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
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 { motion } from 'framer-motion';
|
||||
import { format } from 'date-fns';
|
||||
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
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() {
|
||||
const [documents, setDocuments] = useState([]);
|
||||
const [documents, setDocuments] = useState({});
|
||||
const [expandedFolders, setExpandedFolders] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
@ -16,23 +15,29 @@ function Dashboard() {
|
||||
useEffect(() => {
|
||||
const fetchDocuments = async () => {
|
||||
try {
|
||||
if (!user?.id) return;
|
||||
|
||||
const response = await getSharedDocuments(user.id);
|
||||
// Group the documents by company and date
|
||||
const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
|
||||
setDocuments(groupedDocs);
|
||||
} catch (err) {
|
||||
setError('Failed to fetch documents');
|
||||
console.error('Error fetching documents:', err);
|
||||
setError('Failed to fetch documents');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (user) {
|
||||
fetchDocuments();
|
||||
}
|
||||
fetchDocuments();
|
||||
}, [user]);
|
||||
|
||||
const groupDocumentsByCompanyAndDate = (docs) => {
|
||||
if (!Array.isArray(docs)) {
|
||||
console.error('Expected array of documents, received:', docs);
|
||||
return {};
|
||||
}
|
||||
|
||||
return docs.reduce((acc, doc) => {
|
||||
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
|
||||
if (!acc[folderName]) {
|
||||
@ -43,52 +48,16 @@ function Dashboard() {
|
||||
}, {});
|
||||
};
|
||||
|
||||
// const handleDownload = async (s3Key, fileName) => {
|
||||
// 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 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) => {
|
||||
try {
|
||||
await downloadDocument(s3Key);
|
||||
} catch (err) {
|
||||
console.error('Error downloading document:', err);
|
||||
alert('Failed to download document. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (s3Key, fileName) => {
|
||||
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) => {
|
||||
const toggleFolder = (folderName) => {
|
||||
setExpandedFolders(prev => ({
|
||||
...prev,
|
||||
[folderName]: !prev[folderName]
|
||||
@ -97,94 +66,87 @@ const toggleFolder = (folderName) => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||
<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 documents...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center text-red-600 p-4">
|
||||
{error}
|
||||
<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="container mx-auto px-4 py-8">
|
||||
<h1 className="text-2xl font-bold mb-6">Your Laboratory Reports</h1>
|
||||
|
||||
{Object.entries(documents).length === 0 ? (
|
||||
<p className="text-gray-600">No documents have been shared with you yet.</p>
|
||||
) : (
|
||||
<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>
|
||||
<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 mt-20">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Your Documents</h1>
|
||||
<p className="text-gray-400">Access and manage your shared documents</p>
|
||||
</header>
|
||||
|
||||
{expandedFolders[folderName] && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
className="mt-2 space-y-2 pl-8"
|
||||
<div className="grid gap-6">
|
||||
{Object.entries(documents).length > 0 ? (
|
||||
Object.entries(documents).map(([folderName, docs]) => (
|
||||
<div
|
||||
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
|
||||
key={doc.id}
|
||||
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h2 className="text-xl font-semibold">{doc.title}</h2>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
doc.status === 'completed'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{doc.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>Created: {new Date(doc.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={() => handleDownload(doc.s3Key, doc.title)}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
|
||||
<div className="flex items-center space-x-3">
|
||||
<FiFolder className="w-5 h-5 text-blue-400" />
|
||||
<span className="font-medium">{folderName}</span>
|
||||
<span className="text-sm text-gray-400">({docs.length} files)</span>
|
||||
</div>
|
||||
<FiChevronRight
|
||||
className={`w-5 h-5 transition-transform duration-200 ${
|
||||
expandedFolders[folderName] ? 'rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{expandedFolders[folderName] && (
|
||||
<div className="border-t border-gray-700">
|
||||
{docs.map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="px-6 py-3 flex items-center justify-between hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<FiFile className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-200">{doc.title}</span>
|
||||
</div>
|
||||
<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" />
|
||||
Download
|
||||
<FiDownload className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-400">No documents available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { uploadDocument, getAllUsers } from '../../services/api';
|
||||
import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api';
|
||||
|
||||
function DocumentUpload() {
|
||||
const [file, setFile] = useState(null);
|
||||
@ -8,9 +8,11 @@ function DocumentUpload() {
|
||||
const [availableUsers, setAvailableUsers] = useState([]);
|
||||
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
getCurrentUser();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
@ -22,18 +24,22 @@ function DocumentUpload() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const selectedFile = event.target.files[0];
|
||||
setFile(selectedFile);
|
||||
const getCurrentUser = async () => {
|
||||
try {
|
||||
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();
|
||||
|
||||
console.log('Form submission:', { file, title, selectedUsers });
|
||||
|
||||
if (!file || !title || selectedUsers.length === 0) {
|
||||
setErrorMessage('Please provide a title, file, and select at least one user');
|
||||
if (!file || !title || selectedUsers.length === 0 || !currentUser) {
|
||||
setErrorMessage('Please provide all required information');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -43,15 +49,18 @@ const handleSubmit = async (event) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
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 {
|
||||
console.log('Sending request with:', {
|
||||
title,
|
||||
sharedWith: selectedUsers[0],
|
||||
fileSize: file.size
|
||||
});
|
||||
|
||||
const response = await uploadDocument(formData);
|
||||
console.log('Upload response:', response);
|
||||
|
||||
@ -66,6 +75,17 @@ const handleSubmit = async (event) => {
|
||||
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 (
|
||||
<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>
|
||||
@ -103,7 +123,7 @@ const handleSubmit = async (event) => {
|
||||
<select
|
||||
multiple
|
||||
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"
|
||||
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 { useAuth } from '../../hooks/useAuth';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiUser, FiLock } from 'react-icons/fi'; // Import icons
|
||||
|
||||
const Login = () => {
|
||||
const [username, setUsername] = useState(''); // Changed from email to username
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { login } = useAuth();
|
||||
@ -15,8 +96,7 @@ const Login = () => {
|
||||
|
||||
try {
|
||||
const userData = await login(username, password);
|
||||
console.log('Login result:', userData);
|
||||
if (userData?.isAdmin) {
|
||||
if (userData?.isAdmin) {
|
||||
navigate('/admin');
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
@ -27,35 +107,39 @@ const Login = () => {
|
||||
};
|
||||
|
||||
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 className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
|
||||
<div className="w-full max-w-md px-8 py-10 bg-white/5 backdrop-blur-lg rounded-2xl shadow-2xl">
|
||||
<div className="mb-10 text-center">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Welcome Back</h1>
|
||||
<p className="text-gray-400">Please sign in to continue</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{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}
|
||||
</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
|
||||
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"
|
||||
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"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div className="relative">
|
||||
<FiLock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<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"
|
||||
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"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
@ -63,13 +147,17 @@ const Login = () => {
|
||||
</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>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-400">
|
||||
Need help? Contact your administrator
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</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 {
|
||||
const response = await api.get(`/documents/download/${documentId}`, {
|
||||
const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, {
|
||||
responseType: 'blob',
|
||||
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) {
|
||||
console.error('Download error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const createUser = (userData) => {
|
||||
return api.post('/admin/users', {
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
password: userData.password,
|
||||
isAdmin: userData.isAdmin // Add this line
|
||||
isAdmin: userData.isAdmin
|
||||
});
|
||||
};
|
||||
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) => {
|
||||
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, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
console.log('Upload API response:', response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Upload API error:', error);
|
||||
console.error('API Error:', error.response?.data);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@ -67,7 +96,7 @@ export const getUserInfo = () => api.get('/auth/user-info');
|
||||
|
||||
|
||||
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 default api;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user