everything works!

This commit is contained in:
dimitar 2024-11-02 09:26:40 +01:00
parent 01017bb36e
commit a32992b17b
19 changed files with 1554 additions and 727 deletions

View File

@ -5,6 +5,7 @@
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public" DATABASE_URL="postgresql://root:admin@localhost:5432/imk?schema=public"
JWT_SECRET=some-secret
AWS_REGION=EU2 AWS_REGION=EU2
AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6 AWS_ACCESS_KEY_ID=4d2f5655369a02100375e3247d7e1fe6
AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4 AWS_SECRET_ACCESS_KEY=6d4723e14c0d799b89948c24dbe983e4

View File

@ -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;

View File

@ -8,32 +8,36 @@ datasource db {
} }
model Document { model Document {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
title String title String
content String?
s3Key String s3Key String
status String @default("pending") status String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
sharedWithId Int uploadedById Int
sharedWith User @relation("SharedDocuments", fields: [sharedWithId], references: [id]) uploadedBy User @relation("UploadedDocuments", fields: [uploadedById], references: [id])
Notification Notification[]
sharedWith User[] @relation("SharedDocuments")
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
email String @unique email String @unique
name String? name String?
password String password String
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
sharedDocuments Document[] @relation("SharedDocuments") uploadedDocuments Document[] @relation("UploadedDocuments")
notifications Notification[] Notification Notification[]
sharedDocuments Document[] @relation("SharedDocuments")
} }
model Notification { model Notification {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
message String message String
read Boolean @default(false) read Boolean @default(false)
userId Int userId Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id]) documentId Int
document Document @relation(fields: [documentId], references: [id])
user User @relation(fields: [userId], references: [id])
} }

View File

@ -1,3 +1,118 @@
// import {
// Controller,
// Get,
// Post,
// Body,
// Param,
// Put,
// UseInterceptors,
// UploadedFile,
// ParseIntPipe,
// UseGuards,
// } from '@nestjs/common';
// import { FileInterceptor } from '@nestjs/platform-express';
// import { AdminService } from './admin.service';
// //import { CreateDocumentDto } from '../dto/create-document.dto';
// import { UpdateDocumentDto } from '../dto/update-document.dto';
// import { AdminGuard } from '../auth/admin.guard';
// import { JwtAuthGuard } from '../auth/jwt-auth.guard';
// import { CreateUserDto } from '../dto/create-user.dto';
// import { S3Service } from 'src/s3/s3.service';
// import { PrismaService } from 'src/prisma/prisma.service';
// @Controller('admin')
// @UseGuards(JwtAuthGuard, AdminGuard)
// export class AdminController {
// constructor(
// private readonly adminService: AdminService,
// private readonly s3Service: S3Service,
// private readonly prisma: PrismaService,
// ) {}
// @Get('documents')
// getAllDocuments() {
// return this.adminService.getAllDocuments();
// }
// @Put('documents/:id')
// @UseInterceptors(FileInterceptor('file'))
// updateDocument(
// @Param('id', ParseIntPipe) id: number,
// @Body() updateDocumentDto: UpdateDocumentDto,
// @UploadedFile() file?: Express.Multer.File,
// ) {
// return this.adminService.updateDocument(id, updateDocumentDto, file);
// }
// @Get('users')
// getAllUsers() {
// return this.adminService.getAllUsers();
// }
// @Post('test-document')
// async testDocumentCreation() {
// try {
// const document = await this.prisma.document.create({
// data: {
// title: 'Test Document',
// s3Key: 'test-key',
// status: 'completed',
// sharedWithId: 2, // ID of 'pero' user
// },
// });
// return document;
// } catch (error) {
// console.error('Test document creation error:', error);
// throw error;
// }
// }
// @Post('users')
// async createUser(@Body() createUserDto: CreateUserDto) {
// return this.adminService.createUser(createUserDto);
// }
// @Post('documents/:id/share')
// async shareDocument(
// @Param('id') id: string,
// @Body() { userId }: { userId: number },
// ) {
// return this.adminService.shareDocument(+id, userId);
// }
// @Put('documents/:id/status')
// async updateDocumentStatus(
// @Param('id') id: string,
// @Body() { status }: { status: string },
// ) {
// return this.adminService.updateDocumentStatus(+id, status);
// }
// @Post('documents')
// @UseInterceptors(FileInterceptor('file'))
// async uploadDocument(
// @UploadedFile() file: Express.Multer.File,
// @Body('title') title: string,
// @Body('sharedWithId') sharedWithId: number,
// @Body('uploadedById') uploadedById: number
// ) {
// const document = await this.adminService.uploadDocument(
// file,
// title,
// sharedWithId,
// uploadedById // Add this missing parameter
// );
// return document;
// }
// @Get('test-s3-connection')
// async testS3Connection() {
// const isConnected = await this.s3Service.testConnection();
// if (isConnected) {
// return { message: 'Successfully connected to S3' };
// } else {
// return { message: 'Failed to connect to S3' };
// }
// }
// }
import { import {
Controller, Controller,
Get, Get,
@ -9,10 +124,10 @@ import {
UploadedFile, UploadedFile,
ParseIntPipe, ParseIntPipe,
UseGuards, UseGuards,
BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express'; import { FileInterceptor } from '@nestjs/platform-express';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
//import { CreateDocumentDto } from '../dto/create-document.dto';
import { UpdateDocumentDto } from '../dto/update-document.dto'; import { UpdateDocumentDto } from '../dto/update-document.dto';
import { AdminGuard } from '../auth/admin.guard'; import { AdminGuard } from '../auth/admin.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@ -44,27 +159,60 @@ export class AdminController {
return this.adminService.updateDocument(id, updateDocumentDto, file); return this.adminService.updateDocument(id, updateDocumentDto, file);
} }
@Post('documents')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body('title') title: string,
@Body('sharedWithId') sharedWithId: string, // Accept as string first
@Body('uploadedById') uploadedById: string // Accept as string first
) {
if (!sharedWithId || !uploadedById) {
throw new BadRequestException('sharedWithId and uploadedById are required');
}
// Parse the string values to numbers
const parsedSharedWithId = parseInt(sharedWithId, 10);
const parsedUploadedById = parseInt(uploadedById, 10);
// Validate that the parsing was successful
if (isNaN(parsedSharedWithId) || isNaN(parsedUploadedById)) {
throw new BadRequestException('sharedWithId and uploadedById must be valid numbers');
}
const document = await this.adminService.uploadDocument(
file,
title,
parsedSharedWithId,
parsedUploadedById
);
return document;
}
@Get('users') @Get('users')
getAllUsers() { getAllUsers() {
return this.adminService.getAllUsers(); return this.adminService.getAllUsers();
} }
@Post('test-document')
async testDocumentCreation() { // @Post('test-document')
try { // async testDocumentCreation() {
const document = await this.prisma.document.create({ // try {
data: { // const document = await this.prisma.document.create({
title: 'Test Document', // data: {
s3Key: 'test-key', // title: 'Test Document',
status: 'completed', // s3Key: 'test-key',
sharedWithId: 2, // ID of 'pero' user // status: 'completed',
}, // sharedWith: {
}); // connect: { id: 2 } // ID of 'pero' user
return document; // }
} catch (error) { // },
console.error('Test document creation error:', error); // });
throw error; // return document;
} // } catch (error) {
} // console.error('Test document creation error:', error);
// throw error;
// }
// }
@Post('users') @Post('users')
async createUser(@Body() createUserDto: CreateUserDto) { async createUser(@Body() createUserDto: CreateUserDto) {
@ -87,41 +235,14 @@ export class AdminController {
return this.adminService.updateDocumentStatus(+id, status); return this.adminService.updateDocumentStatus(+id, status);
} }
@Post('documents')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body('title') title: string,
@Body('sharedWith') sharedWithId: string,
) {
console.log('Received upload request:', {
fileExists: !!file,
fileSize: file?.size,
title,
sharedWithId,
});
try { // @Get('test-s3-connection')
const document = await this.adminService.uploadDocument( // async testS3Connection() {
file, // const isConnected = await this.s3Service.testConnection();
title, // if (isConnected) {
Number(sharedWithId), // return { message: 'Successfully connected to S3' };
); // } else {
// return { message: 'Failed to connect to S3' };
console.log('Document created:', document); // }
return document; // }
} catch (error) { }
console.error('Upload error:', error);
throw error;
}
}
@Get('test-s3-connection')
async testS3Connection() {
const isConnected = await this.s3Service.testConnection();
if (isConnected) {
return { message: 'Successfully connected to S3' };
} else {
return { message: 'Failed to connect to S3' };
}
}
}

