opt for prod

This commit is contained in:
dimitar 2025-03-31 05:37:44 +02:00
parent d9f9aaedc5
commit 2958bf69ed
10 changed files with 450 additions and 173 deletions

View File

@ -36,3 +36,6 @@ ADMIN_EMAIL=taratur@gmail.com
DEFAULT_ADMIN_EMAIL=taratur@gmail.com DEFAULT_ADMIN_EMAIL=taratur@gmail.com
DEFAULT_ADMIN_PASSWORD=irina7654321 DEFAULT_ADMIN_PASSWORD=irina7654321
DEFAULT_ADMIN_NAME=admin DEFAULT_ADMIN_NAME=admin
CORS_ORIGIN=http://localhost:5173
NODE_ENV=development

View File

@ -32,6 +32,7 @@
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.13.1", "pg": "^8.13.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^5.0.0",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
"devDependencies": { "devDependencies": {
@ -41,7 +42,7 @@
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.17.28",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
@ -2947,6 +2948,65 @@
"node-pre-gyp": "bin/node-pre-gyp" "node-pre-gyp": "bin/node-pre-gyp"
} }
}, },
"node_modules/@mapbox/node-pre-gyp/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nestjs/cli": { "node_modules/@nestjs/cli": {
"version": "10.4.9", "version": "10.4.9",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz",
@ -7378,6 +7438,69 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/flat-cache/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/flat-cache/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/flat-cache/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/flat-cache/node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.3.3", "version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@ -10825,64 +10948,20 @@
} }
}, },
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^10.3.7"
}, },
"bin": { "bin": {
"rimraf": "bin.js" "rimraf": "dist/esm/bin.mjs"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/run-async": { "node_modules/run-async": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",

View File

@ -57,7 +57,7 @@
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/node": "^20.3.1", "@types/node": "^20.17.28",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",

View File

@ -1,4 +1,3 @@
// src/main.ts
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
import { Logger, ValidationPipe } from "@nestjs/common"; import { Logger, ValidationPipe } from "@nestjs/common";
@ -15,9 +14,26 @@ async function bootstrap() {
// Enable CORS // Enable CORS
app.enableCors({ app.enableCors({
origin: true, origin: [
"https://www.placebo.mk",
"https://placebo.mk",
"http://localhost:5173",
],
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
credentials: true, credentials: true,
allowedHeaders: [
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
],
exposedHeaders: [
'Access-Control-Allow-Origin',
'Access-Control-Allow-Credentials',
],
preflightContinue: false,
optionsSuccessStatus: 204,
}); });
// Global pipes // Global pipes

View File

@ -2,6 +2,11 @@ version: "3.8"
services: services:
backend: backend:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
container_name: imk-backend container_name: imk-backend
build: build:
context: ./backend context: ./backend
@ -12,6 +17,7 @@ services:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 - PORT=3000
- DATABASE_URL=postgresql://postgres:postgres@imk-postgres:5432/postgres?schema=public - DATABASE_URL=postgresql://postgres:postgres@imk-postgres:5432/postgres?schema=public
- FRONTEND_URL=https://www.placebo.mk
env_file: env_file:
- .env - .env
deploy: deploy:
@ -40,7 +46,7 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 15s start_period: 15s
restart: unless-stopped restart: always
postgres: postgres:
container_name: imk-postgres container_name: imk-postgres
image: postgres:14-alpine image: postgres:14-alpine
@ -60,7 +66,7 @@ services:
retries: 5 retries: 5
networks: networks:
- app_network - app_network
restart: unless-stopped restart: always
redis: redis:
container_name: imk-redis container_name: imk-redis
@ -77,7 +83,7 @@ services:
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 3 retries: 3
restart: unless-stopped restart: always
networks: networks:
app_network: app_network:

View File

@ -1,28 +1,41 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { getAllUsers, getAllDocuments, getUserInfo, createUser, resetUserPassword } from '../../services/api'; import {
import DocumentUpload from '../documentUpload/DocumentUpload'; getAllUsers,
import { useNavigate } from 'react-router-dom'; getAllDocuments,
import { FiUsers, FiFile, FiUpload, FiUserPlus, FiLoader, FiKey } from 'react-icons/fi'; getUserInfo,
createUser,
resetUserPassword,
} from "../../services/api";
import DocumentUpload from "../documentUpload/DocumentUpload";
import { useNavigate } from "react-router-dom";
import {
FiUsers,
FiFile,
FiUpload,
FiUserPlus,
FiLoader,
FiKey,
} from "react-icons/fi";
function AdminPanel() { function AdminPanel() {
const navigate = useNavigate(); const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('documents'); const [activeTab, setActiveTab] = useState("documents");
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [documents, setDocuments] = useState([]); const [documents, setDocuments] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState("");
const [isAdmin, setIsAdmin] = useState(false); const [isAdmin, setIsAdmin] = useState(false);
const [resetPasswordModal, setResetPasswordModal] = useState({ const [resetPasswordModal, setResetPasswordModal] = useState({
isOpen: false, isOpen: false,
userId: null, userId: null,
userName: '', userName: "",
newPassword: '', newPassword: "",
}); });
const [newUser, setNewUser] = useState({ const [newUser, setNewUser] = useState({
name: '', name: "",
email: '', email: "",
password: '', password: "",
isAdmin: false isAdmin: false,
}); });
useEffect(() => { useEffect(() => {
@ -39,32 +52,32 @@ function AdminPanel() {
try { try {
const response = await getUserInfo(); const response = await getUserInfo();
if (!response?.data?.isAdmin) { if (!response?.data?.isAdmin) {
navigate('/'); navigate("/");
} else { } else {
setIsAdmin(true); setIsAdmin(true);
await fetchData(); await fetchData();
} }
} catch (error) { } catch (error) {
console.error('Admin check failed:', error); // console.error("Admin check failed:", error);
navigate('/'); navigate("/");
} }
}; };
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
setError(''); setError("");
try { try {
if (activeTab === 'users') { if (activeTab === "users") {
const response = await getAllUsers(); const response = await getAllUsers();
setUsers(response.data); setUsers(response.data);
} else if (activeTab === 'documents') { } else if (activeTab === "documents") {
const response = await getAllDocuments(); const response = await getAllDocuments();
console.log('Documents data:', response.data); // console.log('Documents data:', response.data);
setDocuments(response.data); setDocuments(response.data);
} }
} catch (err) { } catch (err) {
console.error('Fetch error:', err); // console.error("Fetch error:", err);
setError('Failed to fetch data. Please try again.'); setError("Failed to fetch data. Please try again.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -75,41 +88,44 @@ function AdminPanel() {
try { try {
await createUser(newUser); await createUser(newUser);
setNewUser({ setNewUser({
name: '', name: "",
email: '', email: "",
password: '', password: "",
isAdmin: false isAdmin: false,
}); });
fetchData(); fetchData();
} catch (err) { } catch (err) {
setError('Failed to create user'); setError("Failed to create user");
} }
}; };
const handleResetPassword = async (e) => { const handleResetPassword = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
await resetUserPassword(resetPasswordModal.userId, resetPasswordModal.newPassword); await resetUserPassword(
resetPasswordModal.userId,
resetPasswordModal.newPassword,
);
setResetPasswordModal({ setResetPasswordModal({
isOpen: false, isOpen: false,
userId: null, userId: null,
userName: '', userName: "",
newPassword: '', newPassword: "",
}); });
// Show success message // Show success message
setError('Password reset successful'); setError("Password reset successful");
setTimeout(() => setError(''), 3000); setTimeout(() => setError(""), 3000);
} catch (err) { } catch (err) {
setError('Failed to reset password'); setError("Failed to reset password");
} }
}; };
if (!isAdmin) return null; if (!isAdmin) return null;
const tabs = [ const tabs = [
{ id: 'documents', name: 'Documents', icon: FiFile }, { id: "documents", name: "Documents", icon: FiFile },
{ id: 'users', name: 'Users', icon: FiUsers }, { id: "users", name: "Users", icon: FiUsers },
{ id: 'upload', name: 'Upload Document', icon: FiUpload } { id: "upload", name: "Upload Document", icon: FiUpload },
]; ];
if (loading) { if (loading) {
@ -127,7 +143,9 @@ function AdminPanel() {
<div className="min-h-screen bg-gradient-to-br from-primary-900 to-primary-800 p-6"> <div className="min-h-screen bg-gradient-to-br from-primary-900 to-primary-800 p-6">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<header className="mb-8 mt-20"> <header className="mb-8 mt-20">
<h1 className="text-3xl font-bold text-white mb-2">Admin Dashboard</h1> <h1 className="text-3xl font-bold text-white mb-2">
Admin Dashboard
</h1>
<p className="text-neutral-400">Manage users and documents</p> <p className="text-neutral-400">Manage users and documents</p>
</header> </header>
@ -139,9 +157,11 @@ function AdminPanel() {
onClick={() => setActiveTab(id)} onClick={() => setActiveTab(id)}
className={` className={`
px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors
${activeTab === id ${
? 'bg-primary-600 text-white' activeTab === id
: 'text-neutral-400 hover:bg-primary-700/50 hover:text-white'} ? "bg-primary-600 text-white"
: "text-neutral-400 hover:bg-primary-700/50 hover:text-white"
}
`} `}
> >
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
@ -167,11 +187,13 @@ function AdminPanel() {
type="password" type="password"
placeholder="New Password" placeholder="New Password"
value={resetPasswordModal.newPassword} value={resetPasswordModal.newPassword}
onChange={(e) => setResetPasswordModal({ onChange={(e) =>
...resetPasswordModal, setResetPasswordModal({
newPassword: e.target.value ...resetPasswordModal,
})} newPassword: e.target.value,
className="w-full bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2 })
}
className="w-full bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500 text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500
focus:ring-1 focus:ring-primary-500" focus:ring-1 focus:ring-primary-500"
required required
@ -180,20 +202,22 @@ function AdminPanel() {
<div className="flex justify-end space-x-3"> <div className="flex justify-end space-x-3">
<button <button
type="button" type="button"
onClick={() => setResetPasswordModal({ onClick={() =>
isOpen: false, setResetPasswordModal({
userId: null, isOpen: false,
userName: '', userId: null,
newPassword: '', userName: "",
})} newPassword: "",
})
}
className="px-4 py-2 text-neutral-400 hover:text-white transition-colors" className="px-4 py-2 text-neutral-400 hover:text-white transition-colors"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="flex items-center justify-center space-x-2 px-4 py-2 className="flex items-center justify-center space-x-2 px-4 py-2
bg-primary-600 hover:bg-primary-700 text-white rounded-lg bg-primary-600 hover:bg-primary-700 text-white rounded-lg
transition-colors shadow-lg" transition-colors shadow-lg"
> >
<FiKey className="w-4 h-4" /> <FiKey className="w-4 h-4" />
@ -206,30 +230,48 @@ function AdminPanel() {
)} )}
<div className="grid gap-6"> <div className="grid gap-6">
{activeTab === 'documents' && ( {activeTab === "documents" && (
<div className="bg-primary-800/50 backdrop-blur-lg rounded-xl overflow-hidden shadow-xl"> <div className="bg-primary-800/50 backdrop-blur-lg rounded-xl overflow-hidden shadow-xl">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-primary-700"> <tr className="border-b border-primary-700">
<th className="px-6 py-4 text-left text-sm text-neutral-400">Title</th> <th className="px-6 py-4 text-left text-sm text-neutral-400">
<th className="px-6 py-4 text-left text-sm text-neutral-400">Status</th> Title
<th className="px-6 py-4 text-left text-sm text-neutral-400">Uploaded By</th> </th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">Shared With</th> <th className="px-6 py-4 text-left text-sm text-neutral-400">
<th className="px-6 py-4 text-left text-sm text-neutral-400">Created At</th> Status
</th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">
Uploaded By
</th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">
Shared With
</th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">
Created At
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{documents.map((doc) => ( {documents.map((doc) => (
<tr key={doc.id} className="border-b border-primary-700/50 hover:bg-primary-700/30"> <tr
key={doc.id}
className="border-b border-primary-700/50 hover:bg-primary-700/30"
>
<td className="px-6 py-4 text-white">{doc.title}</td> <td className="px-6 py-4 text-white">{doc.title}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${ <span
doc.status === 'completed' ? 'bg-green-500/20 text-green-300' : className={`px-3 py-1 rounded-full text-xs font-medium ${
doc.status === 'pending' ? 'bg-yellow-500/20 text-yellow-300' : doc.status === "completed"
doc.status === 'uploading' ? 'bg-primary-500/20 text-primary-300' : ? "bg-green-500/20 text-green-300"
'bg-neutral-500/20 text-neutral-300' : doc.status === "pending"
}`}> ? "bg-yellow-500/20 text-yellow-300"
: doc.status === "uploading"
? "bg-primary-500/20 text-primary-300"
: "bg-neutral-500/20 text-neutral-300"
}`}
>
{doc.status} {doc.status}
</span> </span>
</td> </td>
@ -237,13 +279,16 @@ function AdminPanel() {
{doc.uploadedBy?.name} ({doc.uploadedBy?.email}) {doc.uploadedBy?.name} ({doc.uploadedBy?.email})
</td> </td>
<td className="px-6 py-4 text-neutral-300"> <td className="px-6 py-4 text-neutral-300">
{doc.sharedWith && doc.sharedWith.length > 0 {doc.sharedWith && doc.sharedWith.length > 0
? doc.sharedWith.map(user => ( ? doc.sharedWith.map((user) => (
<div key={user.id} className="whitespace-nowrap"> <div
key={user.id}
className="whitespace-nowrap"
>
{user.name} ({user.email}) {user.name} ({user.email})
</div> </div>
)) ))
: 'None'} : "None"}
</td> </td>
<td className="px-6 py-4 text-neutral-300"> <td className="px-6 py-4 text-neutral-300">
{new Date(doc.createdAt).toLocaleString()} {new Date(doc.createdAt).toLocaleString()}
@ -256,17 +301,24 @@ function AdminPanel() {
</div> </div>
)} )}
{activeTab === 'users' && ( {activeTab === "users" && (
<> <>
<div className="bg-primary-800/50 backdrop-blur-lg rounded-xl p-6 mb-6 shadow-xl"> <div className="bg-primary-800/50 backdrop-blur-lg rounded-xl p-6 mb-6 shadow-xl">
<h2 className="text-xl font-bold text-white mb-4">Create New User</h2> <h2 className="text-xl font-bold text-white mb-4">
<form onSubmit={handleCreateUser} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> Create New User
</h2>
<form
onSubmit={handleCreateUser}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
<input <input
type="text" type="text"
placeholder="Name" placeholder="Name"
value={newUser.name} value={newUser.name}
onChange={(e) => setNewUser({ ...newUser, name: e.target.value })} onChange={(e) =>
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2 setNewUser({ ...newUser, name: e.target.value })
}
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500 text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500
focus:ring-1 focus:ring-primary-500" focus:ring-1 focus:ring-primary-500"
required required
@ -275,8 +327,10 @@ function AdminPanel() {
type="email" type="email"
placeholder="Email" placeholder="Email"
value={newUser.email} value={newUser.email}
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} onChange={(e) =>
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2 setNewUser({ ...newUser, email: e.target.value })
}
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500 text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500
focus:ring-1 focus:ring-primary-500" focus:ring-1 focus:ring-primary-500"
required required
@ -285,8 +339,10 @@ function AdminPanel() {
type="password" type="password"
placeholder="Password" placeholder="Password"
value={newUser.password} value={newUser.password}
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} onChange={(e) =>
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2 setNewUser({ ...newUser, password: e.target.value })
}
className="bg-primary-700/30 border border-primary-600 rounded-lg px-4 py-2
text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500 text-white placeholder-neutral-400 focus:outline-none focus:border-primary-500
focus:ring-1 focus:ring-primary-500" focus:ring-1 focus:ring-primary-500"
required required
@ -296,16 +352,20 @@ function AdminPanel() {
type="checkbox" type="checkbox"
id="isAdmin" id="isAdmin"
checked={newUser.isAdmin} checked={newUser.isAdmin}
onChange={(e) => setNewUser({ ...newUser, isAdmin: e.target.checked })} onChange={(e) =>
className="rounded border-primary-600 bg-primary-700/30 text-primary-500 setNewUser({ ...newUser, isAdmin: e.target.checked })
}
className="rounded border-primary-600 bg-primary-700/30 text-primary-500
focus:ring-primary-500" focus:ring-primary-500"
/> />
<label htmlFor="isAdmin" className="text-white">Is Admin</label> <label htmlFor="isAdmin" className="text-white">
Is Admin
</label>
</div> </div>
<button <button
type="submit" type="submit"
className="flex items-center justify-center space-x-2 px-4 py-2 className="flex items-center justify-center space-x-2 px-4 py-2
bg-primary-600 hover:bg-primary-700 text-white rounded-lg bg-primary-600 hover:bg-primary-700 text-white rounded-lg
transition-colors shadow-lg" transition-colors shadow-lg"
> >
<FiUserPlus className="w-4 h-4" /> <FiUserPlus className="w-4 h-4" />
@ -318,31 +378,46 @@ function AdminPanel() {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-primary-700"> <tr className="border-b border-primary-700">
<th className="px-6 py-4 text-left text-sm text-neutral-400">Name</th> <th className="px-6 py-4 text-left text-sm text-neutral-400">
<th className="px-6 py-4 text-left text-sm text-neutral-400">Email</th> Name
<th className="px-6 py-4 text-left text-sm text-neutral-400">Role</th> </th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">Actions</th> <th className="px-6 py-4 text-left text-sm text-neutral-400">
Email
</th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">
Role
</th>
<th className="px-6 py-4 text-left text-sm text-neutral-400">
Actions
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{users.map((user) => ( {users.map((user) => (
<tr key={user.id} className="border-b border-primary-700/50 hover:bg-primary-700/30"> <tr
key={user.id}
className="border-b border-primary-700/50 hover:bg-primary-700/30"
>
<td className="px-6 py-4 text-white">{user.name}</td> <td className="px-6 py-4 text-white">{user.name}</td>
<td className="px-6 py-4 text-white">{user.email}</td> <td className="px-6 py-4 text-white">{user.email}</td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium <span
${user.isAdmin ? 'bg-primary-500/20 text-primary-300' : 'bg-neutral-500/20 text-neutral-300'}`}> className={`px-3 py-1 rounded-full text-xs font-medium
{user.isAdmin ? 'Admin' : 'User'} ${user.isAdmin ? "bg-primary-500/20 text-primary-300" : "bg-neutral-500/20 text-neutral-300"}`}
>
{user.isAdmin ? "Admin" : "User"}
</span> </span>
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
<button <button
onClick={() => setResetPasswordModal({ onClick={() =>
isOpen: true, setResetPasswordModal({
userId: user.id, isOpen: true,
userName: user.name, userId: user.id,
newPassword: '', userName: user.name,
})} newPassword: "",
})
}
className="flex items-center space-x-1 text-neutral-400 hover:text-white transition-colors" className="flex items-center space-x-1 text-neutral-400 hover:text-white transition-colors"
> >
<FiKey className="w-4 h-4" /> <FiKey className="w-4 h-4" />
@ -357,7 +432,7 @@ function AdminPanel() {
</> </>
)} )}
{activeTab === 'upload' && ( {activeTab === "upload" && (
<div className="bg-primary-800/50 backdrop-blur-lg rounded-xl p-6 shadow-xl"> <div className="bg-primary-800/50 backdrop-blur-lg rounded-xl p-6 shadow-xl">
<DocumentUpload /> <DocumentUpload />
</div> </div>
@ -368,4 +443,4 @@ function AdminPanel() {
); );
} }
export default AdminPanel; export default AdminPanel;

View File

@ -0,0 +1,70 @@
import { createContext, useContext, useState, useEffect } from "react";
import api from "../services/api";
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const token = localStorage.getItem("token");
if (!token) {
setIsLoading(false);
return;
}
const response = await api.get("/auth/user-info"); // Updated endpoint
setUser(response.data);
} catch (error) {
console.error("Failed to fetch user info:", error);
localStorage.removeItem("token");
} finally {
setIsLoading(false);
}
};
fetchUser();
}, []);
const login = async (username, password) => {
try {
const response = await api.post("/auth/login", { username, password });
console.log("Login response:", response.data); // Debug log
const { access_token } = response.data;
localStorage.setItem("token", access_token);
// Fetch user info after login
const userResponse = await api.get("/auth/user-info");
const userData = userResponse.data;
setUser(userData);
return userData; // Return the user data for redirect logic
} catch (error) {
console.error("Login error:", error);
throw error;
}
};
const logout = () => {
localStorage.removeItem("token");
setUser(null);
};
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@ -1,3 +1,4 @@
// frontend/src/hooks/useAuth.jsx
import { createContext, useContext, useState, useEffect } from "react"; import { createContext, useContext, useState, useEffect } from "react";
import api from "../services/api"; import api from "../services/api";
@ -6,6 +7,7 @@ const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => { export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
const fetchUser = async () => { const fetchUser = async () => {
@ -16,11 +18,12 @@ export const AuthProvider = ({ children }) => {
return; return;
} }
const response = await api.get("/auth/user-info"); // Updated endpoint const response = await api.get("/auth/user-info");
setUser(response.data); setUser(response.data);
} catch (error) { } catch (error) {
console.error("Failed to fetch user info:", error); console.error("Failed to fetch user info:", error);
localStorage.removeItem("token"); localStorage.removeItem("token");
setError(error.message);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -32,17 +35,15 @@ export const AuthProvider = ({ children }) => {
const login = async (username, password) => { const login = async (username, password) => {
try { try {
const response = await api.post("/auth/login", { username, password }); const response = await api.post("/auth/login", { username, password });
console.log("Login response:", response.data); // Debug log
const { access_token } = response.data; const { access_token } = response.data;
localStorage.setItem("token", access_token); localStorage.setItem("token", access_token);
// Fetch user info after login // Fetch user info after successful login
const userResponse = await api.get("/auth/user-info"); const userResponse = await api.get("/auth/user-info");
const userData = userResponse.data; setUser(userResponse.data);
setUser(userData); return userResponse.data;
return userData; // Return the user data for redirect logic
} catch (error) { } catch (error) {
console.error("Login error:", error); console.error("Login error:", error);
throw error; throw error;
@ -55,7 +56,7 @@ export const AuthProvider = ({ children }) => {
}; };
return ( return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}> <AuthContext.Provider value={{ user, isLoading, error, login, logout }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

View File

@ -1,6 +1,9 @@
import axios from "axios"; import axios from "axios";
const API_URL = "http://localhost:3000"; const API_URL =
process.env.NODE_ENV === "production"
? "https://imkapi.oblak.solutions"
: "http://localhost:3000";
const api = axios.create({ const api = axios.create({
baseURL: API_URL, baseURL: API_URL,
@ -112,5 +115,14 @@ export const forgotPassword = (email) =>
api.post("/auth/forgot-password", { email }); api.post("/auth/forgot-password", { email });
export const resetPassword = (token, newPassword) => export const resetPassword = (token, newPassword) =>
api.post("/auth/reset-password", { token, newPassword }); api.post("/auth/reset-password", { token, newPassword });
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem("token");
window.location.href = "/login";
}
return Promise.reject(error);
},
);
export default api; export default api;

View File

@ -4,14 +4,29 @@ import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { // server: {
port: 5173, // port: 5173,
proxy: { // proxy: {
"/api": { // "/api": {
target: "http://localhost:3000", // target: "http://localhost:3000",
changeOrigin: true, // changeOrigin: true,
secure: false, // secure: false,
// },
// },
// },
build: {
outDir: "dist",
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom", "react-router-dom"],
ui: ["@headlessui/react", "@heroicons/react"],
},
}, },
}, },
}, },
define: {
"process.env.API_URL": JSON.stringify("https://imkapi.oblak.solutions"),
},
}); });