document upload redesign

This commit is contained in:
dimitar 2025-04-02 21:33:11 +02:00
parent 0269cefeeb
commit c396a00bbc
3 changed files with 242 additions and 191 deletions

View File

@ -181,9 +181,9 @@ export class AuthController {
return this.authService.resetPasswordWithToken(token, newPassword); return this.authService.resetPasswordWithToken(token, newPassword);
} }
@Post("logout") // @Post("logout")
async logout(@Request() req) { // async logout(@Request() req) {
await this.authService.logout(req.user.userId); // await this.authService.logout(req.user.userId);
return { message: "Logged out successfully" }; // return { message: "Logged out successfully" };
} // }
} }

View File

@ -1,8 +1,13 @@
import { Injectable, Logger, NotFoundException, ForbiddenException } from '@nestjs/common'; import {
import { PrismaService } from '../prisma/prisma.service'; Injectable,
import { S3Service } from '../s3/s3.service'; Logger,
import { Document, User, Prisma } from '@prisma/client'; NotFoundException,
import { EmailService } from '../email/email.service'; ForbiddenException,
} from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { S3Service } from "../s3/s3.service";
import { Document, User, Prisma } from "@prisma/client";
import { EmailService } from "../email/email.service";
@Injectable() @Injectable()
export class DocumentsService { export class DocumentsService {
@ -13,8 +18,8 @@ export class DocumentsService {
private readonly s3Service: S3Service, private readonly s3Service: S3Service,
private readonly emailService: EmailService, private readonly emailService: EmailService,
) { ) {
this.logger.log('DocumentsService initialized with EmailService'); this.logger.log("DocumentsService initialized with EmailService");
this.logger.debug('EmailService instance:', { this.logger.debug("EmailService instance:", {
exists: !!this.emailService, exists: !!this.emailService,
type: typeof this.emailService, type: typeof this.emailService,
methods: Object.keys(Object.getPrototypeOf(this.emailService)), methods: Object.keys(Object.getPrototypeOf(this.emailService)),
@ -31,16 +36,19 @@ export class DocumentsService {
}); });
} }
async verifyDocumentAccess(documentId: number, userId: number): Promise<boolean> { async verifyDocumentAccess(
documentId: number,
userId: number,
): Promise<boolean> {
const document = await this.prisma.document.findUnique({ const document = await this.prisma.document.findUnique({
where: { id: documentId }, where: { id: documentId },
include: { include: {
uploadedBy: true, uploadedBy: true,
sharedWith: { sharedWith: {
where: { where: {
id: userId id: userId,
} },
} },
}, },
}); });
@ -60,31 +68,31 @@ export class DocumentsService {
{ {
sharedWith: { sharedWith: {
some: { some: {
id: clientId id: clientId,
} },
} },
} },
] ],
}, },
include: { include: {
uploadedBy: { uploadedBy: {
select: { select: {
id: true, id: true,
name: true, name: true,
email: true email: true,
} },
}, },
sharedWith: { sharedWith: {
select: { select: {
id: true, id: true,
name: true, name: true,
email: true email: true,
} },
} },
}, },
orderBy: { orderBy: {
createdAt: 'desc' createdAt: "desc",
} },
}); });
} }
@ -92,10 +100,10 @@ export class DocumentsService {
file: Express.Multer.File, file: Express.Multer.File,
title: string, title: string,
sharedWithId: number, sharedWithId: number,
uploadedById: number uploadedById: number,
) { ) {
this.logger.log('=== Starting document upload process ==='); this.logger.log("=== Starting document upload process ===");
this.logger.debug('Upload parameters:', { this.logger.debug("Upload parameters:", {
title, title,
sharedWithId, sharedWithId,
uploadedById, uploadedById,
@ -104,22 +112,22 @@ export class DocumentsService {
}); });
try { try {
this.logger.debug('Uploading file to S3...'); this.logger.debug("Uploading file to S3...");
const s3Key = await this.s3Service.uploadFile(file, 'documents'); const s3Key = await this.s3Service.uploadFile(file, "documents");
this.logger.debug(`File uploaded to S3 successfully with key: ${s3Key}`); this.logger.debug(`File uploaded to S3 successfully with key: ${s3Key}`);
this.logger.debug('Creating document record in database...'); this.logger.debug("Creating document record in database...");
const document = await this.prisma.document.create({ const document = await this.prisma.document.create({
data: { data: {
title, title,
s3Key, s3Key,
status: 'pending', status: "completed",
uploadedBy: { uploadedBy: {
connect: { id: uploadedById } connect: { id: uploadedById },
}, },
sharedWith: { sharedWith: {
connect: { id: sharedWithId } connect: { id: sharedWithId },
} },
}, },
include: { include: {
uploadedBy: { uploadedBy: {
@ -138,7 +146,7 @@ export class DocumentsService {
}, },
}, },
}); });
this.logger.debug('Document record created:', { this.logger.debug("Document record created:", {
id: document.id, id: document.id,
title: document.title, title: document.title,
uploadedBy: document.uploadedBy, uploadedBy: document.uploadedBy,
@ -146,7 +154,7 @@ export class DocumentsService {
}); });
// Send email notifications // Send email notifications
this.logger.log('=== Starting email notification process ==='); this.logger.log("=== Starting email notification process ===");
// Notify the user who the document is shared with // Notify the user who the document is shared with
const sharedUser = document.sharedWith[0]; const sharedUser = document.sharedWith[0];
@ -157,30 +165,32 @@ export class DocumentsService {
name: sharedUser.name, name: sharedUser.name,
}); });
try { try {
this.logger.debug('Calling EmailService.sendDocumentNotification for shared user...'); this.logger.debug(
"Calling EmailService.sendDocumentNotification for shared user...",
);
await this.emailService.sendDocumentNotification( await this.emailService.sendDocumentNotification(
sharedUser.email, sharedUser.email,
sharedUser.name, sharedUser.name,
document.title, document.title,
'shared' "shared",
); );
this.logger.debug('Shared user notification sent successfully'); this.logger.debug("Shared user notification sent successfully");
} catch (error) { } catch (error) {
this.logger.error('Failed to send notification to shared user:', { this.logger.error("Failed to send notification to shared user:", {
error: error.message, error: error.message,
code: error.code, code: error.code,
command: error.command, command: error.command,
stack: error.stack, stack: error.stack,
}); });
// Log the email service instance to verify it's properly injected // Log the email service instance to verify it's properly injected
this.logger.debug('EmailService instance:', { this.logger.debug("EmailService instance:", {
exists: !!this.emailService, exists: !!this.emailService,
type: typeof this.emailService, type: typeof this.emailService,
methods: Object.keys(Object.getPrototypeOf(this.emailService)), methods: Object.keys(Object.getPrototypeOf(this.emailService)),
}); });
} }
} else { } else {
this.logger.warn('No shared user found in the document record'); this.logger.warn("No shared user found in the document record");
} }
// Notify the uploader // Notify the uploader
@ -192,36 +202,38 @@ export class DocumentsService {
name: uploader.name, name: uploader.name,
}); });
try { try {
this.logger.debug('Calling EmailService.sendDocumentNotification for uploader...'); this.logger.debug(
"Calling EmailService.sendDocumentNotification for uploader...",
);
await this.emailService.sendDocumentNotification( await this.emailService.sendDocumentNotification(
uploader.email, uploader.email,
uploader.name, uploader.name,
document.title, document.title,
'uploaded' "uploaded",
); );
this.logger.debug('Uploader notification sent successfully'); this.logger.debug("Uploader notification sent successfully");
} catch (error) { } catch (error) {
this.logger.error('Failed to send notification to uploader:', { this.logger.error("Failed to send notification to uploader:", {
error: error.message, error: error.message,
code: error.code, code: error.code,
command: error.command, command: error.command,
stack: error.stack, stack: error.stack,
}); });
// Log the email service instance to verify it's properly injected // Log the email service instance to verify it's properly injected
this.logger.debug('EmailService instance:', { this.logger.debug("EmailService instance:", {
exists: !!this.emailService, exists: !!this.emailService,
type: typeof this.emailService, type: typeof this.emailService,
methods: Object.keys(Object.getPrototypeOf(this.emailService)), methods: Object.keys(Object.getPrototypeOf(this.emailService)),
}); });
} }
} else { } else {
this.logger.warn('No uploader found in the document record'); this.logger.warn("No uploader found in the document record");
} }
this.logger.log('=== Document upload process completed ==='); this.logger.log("=== Document upload process completed ===");
return document; return document;
} catch (error) { } catch (error) {
this.logger.error('Error in uploadDocument:', { this.logger.error("Error in uploadDocument:", {
error: error.message, error: error.message,
code: error.code, code: error.code,
stack: error.stack, stack: error.stack,

View File

@ -1,13 +1,16 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { uploadDocument, getAllUsers, getUserInfo } from '../../services/api'; import { motion } from "framer-motion";
import { FiUpload, FiLoader, FiCheckCircle, FiUsers } from "react-icons/fi";
import { uploadDocument, getAllUsers, getUserInfo } from "../../services/api";
import { Card } from "../UI/Card";
function DocumentUpload() { function DocumentUpload() {
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [title, setTitle] = useState(''); const [title, setTitle] = useState("");
const [selectedUsers, setSelectedUsers] = useState([]); const [selectedUserId, setSelectedUserId] = useState(""); // Changed from array to single value
const [availableUsers, setAvailableUsers] = useState([]); const [availableUsers, setAvailableUsers] = useState([]);
const [status, setStatus] = useState('idle'); // idle, uploading, completed, failed const [status, setStatus] = useState("idle");
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState("");
const [currentUser, setCurrentUser] = useState(null); const [currentUser, setCurrentUser] = useState(null);
useEffect(() => { useEffect(() => {
@ -15,158 +18,194 @@ function DocumentUpload() {
getCurrentUser(); getCurrentUser();
}, []); }, []);
const fetchUsers = async () => {
try {
const response = await getAllUsers();
setAvailableUsers(response.data.filter(user => !user.isAdmin));
} catch (error) {
setErrorMessage('Failed to load users');
}
};
const getCurrentUser = async () => { const getCurrentUser = async () => {
try { try {
const response = await getUserInfo(); const response = await getUserInfo();
console.log('Current user data:', response.data); // Debug log
setCurrentUser(response.data); setCurrentUser(response.data);
} catch (error) { } catch (error) {
console.error('Failed to get current user:', error); console.error("Failed to get current user:", error);
setErrorMessage('Failed to get current user info'); setErrorMessage("Failed to get current user info");
}
};
const fetchUsers = async () => {
try {
const response = await getAllUsers();
setAvailableUsers(response.data.filter((user) => !user.isAdmin));
} catch (error) {
setErrorMessage("Failed to load users");
} }
}; };
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
if (!file || !title || selectedUsers.length === 0 || !currentUser) { if (!file || !title || !selectedUserId || !currentUser?.id) {
setErrorMessage('Please provide all required information'); setErrorMessage("Please provide all required information");
return; return;
} }
setStatus('uploading'); // Convert IDs to numbers
setErrorMessage(''); const sharedWithId = parseInt(selectedUserId, 10);
const uploadedById = parseInt(currentUser.id, 10);
if (isNaN(sharedWithId) || isNaN(uploadedById)) {
setErrorMessage("Invalid user IDs");
return;
}
setStatus("uploading");
setErrorMessage("");
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('sharedWithId', selectedUsers[0]); // Remove toString() formData.append("sharedWithId", sharedWithId);
formData.append('uploadedById', currentUser.id); // Remove toString() formData.append("uploadedById", uploadedById);
// Debug log // Debug log
console.log('Form Data Contents:', { console.log("Form Data:", {
file: formData.get('file'), title,
title: formData.get('title'), sharedWithId,
sharedWithId: formData.get('sharedWithId'), uploadedById,
uploadedById: formData.get('uploadedById') fileName: file.name,
}); });
try { try {
const response = await uploadDocument(formData); await uploadDocument(formData);
console.log('Upload response:', response); setStatus("completed");
setTitle("");
setStatus('completed');
setTitle('');
setFile(null); setFile(null);
setSelectedUsers([]); setSelectedUserId("");
event.target.reset(); event.target.reset();
} catch (error) { } catch (error) {
console.error('Upload error:', error); console.error("Upload error:", error);
setStatus('failed'); setStatus("failed");
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="min-h-screen bg-gradient-to-br from-primary-900 to-primary-800 p-6">
<h2 className="text-2xl font-bold mb-6">Upload Document</h2> <div className="max-w-4xl mx-auto">
<header className="mb-8 mt-20">
<h1 className="text-3xl font-bold text-white mb-2">
Upload Document
</h1>
<p className="text-neutral-400">Share documents with your clients</p>
</header>
<form onSubmit={handleSubmit} className="space-y-4"> <motion.div
<div> initial={{ opacity: 0, y: 20 }}
<label className="block text-sm font-medium text-gray-700"> animate={{ opacity: 1, y: 0 }}
Document Title transition={{ duration: 0.5 }}
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
File
</label>
<input
type="file"
onChange={handleFileChange}
className="mt-1 block w-full"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Share with Users
</label>
<select
multiple
value={selectedUsers}
onChange={handleUserSelect}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
required
>
{availableUsers.map(user => (
<option key={user.id} value={user.id}>
{user.name} ({user.email})
</option>
))}
</select>
</div>
{errorMessage && (
<div className="text-red-500 text-sm mt-2">
{errorMessage}
</div>
)}
<button
type="submit"
disabled={status === 'uploading'}
className={`w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white
${status === 'uploading'
? 'bg-gray-400 cursor-not-allowed'
: 'bg-indigo-600 hover:bg-indigo-700'
}`}
> >
{status === 'uploading' ? ( <Card className="bg-neutral-900/80 backdrop-blur-lg border border-neutral-800">
<> <form onSubmit={handleSubmit} className="space-y-6">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> {/* Title Input */}
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> <div>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <label className="block text-sm font-medium text-white mb-2">
</svg> Document Title
Uploading... </label>
</> <input
) : 'Upload Document'} type="text"
</button> value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-neutral-800/50 border border-neutral-700
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500"
required
/>
</div>
{status === 'completed' && ( {/* File Input */}
<div className="text-green-500 text-sm mt-2"> <div>
Document uploaded successfully! <label className="block text-sm font-medium text-white mb-2">
</div> File
)} </label>
</form> <div className="flex items-center justify-center w-full">
<label
className="w-full flex flex-col items-center px-4 py-6 rounded-lg
border-2 border-dashed border-neutral-700 hover:border-primary-500
cursor-pointer transition-colors"
>
<FiUpload className="w-8 h-8 text-neutral-400" />
<span className="mt-2 text-sm text-neutral-400">
{file ? file.name : "Select a file"}
</span>
<input
type="file"
className="hidden"
onChange={(e) => setFile(e.target.files[0])}
required
/>
</label>
</div>
</div>
{/* User Selection */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Share with User
</label>
<div className="relative">
<FiUsers className="absolute top-3 left-3 text-neutral-400" />
<select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="w-full pl-10 px-4 py-2 rounded-lg bg-neutral-800/50 border border-neutral-700
text-white focus:outline-none focus:border-primary-500"
required
>
<option value="">Select a user</option>
{availableUsers.map((user) => (
<option key={user.id} value={user.id}>
{user.name} ({user.email})
</option>
))}
</select>
</div>
</div>
{/* Error Message */}
{errorMessage && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-200">
{errorMessage}
</div>
)}
{/* Success Message */}
{status === "completed" && (
<div className="p-4 bg-green-500/10 border border-green-500/20 rounded-lg text-green-200">
Document uploaded successfully!
</div>
)}
{/* Submit Button */}
<button
type="submit"
disabled={status === "uploading"}
className="w-full flex items-center justify-center px-4 py-2 rounded-lg
bg-primary-600 hover:bg-primary-700 text-white font-medium
transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{status === "uploading" ? (
<>
<FiLoader className="w-5 h-5 mr-2 animate-spin" />
Uploading...
</>
) : status === "completed" ? (
<>
<FiCheckCircle className="w-5 h-5 mr-2" />
Upload Complete
</>
) : (
"Upload Document"
)}
</button>
</form>
</Card>
</motion.div>
</div>
</div> </div>
); );
} }