View File

@ -1,7 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service'; import { S3Service } from '../s3/s3.service';
//import { CreateDocumentDto } from '../dto/create-document.dto';
import { UpdateDocumentDto } from '../dto/update-document.dto'; import { UpdateDocumentDto } from '../dto/update-document.dto';
import { CreateUserDto } from '../dto/create-user.dto'; import { CreateUserDto } from '../dto/create-user.dto';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@ -9,93 +8,44 @@ import * as bcrypt from 'bcrypt';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
constructor( constructor(
private prisma: PrismaService, private readonly prisma: PrismaService,
private s3Service: S3Service, private readonly s3Service: S3Service,
) {} ) {}
async uploadDocument(
file: Express.Multer.File,
title: string,
sharedWithId: number,
) {
try {
// First verify the user exists
console.log('Verifying user:', sharedWithId);
const user = await this.prisma.user.findUnique({
where: { id: sharedWithId },
});
if (!user) {
throw new NotFoundException(`User with ID ${sharedWithId} not found`);
}
console.log('Found user:', user);
// Upload file to S3
console.log('Uploading file to S3...');
const s3Key = await this.s3Service.uploadFile(file, 'documents');
console.log('File uploaded to S3:', s3Key);
console.log('Creating document record with data:', {
title,
s3Key,
sharedWithId,
});
const document = await this.prisma.document.create({
data: {
title,
s3Key,
status: 'completed',
sharedWithId,
},
include: {
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
console.log('Created document:', document);
return document;
} catch (error) {
console.error('Error in uploadDocument:', error);
if (error.code === 'P2002') {
console.error('Unique constraint violation');
}
if (error.code === 'P2003') {
console.error('Foreign key constraint violation');
}
throw error;
}
}
async getAllDocuments() { async getAllDocuments() {
try { return this.prisma.document.findMany({
const documents = await this.prisma.document.findMany({ include: {
include: { uploadedBy: {
sharedWith: { select: {
select: { id: true,
id: true, name: true,
name: true, email: true,
email: true,
},
}, },
}, },
orderBy: { sharedWith: {
createdAt: 'desc', select: {
id: true,
name: true,
email: true,
},
}, },
}); },
});
}
console.log('Retrieved documents with shared users:', documents); async getAllUsers() {
return documents; return this.prisma.user.findMany();
} catch (error) { }
console.error('Error fetching documents:', error);
throw error; async createUser(createUserDto: CreateUserDto) {
} const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
return this.prisma.user.create({
data: {
...createUserDto,
password: hashedPassword,
},
});
} }
async updateDocument( async updateDocument(
@ -103,76 +53,26 @@ export class AdminService {
updateDocumentDto: UpdateDocumentDto, updateDocumentDto: UpdateDocumentDto,
file?: Express.Multer.File, file?: Express.Multer.File,
) { ) {
const { ...documentData } = updateDocumentDto; let s3Key = undefined;
let s3Key: string | undefined;
if (file) { if (file) {
const oldDocument = await this.prisma.document.findUnique({
where: { id },
select: { s3Key: true },
});
if (oldDocument?.s3Key) {
await this.s3Service.deleteFile(oldDocument.s3Key);
}
s3Key = await this.s3Service.uploadFile(file, 'documents'); s3Key = await this.s3Service.uploadFile(file, 'documents');
} }
return this.prisma.document.update({ return this.prisma.document.update({
where: { id }, where: { id },
data: { data: {
...documentData, ...updateDocumentDto,
...(s3Key && { s3Key }), ...(s3Key && { s3Key }),
}, },
}); });
} }
// async createUser(createUserDto: CreateUserDto) {
// const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
// return this.prisma.user.create({
// data: {
// ...createUserDto,
// password: hashedPassword,
// },
// });
// }
async createUser(userData: {
name: string;
email: string;
password: string;
isAdmin: boolean;
}) {
const hashedPassword = await bcrypt.hash(userData.password, 10);
return this.prisma.user.create({
data: {
name: userData.name,
email: userData.email,
password: hashedPassword,
isAdmin: userData.isAdmin,
},
select: {
id: true,
name: true,
email: true,
isAdmin: true,
},
});
}
async shareDocument(documentId: number, userId: number) { async shareDocument(documentId: number, userId: number) {
return this.prisma.document.update({ return this.prisma.document.update({
where: { id: documentId }, where: { id: documentId },
data: { data: {
sharedWithId: userId,
},
include: {
sharedWith: { sharedWith: {
select: { connect: { id: userId },
id: true,
name: true,
email: true,
},
}, },
}, },
}); });
@ -185,43 +85,42 @@ export class AdminService {
}); });
} }
async getDocumentUrl(documentId: number) { async uploadDocument(
const document = await this.prisma.document.findUnique({ file: Express.Multer.File,
where: { id: documentId }, title: string,
}); sharedWithId: number,
if (!document) { uploadedById: number
throw new NotFoundException('Document not found'); ) {
} const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.s3Service.getFileUrl(document.s3Key);
} return this.prisma.document.create({
async getUserWithDocuments(userId: number) { data: {
return this.prisma.user.findUnique({ title,
where: { id: userId }, s3Key,
select: { status: 'pending',
id: true, sharedWith: {
name: true, connect: { id: sharedWithId }
email: true, },
sharedDocuments: true, uploadedBy: {
connect: { id: uploadedById }
}
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true,
},
},
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
}, },
}); });
} }
}
async getAllUsers() {
try {
const users = await this.prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
isAdmin: true,
sharedDocuments: true,
},
});
console.log('All users:', users);
return users;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
}

View File

@ -14,6 +14,7 @@ import { PrismaModule } from './prisma/prisma.module';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { AuthController } from './auth/auth.controller'; import { AuthController } from './auth/auth.controller';
import { DocumentsController } from './documents/documents.controller'; import { DocumentsController } from './documents/documents.controller';
import { JwtModule } from '@nestjs/jwt';
@Module({ @Module({
imports: [ imports: [
@ -26,7 +27,13 @@ import { DocumentsController } from './documents/documents.controller';
// database: 'imk', // database: 'imk',
// synchronize: true, // synchronize: true,
// }), // }),
ConfigModule.forRoot(), ConfigModule.forRoot({
isGlobal: true,
}),
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
AuthModule, AuthModule,
AdminModule, AdminModule,
ClientModule, ClientModule,

View File

@ -3,12 +3,14 @@ import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import { CreateUserDto } from '../dto/create-user.dto'; import { CreateUserDto } from '../dto/create-user.dto';
import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private jwtService: JwtService, private jwtService: JwtService,
private configService: ConfigService,
) {} ) {}
async validateUser(username: string, password: string): Promise<any> { async validateUser(username: string, password: string): Promise<any> {
@ -24,9 +26,16 @@ export class AuthService {
} }
async login(user: any) { async login(user: any) {
const payload = { username: user.email, sub: user.id }; // const payload = { username: user.email, sub: user.id };
// return {
// access_token: this.jwtService.sign(payload),
// };
const payload = { username: user.username, sub: user.id };
console.log(payload);
return { return {
access_token: this.jwtService.sign(payload), access_token: this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_SECRET'),
}),
}; };
} }
@ -59,15 +68,32 @@ export class AuthService {
return result; return result;
} }
// async getUserInfo(userId: number) {
// return this.prisma.user.findUnique({
// where: { id: userId },
// select: {
// id: true,
// name: true,
// email: true,
// isAdmin: true,
// },
// });
// }
async getUserInfo(userId: number) { async getUserInfo(userId: number) {
if (!userId) {
throw new Error('User ID is required');
}
return this.prisma.user.findUnique({ return this.prisma.user.findUnique({
where: { id: userId }, where: {
id: userId, // Make sure userId is properly passed and converted to number if needed
},
select: { select: {
id: true, id: true,
name: true, name: true,
email: true, email: true,
isAdmin: true, isAdmin: true
}, }
}); });
} }
} }

View File

@ -1,15 +1,15 @@
import { ExtractJwt, Strategy } from 'passport-jwt'; import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants'; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() { constructor(configService: ConfigService) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: jwtConstants.secret, secretOrKey: configService.get('JWT_SECRET'),
}); });
} }

View File

@ -15,7 +15,11 @@ export class ClientService {
async getDocuments(userId: string) { async getDocuments(userId: string) {
return this.prisma.document.findMany({ return this.prisma.document.findMany({
where: { where: {
sharedWithId: Number(userId), sharedWith: {
some: {
id: Number(userId),
},
},
}, },
include: { include: {
sharedWith: { sharedWith: {

View File

@ -1,33 +1,81 @@
import { Controller, Get, Param, Req, Res, UseGuards, Logger, Request } from '@nestjs/common';
import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express'; import { Response } from 'express';
import { DocumentsService } from './documents.service'; import { DocumentsService } from './documents.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { S3Service } from '../s3/s3.service';
interface S3File {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}
@Controller('documents') @Controller('documents')
export class DocumentsController { export class DocumentsController {
constructor(private readonly documentsService: DocumentsService) {} private readonly logger = new Logger(DocumentsController.name);
constructor(
private readonly documentsService: DocumentsService,
private readonly s3Service: S3Service
) {}
@Get('shared/:userId') @Get('shared/:userId')
@UseGuards(JwtAuthGuard)
async getSharedDocuments(@Param('userId') userId: string) { async getSharedDocuments(@Param('userId') userId: string) {
return this.documentsService.getClientDocuments(parseInt(userId)); return this.documentsService.getClientDocuments(Number(userId));
} }
@Get('shared/download/:key') @Get('download/:key')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
async downloadDocument( async downloadDocument(
@Param('key') key: string, @Param('key') key: string,
@Req() req, @Request() req,
@Res() res: Response @Res() res: Response
) { ) {
const file = await this.documentsService.downloadDocument(key, req.user.id); try {
this.logger.debug(`Download request for key: ${key}`);
res.set({
'Content-Type': file.contentType, const decodedKey = decodeURIComponent(key);
'Content-Length': file.contentLength, this.logger.debug(`Decoded key: ${decodedKey}`);
'Content-Disposition': `attachment; filename="${file.fileName}"`,
});
res.send(file.buffer); // Get document from database first to verify access
const document = await this.documentsService.findDocumentByS3Key(decodedKey);
if (!document) {
return res.status(404).json({ message: 'Document not found' });
}
// Verify user has access to this document
const hasAccess = await this.documentsService.verifyDocumentAccess(
document.id,
req.user.id
);
if (!hasAccess) {
return res.status(403).json({ message: 'Access denied' });
}
// Get the file from S3
const file = await this.s3Service.getFile(decodedKey);
if (!file || !file.buffer) {
return res.status(404).json({ message: 'File not found in storage' });
}
res.set({
'Content-Type': file.contentType || 'application/octet-stream',
'Content-Length': file.contentLength,
'Content-Disposition': `attachment; filename="${encodeURIComponent(file.fileName)}"`,
});
return res.send(file.buffer);
} catch (error) {
this.logger.error('Download error:', error);
return res.status(500).json({
message: 'Failed to download file',
error: error.message
});
}
} }
} }

View File

@ -1,236 +1,120 @@
// // import { Injectable } from '@nestjs/common'; import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common';
// // import { PrismaService } from '../prisma/prisma.service';
// // //import { Document } from '@prisma/client';
// // @Injectable()
// // export class DocumentsService {
// // downloadDocument(key: string, id: any) {
// // throw new Error('Method not implemented.');
// // }
// // constructor(private readonly prisma: PrismaService) {}
// // async getClientDocuments(clientId: number) {
// // // return this.prisma.document.findMany({
// // // where: {
// // // sharedWithId: clientId,
// // // },
// // // include: {
// // // sharedWith: {
// // // select: {
// // // id: true,
// // // name: true,
// // // email: true,
// // // },
// // // },
// // // },
// // // });
// // return this.prisma.document.findMany({
// // where: {
// // sharedWithId: clientId,
// // },
// // orderBy: {
// // createdAt: 'desc',
// // },
// // });
// // }
// // }
// import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
// import { PrismaService } from '../prisma/prisma.service';
// import { S3Service } from '../s3/s3.service';
// import { Document, User } from '@prisma/client';
// @Injectable()
// export class DocumentsService {
// constructor(
// private prisma: PrismaService,
// private s3Service: S3Service,
// ) {}
// async findAllByClient(userId: number): Promise<Document[]> {
// const documents = await this.prisma.document.findMany({
// where: {
// sharedWithId: userId,
// },
// include: {
// sharedWith: {
// select: {
// id: true,
// name: true,
// email: true,
// },
// },
// },
// orderBy: {
// createdAt: 'desc',
// },
// });
// return documents;
// }
// async getClientDocuments(clientId: number) {
// // return this.prisma.document.findMany({
// // where: {
// // sharedWithId: clientId,
// // },
// // include: {
// // sharedWith: {
// // select: {
// // id: true,
// // name: true,
// // email: true,
// // },
// // },
// // },
// // });
// return this.prisma.document.findMany({
// where: {
// sharedWithId: clientId,
// },
// orderBy: {
// createdAt: 'desc',
// },
// });
// }
// async downloadDocument(s3Key: string, userId: number): Promise<any> {
// // Verify document exists and user has access
// const document = await this.prisma.document.findFirst({
// where: {
// s3Key: s3Key,
// sharedWithId: userId,
// },
// });
// if (!document) {
// throw new NotFoundException('Document not found or access denied');
// }
// try {
// // Get document stream from S3
// const fileStream = await this.s3Service.getObject(s3Key).createReadStream();
// return fileStream;
// } catch (error) {
// console.error('Error downloading document:', error);
// throw new NotFoundException('Document file not found in storage');
// }
// }
// async getAllDocuments(): Promise<Document[]> {
// return this.prisma.document.findMany({
// include: {
// sharedWith: {
// select: {
// id: true,
// name: true,
// email: true,
// },
// },
// },
// orderBy: {
// createdAt: 'desc',
// },
// });
// }
// async shareDocument(documentId: number, userId: number): Promise<Document> {
// const document = await this.prisma.document.update({
// where: { id: documentId },
// data: {
// sharedWithId: userId,
// },
// include: {
// sharedWith: {
// select: {
// id: true,
// name: true,
// email: true,
// },
// },
// },
// });
// return document;
// }
// async updateStatus(documentId: number, status: string): Promise<Document> {
// return this.prisma.document.update({
// where: { id: documentId },
// data: { status },
// include: {
// sharedWith: {
// select: {
// id: true,
// name: true,
// email: true,
// },
// },
// },
// });
// }
// }
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { S3Service } from '../s3/s3.service'; import { S3Service } from '../s3/s3.service';
import { Document, User } from '@prisma/client'; import { Document, User, Prisma } from '@prisma/client';
import { Readable } from 'stream';
@Injectable() @Injectable()
export class DocumentsService { export class DocumentsService {
private readonly logger = new Logger(DocumentsService.name);
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly s3Service: S3Service, private readonly s3Service: S3Service
) {} ) {}
async getClientDocuments(clientId: number) { async findDocumentByS3Key(s3Key: string) {
return this.prisma.document.findMany({ return this.prisma.document.findFirst({
where: { where: { s3Key },
sharedWithId: clientId, include: {
}, uploadedBy: true,
orderBy: { sharedWith: true,
createdAt: 'desc',
}, },
}); });
} }
async downloadDocument(s3Key: string, userId: number) { async verifyDocumentAccess(documentId: number, userId: number): Promise<boolean> {
// Verify document access const document = await this.prisma.document.findUnique({
const document = await this.prisma.document.findFirst({ where: { id: documentId },
where: { include: {
s3Key: s3Key, uploadedBy: true,
sharedWithId: userId, sharedWith: {
where: {
id: userId
}
}
}, },
}); });
if (!document) { if (!document) {
throw new NotFoundException('Document not found or access denied'); return false;
} }
try { // User has access if they uploaded the document or it's shared with them
const s3Response = await this.s3Service.getObject(s3Key); return document.uploadedBy.id === userId || document.sharedWith.length > 0;
if (!s3Response.Body) {
throw new Error('No file content received from S3');
}
// Convert the response body to a buffer
const streamBody = s3Response.Body as Readable;
const chunks: Buffer[] = [];
for await (const chunk of streamBody) {
chunks.push(Buffer.from(chunk));
}
const fileBuffer = Buffer.concat(chunks);
return {
buffer: fileBuffer,
contentType: s3Response.ContentType || 'application/octet-stream',
contentLength: s3Response.ContentLength,
fileName: s3Key.split('/').pop() || 'download'
};
} catch (error) {
console.error('Error downloading from S3:', error);
throw new NotFoundException('Failed to download file');
}
} }
}
async getClientDocuments(clientId: number) {
return this.prisma.document.findMany({
where: {
OR: [
{ uploadedById: clientId },
{
sharedWith: {
some: {
id: clientId
}
}
}
]
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true
}
},
sharedWith: {
select: {
id: true,
name: true,
email: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
}
async uploadDocument(
file: Express.Multer.File,
title: string,
sharedWithId: number,
uploadedById: number
) {
const s3Key = await this.s3Service.uploadFile(file, 'documents');
return this.prisma.document.create({
data: {
title,
s3Key,
status: 'pending',
uploadedBy: {
connect: { id: uploadedById }
},
sharedWith: {
connect: { id: sharedWithId }
}
},
include: {
uploadedBy: {
select: {
id: true,
name: true,
email: true,
},
},
sharedWith: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
}
}

View File

@ -0,0 +1,6 @@
export interface FileResponse {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}

View File

@ -0,0 +1,6 @@
export interface S3File {
buffer: Buffer;
contentType: string;
contentLength: number;
fileName: string;
}

View File

@ -1,101 +1,70 @@
import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import {
S3Client,
DeleteObjectCommand,
GetObjectCommand,
ListObjectsCommand,
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Upload } from '@aws-sdk/lib-storage';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { S3File } from '../interfaces/s3-file.interface';
@Injectable() @Injectable()
export class S3Service { export class S3Service {
private s3Client: S3Client; private readonly s3Client: S3Client;
private readonly logger = new Logger(S3Service.name); private readonly logger = new Logger(S3Service.name);
constructor(private configService: ConfigService) { constructor(private readonly configService: ConfigService) {
this.s3Client = new S3Client({ this.s3Client = new S3Client({
region: this.configService.get('AWS_REGION'), region: this.configService.get<string>('AWS_REGION'),
endpoint: this.configService.get('AWS_ENDPOINT_URL'),
credentials: { credentials: {
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'), accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'), secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
}, },
forcePathStyle: true, // Needed for non-AWS S3 compatible services endpoint: this.configService.get<string>('AWS_ENDPOINT_URL'),
forcePathStyle: true, // Required for Contabo Object Storage
}); });
} }
async getObject(key: string) {
try {
const command = new GetObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Key: key,
});
const response = await this.s3Client.send(command);
return response;
} catch (error) {
this.logger.error(`Error getting object from S3: ${error.message}`);
throw error;
}
}
async uploadFile(file: Express.Multer.File, folder: string): Promise<string> { async uploadFile(file: Express.Multer.File, folder: string): Promise<string> {
try { try {
const key = `${folder}/${Date.now()}-${file.originalname}`; const key = `${folder}/${Date.now()}-${file.originalname}`;
const command = new PutObjectCommand({ const command = new PutObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
Key: key, Key: key,
Body: file.buffer, Body: file.buffer,
ContentType: file.mimetype, ContentType: file.mimetype,
}); });
await this.s3Client.send(command); await this.s3Client.send(command);
this.logger.debug(`File uploaded successfully: ${key}`);
return key; return key;
} catch (error) { } catch (error) {
this.logger.error(`Error uploading file to S3: ${error.message}`); this.logger.error(`Error uploading file to S3: ${error.message}`);
throw error; throw new InternalServerErrorException('Failed to upload file to storage');
} }
} }
async deleteFile(key: string): Promise<void> { async getFile(key: string): Promise<S3File> {
try { try {
const command = new DeleteObjectCommand({ const command = new GetObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'), Bucket: this.configService.get<string>('AWS_S3_BUCKET_NAME'),
Key: key, Key: key,
}); });
await this.s3Client.send(command); const response = await this.s3Client.send(command);
} catch (error) {
this.logger.error(`Error deleting file from S3: ${error.message}`); const chunks = [];
throw error; for await (const chunk of response.Body as any) {
} chunks.push(chunk);
}
async getFileUrl(key: string): Promise<string> {
const command = new GetObjectCommand({
Bucket: this.configService.get('AWS_S3_BUCKET_NAME'),
Key: key,
});
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); // URL expires in 1 hour
return url;
}
async testConnection(): Promise<boolean> {
try {
await this.getObject('test-connection');
return true;
} catch (error) {
if (error.name === 'NoSuchKey') {
// This is expected as we're just testing connection
return true;
} }
this.logger.error('Failed to connect to S3', error); const buffer = Buffer.concat(chunks);
return false;
return {
buffer,
contentType: response.ContentType || 'application/octet-stream',
contentLength: response.ContentLength || buffer.length,
fileName: key.split('/').pop() || 'download',
};
} catch (error) {
this.logger.error(`Error getting file from S3: ${error.message}`);
throw new InternalServerErrorException('Failed to retrieve file from storage');
} }
} }
} }

View File

@ -271,5 +271,714 @@ function AdminPanel() {
</div> </div>
); );
} }
export default AdminPanel;
export default AdminPanel; // import { useState, useEffect } from 'react';
// import { useAuth } from '../../hooks/useAuth';
// import { format } from 'date-fns';
// import { FiUpload, FiUsers, FiFile, FiTrash2, FiShare2, FiLoader, FiSearch, FiChevronRight } from 'react-icons/fi';
// import api from '../../services/api';
// function AdminPanel() {
// const [documents, setDocuments] = useState([]);
// const [users, setUsers] = useState([]);
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState('');
// const [selectedFile, setSelectedFile] = useState(null);
// const [uploadTitle, setUploadTitle] = useState('');
// const [selectedUser, setSelectedUser] = useState('');
// const [searchTerm, setSearchTerm] = useState('');
// const [expandedUsers, setExpandedUsers] = useState({});
// useEffect(() => {
// fetchData();
// }, []);
// const fetchData = async () => {
// try {
// setLoading(true);
// const [documentsRes, usersRes] = await Promise.all([
// api.get('/admin/documents'),
// api.get('/admin/users')
// ]);
// console.log('Documents response:', documentsRes.data);
// console.log('Users response:', usersRes.data);
// // Group documents by user
// const groupedDocs = groupDocumentsByUser(documentsRes.data || []);
// setDocuments(groupedDocs);
// setUsers(usersRes.data || []);
// } catch (err) {
// setError('Failed to fetch data');
// console.error('Error:', err);
// } finally {
// setLoading(false);
// }
// };
// const groupDocumentsByUser = (docs) => {
// return docs.reduce((acc, doc) => {
// const userName = doc.sharedWith?.name || 'Unassigned';
// if (!acc[userName]) {
// acc[userName] = [];
// }
// acc[userName].push(doc);
// return acc;
// }, {});
// };
// const toggleUser = (userName) => {
// setExpandedUsers(prev => ({
// ...prev,
// [userName]: !prev[userName]
// }));
// };
// const handleFileUpload = async (e) => {
// e.preventDefault();
// if (!selectedFile || !uploadTitle || !selectedUser) {
// alert('Please fill in all fields');
// return;
// }
// const formData = new FormData();
// formData.append('file', selectedFile);
// formData.append('title', uploadTitle);
// formData.append('sharedWith', selectedUser);
// try {
// await api.post('/admin/documents', formData, {
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// });
// setSelectedFile(null);
// setUploadTitle('');
// setSelectedUser('');
// fetchData();
// alert('Document uploaded successfully');
// } catch (err) {
// console.error('Upload error:', err);
// alert('Failed to upload document');
// }
// };
// const handleDelete = async (documentId) => {
// if (!window.confirm('Are you sure you want to delete this document?')) {
// return;
// }
// try {
// await api.delete(`/admin/documents/${documentId}`);
// fetchData();
// alert('Document deleted successfully');
// } catch (err) {
// console.error('Delete error:', err);
// alert('Failed to delete document');
// }
// };
// const handleShare = async (documentId) => {
// const userId = prompt('Enter user ID to share with:');
// if (!userId) return;
// try {
// await api.post(`/admin/documents/${documentId}/share`, { userId });
// fetchData();
// alert('Document shared successfully');
// } catch (err) {
// console.error('Share error:', err);
// alert('Failed to share document');
// }
// };
// if (loading) {
// return (
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
// <div className="flex items-center space-x-3 text-blue-500">
// <FiLoader className="w-6 h-6 animate-spin" />
// <span className="text-lg font-medium">Loading admin panel...</span>
// </div>
// </div>
// );
// }
// if (error) {
// return (
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
// <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
// {error}
// </div>
// </div>
// );
// }
// return (
// <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
// <div className="max-w-7xl mx-auto">
// <header className="mb-8">
// <h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
// <p className="text-gray-400">Manage documents and users</p>
// </header>
// {/* Upload Section - remains the same */}
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 mb-8">
// {/* ... upload form content remains the same ... */}
// </div>
// {/* Search Bar */}
// <div className="relative mb-6">
// <FiSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
// <input
// type="text"
// placeholder="Search documents..."
// value={searchTerm}
// onChange={(e) => setSearchTerm(e.target.value)}
// className="w-full pl-10 pr-4 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400"
// />
// </div>
// {/* Documents List - Grouped by User */}
// <div className="space-y-4">
// {Object.entries(documents).map(([userName, userDocs]) => {
// const filteredDocs = userDocs.filter(doc =>
// doc.title.toLowerCase().includes(searchTerm.toLowerCase())
// );
// if (filteredDocs.length === 0) return null;
// return (
// <div
// key={userName}
// className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden"
// >
// <button
// onClick={() => toggleUser(userName)}
// className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
// >
// <div className="flex items-center space-x-3">
// <FiUsers className="w-5 h-5 text-blue-400" />
// <span className="font-medium">{userName}</span>
// <span className="text-sm text-gray-400">({filteredDocs.length} documents)</span>
// </div>
// <FiChevronRight
// className={`w-5 h-5 transition-transform duration-200 ${
// expandedUsers[userName] ? 'rotate-90' : ''
// }`}
// />
// </button>
// {expandedUsers[userName] && (
// <div className="border-t border-gray-700 divide-y divide-gray-700">
// {filteredDocs.map(doc => (
// <div
// key={doc.id}
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
// >
// <div className="flex items-center space-x-4">
// <FiFile className="w-5 h-5 text-gray-400" />
// <div>
// <h3 className="text-white font-medium">{doc.title}</h3>
// <p className="text-sm text-gray-400">
// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')}
// </p>
// </div>
// </div>
// <div className="flex items-center space-x-2">
// <button
// onClick={() => handleShare(doc.id)}
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
// title="Share document"
// >
// <FiShare2 className="w-4 h-4" />
// </button>
// <button
// onClick={() => handleDelete(doc.id)}
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
// title="Delete document"
// >
// <FiTrash2 className="w-4 h-4" />
// </button>
// </div>
// </div>
// ))}
// </div>
// )}
// </div>
// );
// })}
// {Object.keys(documents).length === 0 && (
// <div className="text-center py-12 text-gray-400">
// No documents available
// </div>
// )}
// </div>
// </div>
// </div>
// )
// }
// export default AdminPanel;
// import { useState, useEffect } from 'react';
// import { useAuth } from '../../hooks/useAuth';
// import { format } from 'date-fns';
// import {
// FiUpload,
// FiDownload,
// FiUsers,
// FiFile,
// FiTrash2,
// FiShare2,
// FiLoader,
// FiSearch,
// FiChevronRight,
// FiUserPlus,
// FiEdit2,
// FiKey
// } from 'react-icons/fi';
// import api from '../../services/api';
// function AdminPanel() {
// const [documents, setDocuments] = useState({});
// const [users, setUsers] = useState([]);
// const [expandedUsers, setExpandedUsers] = useState({});
// const [loading, setLoading] = useState(true);
// const [error, setError] = useState('');
// const [selectedFile, setSelectedFile] = useState(null);
// const [uploadTitle, setUploadTitle] = useState('');
// const [selectedUser, setSelectedUser] = useState('');
// const [searchTerm, setSearchTerm] = useState('');
// const [activeTab, setActiveTab] = useState('documents'); // 'documents' or 'users'
// const [newUser, setNewUser] = useState({ username: '', password: '', name: '', isAdmin: false });
// useEffect(() => {
// fetchData();
// }, []);
// const fetchData = async () => {
// try {
// setLoading(true);
// const [documentsRes, usersRes] = await Promise.all([
// api.get('/admin/documents'),
// api.get('/admin/users')
// ]);
// const groupedDocs = groupDocumentsByUser(documentsRes.data || []);
// const initialExpandedState = Object.keys(groupedDocs).reduce((acc, userName) => {
// acc[userName] = false;
// return acc;
// }, {});
// setDocuments(groupedDocs);
// setUsers(usersRes.data || []);
// setExpandedUsers(initialExpandedState);
// } catch (err) {
// setError('Failed to fetch data');
// console.error('Error:', err);
// } finally {
// setLoading(false);
// }
// };
// const groupDocumentsByUser = (docs) => {
// return docs.reduce((acc, doc) => {
// const userName = doc.sharedWith?.name || 'Unassigned';
// if (!acc[userName]) {
// acc[userName] = [];
// }
// acc[userName].push(doc);
// return acc;
// }, {});
// };
// const handleAddUser = async (e) => {
// e.preventDefault();
// try {
// await api.post('/admin/users', newUser);
// setNewUser({ username: '', password: '', name: '', isAdmin: false });
// fetchData();
// alert('User added successfully');
// } catch (err) {
// console.error('Error adding user:', err);
// alert('Failed to add user');
// }
// };
// const handleDeleteUser = async (userId) => {
// if (!window.confirm('Are you sure you want to delete this user?')) return;
// try {
// await api.delete(`/admin/users/${userId}`);
// fetchData();
// alert('User deleted successfully');
// } catch (err) {
// console.error('Error deleting user:', err);
// alert('Failed to delete user');
// }
// };
// const handleResetPassword = async (userId) => {
// const newPassword = prompt('Enter new password:');
// if (!newPassword) return;
// try {
// await api.post(`/admin/users/${userId}/reset-password`, { password: newPassword });
// alert('Password reset successfully');
// } catch (err) {
// console.error('Error resetting password:', err);
// alert('Failed to reset password');
// }
// };
// const handleFileUpload = async (e) => {
// e.preventDefault();
// if (!selectedFile || !uploadTitle || !selectedUser) {
// alert('Please fill in all fields');
// return;
// }
// const formData = new FormData();
// formData.append('file', selectedFile);
// formData.append('title', uploadTitle);
// formData.append('sharedWith', selectedUser);
// try {
// await api.post('/admin/documents', formData, {
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// });
// setSelectedFile(null);
// setUploadTitle('');
// setSelectedUser('');
// fetchData();
// alert('Document uploaded successfully');
// } catch (err) {
// console.error('Upload error:', err);
// alert('Failed to upload document');
// }
// };
// const toggleUser = (userName) => {
// console.log('Toggling user:', userName); // Debug log
// setExpandedUsers(prev => ({
// ...prev,
// [userName]: !prev[userName]
// }));
// };
// const handleDownload = async (s3Key, fileName) => {
// try {
// const token = localStorage.getItem('token');
// // Remove 'documents/' from the s3Key as it's already in the path
// const cleanS3Key = s3Key.replace('documents/', '');
// const response = await fetch(`http://localhost:3000/admin/documents/download/${cleanS3Key}`, {
// headers: {
// 'Authorization': `Bearer ${token}`
// }
// });
// if (!response.ok) {
// throw new Error('Download failed');
// }
// const blob = await response.blob();
// const url = window.URL.createObjectURL(blob);
// const link = document.createElement('a');
// link.href = url;
// link.download = fileName;
// document.body.appendChild(link);
// link.click();
// document.body.removeChild(link);
// window.URL.revokeObjectURL(url);
// } catch (err) {
// console.error('Error downloading document:', err);
// alert('Failed to download document. Please try again.');
// }
// };
// if (loading) {
// return (
// <div className="min-h-screen flex items-center justify-center bg-gray-900">
// <div className="flex items-center space-x-3 text-blue-500">
// <FiLoader className="w-6 h-6 animate-spin" />
// <span className="text-lg font-medium">Loading admin panel...</span>
// </div>
// </div>
// );
// }
// return (
// <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
// <div className="max-w-7xl mx-auto mt-20 text-center">
// <header className="mb-8">
// <h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1>
// <p className="text-gray-400">Manage documents and users</p>
// </header>
// {/* Tab Navigation */}
// <div className="flex space-x-4 mb-8">
// <button
// onClick={() => setActiveTab('documents')}
// className={`px-4 py-2 rounded-lg transition-colors ${
// activeTab === 'documents'
// ? 'bg-blue-600 text-white'
// : 'text-gray-400 hover:text-white'
// }`}
// >
// Documents
// </button>
// <button
// onClick={() => setActiveTab('users')}
// className={`px-4 py-2 rounded-lg transition-colors ${
// activeTab === 'users'
// ? 'bg-blue-600 text-white'
// : 'text-gray-400 hover:text-white'
// }`}
// >
// Users
// </button>
// </div>
// {activeTab === 'documents' ? (
// <>
// {/* Upload Section */}
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6 mb-8">
// <h2 className="text-xl font-semibold text-white mb-4">Upload Document</h2>
// <form onSubmit={handleFileUpload} className="space-y-4">
// <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">Document Title</label>
// <input
// type="text"
// value={uploadTitle}
// onChange={(e) => setUploadTitle(e.target.value)}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400"
// placeholder="Enter document title"
// />
// </div>
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">Share With User</label>
// <select
// value={selectedUser}
// onChange={(e) => setSelectedUser(e.target.value)}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
// >
// <option value="">Select user</option>
// {Array.isArray(users) && users.map(user => (
// <option key={user.id} value={user.id}>{user.name}</option>
// ))}
// </select>
// </div>
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">File</label>
// <input
// type="file"
// onChange={(e) => setSelectedFile(e.target.files[0])}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-500 file:text-white hover:file:bg-blue-600"
// />
// </div>
// </div>
// <button
// type="submit"
// className="w-full md:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2"
// >
// <FiUpload className="w-4 h-4" />
// <span>Upload Document</span>
// </button>
// </form>
// </div>
// {/* Documents List */}
// <div className="space-y-4">
// {Object.entries(documents).map(([userName, userDocs]) => (
// <div
// key={userName}
// className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden"
// >
// <button
// onClick={() => toggleUser(userName)}
// className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
// >
// <div className="flex items-center space-x-3">
// <FiUsers className="w-5 h-5 text-blue-400" />
// <span className="font-medium">{userName}</span>
// <span className="text-sm text-gray-400">({userDocs.length} documents)</span>
// </div>
// <FiChevronRight
// className={`w-5 h-5 transition-transform duration-200 ${
// expandedUsers[userName] ? 'rotate-90' : ''
// }`}
// />
// </button>
// {expandedUsers[userName] && (
// <div className="border-t border-gray-700 divide-y divide-gray-700">
// {userDocs.map(doc => (
// <div
// key={doc.id}
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
// >
// <div className="flex items-center space-x-4">
// <FiFile className="w-5 h-5 text-gray-400" />
// <div>
// <h3 className="text-white font-medium">{doc.title}</h3>
// <p className="text-sm text-gray-400">
// Added: {format(new Date(doc.createdAt), 'MMM dd, yyyy')}
// </p>
// </div>
// </div>
// <div className="flex items-center space-x-2">
// <button
// onClick={() => handleDownload(doc.s3Key, doc.title)}
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
// title="Download document"
// >
// <FiDownload className="w-4 h-4" />
// </button>
// <button
// onClick={() => handleShare(doc.id)}
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
// title="Share document"
// >
// <FiShare2 className="w-4 h-4" />
// </button>
// <button
// onClick={() => handleDelete(doc.id)}
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
// title="Delete document"
// >
// <FiTrash2 className="w-4 h-4" />
// </button>
// </div>
// </div>
// ))}
// </div>
// )}
// </div>
// ))}
// </div>
// </>
// ) : (
// // Users Management Section
// <div className="space-y-8">
// {/* Add New User Form */}
// <div className="bg-white/5 backdrop-blur-lg rounded-xl p-6">
// <h2 className="text-xl font-semibold text-white mb-4">Add New User</h2>
// <form onSubmit={handleAddUser} className="grid grid-cols-1 md:grid-cols-2 gap-4">
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">Username</label>
// <input
// type="text"
// value={newUser.username}
// onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
// required
// />
// </div>
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">Password</label>
// <input
// type="password"
// value={newUser.password}
// onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
// required
// />
// </div>
// <div>
// <label className="block text-sm font-medium text-gray-400 mb-1">Full Name</label>
// <input
// type="text"
// value={newUser.name}
// onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
// className="w-full px-4 py-2 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white"
// required
// />
// </div>
// <div className="flex items-center space-x-2">
// <input
// type="checkbox"
// id="isAdmin"
// checked={newUser.isAdmin}
// onChange={(e) => setNewUser({ ...newUser, isAdmin: e.target.checked })}
// className="w-4 h-4 rounded border-gray-700 text-blue-600 focus:ring-blue-500"
// />
// <label htmlFor="isAdmin" className="text-sm font-medium text-gray-400">
// Admin User
// </label>
// </div>
// <div className="md:col-span-2">
// <button
// type="submit"
// className="w-full md:w-auto px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center space-x-2"
// >
// <FiUserPlus className="w-4 h-4" />
// <span>Add User</span>
// </button>
// </div>
// </form>
// </div>
// {/* Users List */}
// <div className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden">
// <div className="px-6 py-4 border-b border-gray-700">
// <h2 className="text-xl font-semibold text-white">Users</h2>
// </div>
// <div className="divide-y divide-gray-700">
// {Array.isArray(users) && users.map(user => (
// <div
// key={user.id}
// className="px-6 py-4 flex items-center justify-between hover:bg-white/5"
// >
// <div className="flex items-center space-x-4">
// <FiUsers className="w-5 h-5 text-gray-400" />
// <div>
// <h3 className="text-white font-medium">{user.name}</h3>
// <p className="text-sm text-gray-400">@{user.username}</p>
// </div>
// {user.isAdmin && (
// <span className="px-2 py-1 text-xs font-medium text-blue-400 bg-blue-400/10 rounded-full">
// Admin
// </span>
// )}
// </div>
// <div className="flex items-center space-x-2">
// <button
// onClick={() => handleResetPassword(user.id)}
// className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
// title="Reset password"
// >
// <FiKey className="w-4 h-4" />
// </button>
// <button
// onClick={() => handleDeleteUser(user.id)}
// className="p-2 text-gray-400 hover:text-red-400 rounded-lg hover:bg-red-500/10 transition-colors"
// title="Delete user"
// >
// <FiTrash2 className="w-4 h-4" />
// </button>
// </div>
// </div>
// ))}
// </div>
// </div>
// </div>
// )}
// </div>
// </div>
// );
// }
// export default AdminPanel;

View File

@ -1,13 +1,12 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { format } from 'date-fns';
import { FiFolder, FiDownload, FiChevronRight, FiChevronDown } from 'react-icons/fi';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { getSharedDocuments } from '../../services/api'; import { getSharedDocuments } from '../../services/api';
import api from '../../services/api'; import { format } from 'date-fns';
import { FiFolder, FiFile, FiDownload, FiChevronRight, FiLoader } from 'react-icons/fi';
import { downloadDocument } from '../../services/api';
function Dashboard() { function Dashboard() {
const [documents, setDocuments] = useState([]); const [documents, setDocuments] = useState({});
const [expandedFolders, setExpandedFolders] = useState({}); const [expandedFolders, setExpandedFolders] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
@ -16,23 +15,29 @@ function Dashboard() {
useEffect(() => { useEffect(() => {
const fetchDocuments = async () => { const fetchDocuments = async () => {
try { try {
if (!user?.id) return;
const response = await getSharedDocuments(user.id); const response = await getSharedDocuments(user.id);
// Group the documents by company and date
const groupedDocs = groupDocumentsByCompanyAndDate(response.data); const groupedDocs = groupDocumentsByCompanyAndDate(response.data);
setDocuments(groupedDocs); setDocuments(groupedDocs);
} catch (err) { } catch (err) {
setError('Failed to fetch documents');
console.error('Error fetching documents:', err); console.error('Error fetching documents:', err);
setError('Failed to fetch documents');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
if (user) { fetchDocuments();
fetchDocuments();
}
}, [user]); }, [user]);
const groupDocumentsByCompanyAndDate = (docs) => { const groupDocumentsByCompanyAndDate = (docs) => {
if (!Array.isArray(docs)) {
console.error('Expected array of documents, received:', docs);
return {};
}
return docs.reduce((acc, doc) => { return docs.reduce((acc, doc) => {
const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`; const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
if (!acc[folderName]) { if (!acc[folderName]) {
@ -43,52 +48,16 @@ function Dashboard() {
}, {}); }, {});
}; };
// const handleDownload = async (s3Key, fileName) => { const handleDownload = async (s3Key, fileName) => {
// try { try {
// const response = await api.get(`/documents/shared/download/${s3Key}`, { await downloadDocument(s3Key);
// responseType: 'blob', } catch (err) {
// }); console.error('Error downloading document:', err);
// const url = window.URL.createObjectURL(new Blob([response.data])); alert('Failed to download document. Please try again.');
// const link = document.createElement('a'); }
// link.href = url; };
// link.setAttribute('download', fileName); // or use the actual filename
// document.body.appendChild(link);
// link.click();
// link.parentNode.removeChild(link);
// } catch (err) {
// console.error('Error downloading document:', err);
// alert('Failed to download document. Please try again.');
// }
// };
// const groupDocumentsByCompanyAndDate = (docs) => {
// return docs.reduce((acc, doc) => {
// const folderName = `${doc.sharedWith?.name || 'Unknown'}-${format(new Date(doc.createdAt), 'yyyy-MM-dd')}`;
// if (!acc[folderName]) {
// acc[folderName] = [];
// }
// acc[folderName].push(doc);
// return acc;
// }, {});
// };
const handleDownload = async (s3Key, fileName) => { const toggleFolder = (folderName) => {
try {
const response = await api.get(`/documents/shared/download/${s3Key}`, {
responseType: 'blob',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', fileName); // or use the actual filename
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
} catch (err) {
console.error('Error downloading document:', err);
alert('Failed to download document. Please try again.');
}
};
const toggleFolder = (folderName) => {
setExpandedFolders(prev => ({ setExpandedFolders(prev => ({
...prev, ...prev,
[folderName]: !prev[folderName] [folderName]: !prev[folderName]
@ -97,94 +66,87 @@ const toggleFolder = (folderName) => {
if (loading) { if (loading) {
return ( return (
<div className="flex justify-center items-center min-h-screen"> <div className="min-h-screen flex items-center justify-center bg-gray-900">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div> <div className="flex items-center space-x-3 text-blue-500">
<FiLoader className="w-6 h-6 animate-spin" />
<span className="text-lg font-medium">Loading documents...</span>
</div>
</div> </div>
); );
} }
if (error) { if (error) {
return ( return (
<div className="text-center text-red-600 p-4"> <div className="min-h-screen flex items-center justify-center bg-gray-900">
{error} <div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
{error}
</div>
</div> </div>
); );
} }
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="min-h-screen bg-gradient-to-br from-gray-900 to-gray-800 p-6">
<h1 className="text-2xl font-bold mb-6">Your Laboratory Reports</h1> <div className="max-w-7xl mx-auto">
<header className="mb-8 mt-20">
{Object.entries(documents).length === 0 ? ( <h1 className="text-3xl font-bold text-white mb-2">Your Documents</h1>
<p className="text-gray-600">No documents have been shared with you yet.</p> <p className="text-gray-400">Access and manage your shared documents</p>
) : ( </header>
<div className="grid gap-4">
{Object.entries(documents).map(([folderName, folderDocs]) => (
<motion.div
key={folderName}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-white p-4 rounded-lg shadow-md"
>
<button
onClick={() => toggleFolder(folderName)}
className="w-full flex items-center text-left p-2 hover:bg-gray-50 rounded-md"
>
<FiFolder className="text-blue-600 mr-2" />
{expandedFolders[folderName] ? (
<FiChevronDown className="mr-2" />
) : (
<FiChevronRight className="mr-2" />
)}
<span className="font-medium">{folderName}</span>
<span className="ml-2 text-sm text-gray-500">
({folderDocs.length} {folderDocs.length === 1 ? 'report' : 'reports'})
</span>
</button>
{expandedFolders[folderName] && ( <div className="grid gap-6">
<motion.div {Object.entries(documents).length > 0 ? (
initial={{ opacity: 0, height: 0 }} Object.entries(documents).map(([folderName, docs]) => (
animate={{ opacity: 1, height: 'auto' }} <div
className="mt-2 space-y-2 pl-8" key={folderName}
className="bg-white/5 backdrop-blur-lg rounded-xl overflow-hidden transition-all duration-200 hover:bg-white/10"
>
<button
onClick={() => toggleFolder(folderName)}
className="w-full px-6 py-4 flex items-center justify-between text-white hover:bg-white/5 transition-colors"
> >
{folderDocs.map((doc) => ( <div className="flex items-center space-x-3">
<div <FiFolder className="w-5 h-5 text-blue-400" />
key={doc.id} <span className="font-medium">{folderName}</span>
className="bg-white p-6 rounded-lg shadow-md hover:shadow-lg transition-shadow" <span className="text-sm text-gray-400">({docs.length} files)</span>
> </div>
<div className="flex justify-between items-start mb-4"> <FiChevronRight
<h2 className="text-xl font-semibold">{doc.title}</h2> className={`w-5 h-5 transition-transform duration-200 ${
<span className={`px-2 py-1 text-xs rounded-full ${ expandedFolders[folderName] ? 'rotate-90' : ''
doc.status === 'completed' }`}
? 'bg-green-100 text-green-800' />
: 'bg-yellow-100 text-yellow-800' </button>
}`}>
{doc.status} {expandedFolders[folderName] && (
</span> <div className="border-t border-gray-700">
</div> {docs.map((doc) => (
<div
<div className="text-sm text-gray-600"> key={doc.id}
<p>Created: {new Date(doc.createdAt).toLocaleDateString()}</p> className="px-6 py-3 flex items-center justify-between hover:bg-white/5 transition-colors"
</div> >
<div className="flex items-center space-x-3">
<div className="mt-4 flex justify-end"> <FiFile className="w-4 h-4 text-gray-400" />
<button <span className="text-gray-200">{doc.title}</span>
onClick={() => handleDownload(doc.s3Key, doc.title)} </div>
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors" <button
onClick={() => handleDownload(doc.s3Key)}
className="p-2 text-gray-400 hover:text-blue-400 rounded-lg hover:bg-blue-500/10 transition-colors"
title="Download document"
> >
<FiDownload className="inline-block mr-2" /> <FiDownload className="w-4 h-4" />
Download
</button> </button>
</div> </div>
</div> ))}
))} </div>
</motion.div> )}
)} </div>
</motion.div> ))
))} ) : (
<div className="text-center py-12">
<p className="text-gray-400">No documents available</p>
</div>
)}
</div> </div>
)} </div>
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { uploadDocument, getAllUsers } from '../../services/api'; import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api';
function DocumentUpload() { function DocumentUpload() {
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
@ -8,9 +8,11 @@ function DocumentUpload() {
const [availableUsers, setAvailableUsers] = useState([]); const [availableUsers, setAvailableUsers] = useState([]);
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
getCurrentUser();
}, []); }, []);
const fetchUsers = async () => { const fetchUsers = async () => {
@ -22,18 +24,22 @@ function DocumentUpload() {
} }
}; };
const handleFileChange = (event) => { const getCurrentUser = async () => {
const selectedFile = event.target.files[0]; try {
setFile(selectedFile); const response = await getUserInfo();
console.log('Current user data:', response.data); // Debug log
setCurrentUser(response.data);
} catch (error) {
console.error('Failed to get current user:', error);
setErrorMessage('Failed to get current user info');
}
}; };
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
console.log('Form submission:', { file, title, selectedUsers }); if (!file || !title || selectedUsers.length === 0 || !currentUser) {
setErrorMessage('Please provide all required information');
if (!file || !title || selectedUsers.length === 0) {
setErrorMessage('Please provide a title, file, and select at least one user');
return; return;
} }
@ -43,15 +49,18 @@ const handleSubmit = async (event) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('title', title); formData.append('title', title);
formData.append('sharedWith', selectedUsers[0]); formData.append('sharedWithId', selectedUsers[0]); // Remove toString()
formData.append('uploadedById', currentUser.id); // Remove toString()
// Debug log
console.log('Form Data Contents:', {
file: formData.get('file'),
title: formData.get('title'),
sharedWithId: formData.get('sharedWithId'),
uploadedById: formData.get('uploadedById')
});
try { try {
console.log('Sending request with:', {
title,
sharedWith: selectedUsers[0],
fileSize: file.size
});
const response = await uploadDocument(formData); const response = await uploadDocument(formData);
console.log('Upload response:', response); console.log('Upload response:', response);
@ -66,6 +75,17 @@ const handleSubmit = async (event) => {
setErrorMessage(error.response?.data?.message || 'Upload failed'); setErrorMessage(error.response?.data?.message || 'Upload failed');
} }
}; };
function handleFileChange(e) {
setFile(e.target.files[0]);
}
const handleUserSelect = (e) => {
const values = Array.from(e.target.selectedOptions, option => option.value);
console.log('Selected user values:', values);
setSelectedUsers(values);
};
return ( return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md"> <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6">Upload Document</h2> <h2 className="text-2xl font-bold mb-6">Upload Document</h2>
@ -103,7 +123,7 @@ const handleSubmit = async (event) => {
<select <select
multiple multiple
value={selectedUsers} value={selectedUsers}
onChange={(e) => setSelectedUsers(Array.from(e.target.selectedOptions, option => option.value))} onChange={handleUserSelect}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required required
> >

View File

@ -1,9 +1,90 @@
// import { useState } from 'react';
// import { useAuth } from '../../hooks/useAuth';
// import { useNavigate } from 'react-router-dom';
// const Login = () => {
// const [username, setUsername] = useState(''); // Changed from email to username
// const [password, setPassword] = useState('');
// const [error, setError] = useState('');
// const { login } = useAuth();
// const navigate = useNavigate();
// const handleSubmit = async (e) => {
// e.preventDefault();
// setError('');
// try {
// const userData = await login(username, password);
// console.log('Login result:', userData);
// if (userData?.isAdmin) {
// navigate('/admin');
// } else {
// navigate('/dashboard');
// }
// } catch (err) {
// setError('Invalid credentials');
// }
// };
// return (
// <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
// <div className="max-w-md w-full space-y-8">
// <div>
// <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
// Sign in to your account
// </h2>
// </div>
// <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
// {error && (
// <div className="text-red-500 text-center text-sm">
// {error}
// </div>
// )}
// <div className="rounded-md shadow-sm -space-y-px">
// <div>
// <input
// type="text"
// required
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
// placeholder="Username"
// value={username}
// onChange={(e) => setUsername(e.target.value)}
// />
// </div>
// <div>
// <input
// type="password"
// required
// className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
// placeholder="Password"
// value={password}
// onChange={(e) => setPassword(e.target.value)}
// />
// </div>
// </div>
// <div>
// <button
// type="submit"
// className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
// >
// Sign in
// </button>
// </div>
// </form>
// </div>
// </div>
// );
// };
// export default Login;
import { useState } from 'react'; import { useState } from 'react';
import { useAuth } from '../../hooks/useAuth'; import { useAuth } from '../../hooks/useAuth';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { FiUser, FiLock } from 'react-icons/fi'; // Import icons
const Login = () => { const Login = () => {
const [username, setUsername] = useState(''); // Changed from email to username const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const { login } = useAuth(); const { login } = useAuth();
@ -15,8 +96,7 @@ const Login = () => {
try { try {
const userData = await login(username, password); const userData = await login(username, password);
console.log('Login result:', userData); if (userData?.isAdmin) {
if (userData?.isAdmin) {
navigate('/admin'); navigate('/admin');
} else { } else {
navigate('/dashboard'); navigate('/dashboard');
@ -27,35 +107,39 @@ const Login = () => {
}; };
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-900 to-gray-800">
<div className="max-w-md w-full space-y-8"> <div className="w-full max-w-md px-8 py-10 bg-white/5 backdrop-blur-lg rounded-2xl shadow-2xl">
<div> <div className="mb-10 text-center">
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900"> <h1 className="text-3xl font-bold text-white mb-2">Welcome Back</h1>
Sign in to your account <p className="text-gray-400">Please sign in to continue</p>
</h2>
</div> </div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="space-y-6">
{error && ( {error && (
<div className="text-red-500 text-center text-sm"> <div className="p-3 text-sm text-red-200 bg-red-500/10 border border-red-500/20 rounded-lg">
{error} {error}
</div> </div>
)} )}
<div className="rounded-md shadow-sm -space-y-px">
<div> <div className="space-y-4">
<div className="relative">
<FiUser className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input <input
type="text" type="text"
required required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
placeholder="Username" placeholder="Username"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
</div> </div>
<div>
<div className="relative">
<FiLock className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input <input
type="password" type="password"
required required
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" className="w-full px-10 py-3 bg-white/5 border border-gray-700 rounded-lg focus:outline-none focus:border-blue-500 text-white placeholder-gray-400 transition-colors"
placeholder="Password" placeholder="Password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
@ -63,13 +147,17 @@ const Login = () => {
</div> </div>
</div> </div>
<div> <button
<button type="submit"
type="submit" className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" >
> Sign In
Sign in </button>
</button>
<div className="mt-6 text-center">
<p className="text-sm text-gray-400">
Need help? Contact your administrator
</p>
</div> </div>
</form> </form>
</div> </div>

View File

@ -22,27 +22,50 @@ api.interceptors.request.use(
} }
); );
export const downloadDocument = async (documentId) => { export const getSharedDocuments = (userId) => {
return api.get(`/documents/shared/${userId}`);
};
const getToken = () => localStorage.getItem('token');
export const downloadDocument = async (key) => {
try { try {
const response = await api.get(`/documents/download/${documentId}`, { const response = await api.get(`/documents/download/${encodeURIComponent(key)}`, {
responseType: 'blob', responseType: 'blob',
headers: { headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`, 'Accept': 'application/octet-stream',
} }
}); });
return response.data;
// Create blob URL and trigger download
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
// Extract filename from key
const fileName = key.split('/').pop();
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return response;
} catch (error) { } catch (error) {
console.error('Download error:', error); console.error('Download error:', error);
throw error; throw error;
} }
}; };
export const createUser = (userData) => { export const createUser = (userData) => {
return api.post('/admin/users', { return api.post('/admin/users', {
name: userData.name, name: userData.name,
email: userData.email, email: userData.email,
password: userData.password, password: userData.password,
isAdmin: userData.isAdmin // Add this line isAdmin: userData.isAdmin
}); });
}; };
export const login = (username, password) => api.post('/auth/login', { username, password }); export const login = (username, password) => api.post('/auth/login', { username, password });
@ -51,15 +74,21 @@ export const updateDocumentStatus = (documentId, status) => api.put(`/admin/docu
export const uploadDocument = async (formData) => { export const uploadDocument = async (formData) => {
try { try {
// Debug log
console.log('Sending to server:', {
title: formData.get('title'),
sharedWithId: formData.get('sharedWithId'),
uploadedById: formData.get('uploadedById')
});
const response = await api.post('/admin/documents', formData, { const response = await api.post('/admin/documents', formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}); });
console.log('Upload API response:', response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Upload API error:', error); console.error('API Error:', error.response?.data);
throw error; throw error;
} }
}; };
@ -67,7 +96,7 @@ export const getUserInfo = () => api.get('/auth/user-info');
export const getAllDocuments = () => api.get('/admin/documents'); export const getAllDocuments = () => api.get('/admin/documents');
export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`); // export const getSharedDocuments = (userId) => api.get(`/documents/shared/${userId}`);
export const getAllUsers = () => api.get('/admin/users'); export const getAllUsers = () => api.get('/admin/users');
export default api; export default api;