diff --git a/admin/src/Layout/Dashboard.tsx b/admin/src/Layout/Dashboard.tsx index 7d78cdf..1b53d6d 100644 --- a/admin/src/Layout/Dashboard.tsx +++ b/admin/src/Layout/Dashboard.tsx @@ -5,6 +5,7 @@ import Sidebar from "./Sidebar"; import UserTable from "../components/UserTable"; import ItemTable from "../components/ItemTable"; import LoanTable from "../components/LoanTable"; +import APIKeyTable from "@/components/APIKeyTable"; import { MoveLeft } from "lucide-react"; type DashboardProps = { @@ -23,6 +24,7 @@ const Dashboard: React.FC = ({ onLogout }) => { viewGegenstaende={() => setActiveView("Gegenstände")} viewSchliessfaecher={() => setActiveView("Schließfächer")} viewUser={() => setActiveView("User")} + viewAPI={() => setActiveView("API")} /> = ({ onLogout }) => { {activeView === "User" && } {activeView === "Ausleihen" && } {activeView === "Gegenstände" && } + {activeView === "API" && } diff --git a/admin/src/Layout/Sidebar.tsx b/admin/src/Layout/Sidebar.tsx index 3637124..05817fe 100644 --- a/admin/src/Layout/Sidebar.tsx +++ b/admin/src/Layout/Sidebar.tsx @@ -6,12 +6,14 @@ type SidebarProps = { viewGegenstaende: () => void; viewSchliessfaecher: () => void; viewUser: () => void; + viewAPI: () => void; }; const Sidebar: React.FC = ({ viewAusleihen, viewGegenstaende, viewUser, + viewAPI, }) => { return ( = ({ > Gegenstände + + API Keys + diff --git a/admin/src/components/APIKeyTable.tsx b/admin/src/components/APIKeyTable.tsx new file mode 100644 index 0000000..2173ce3 --- /dev/null +++ b/admin/src/components/APIKeyTable.tsx @@ -0,0 +1,203 @@ +import React from "react"; +import { + Table, + Spinner, + Text, + VStack, + Button, + HStack, + IconButton, + Heading, +} from "@chakra-ui/react"; +import { Tooltip } from "@/components/ui/tooltip"; +import MyAlert from "./myChakra/MyAlert"; +import { Trash2, RefreshCcwDot, CirclePlus } from "lucide-react"; +import Cookies from "js-cookie"; +import { useState, useEffect } from "react"; +import { deleteAPKey } from "@/utils/userActions"; +import AddAPIKey from "./AddAPIKey"; +import { formatDateTime } from "@/utils/userFuncs"; + +type Items = { + id: number; + apiKey: string; + user: string; + entry_created_at: string; +}; + +const APIKeyTable: React.FC = () => { + const [items, setItems] = useState([]); + const [errorStatus, setErrorStatus] = useState<"error" | "success">("error"); + const [errorMessage, setErrorMessage] = useState(""); + const [errorDsc, setErrorDsc] = useState(""); + const [isError, setIsError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [reload, setReload] = useState(false); + const [addAPIForm, setAddAPIForm] = useState(false); + + const setError = ( + status: "error" | "success", + message: string, + description: string + ) => { + setIsError(false); + setErrorStatus(status); + setErrorMessage(message); + setErrorDsc(description); + setIsError(true); + }; + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + try { + const response = await fetch("http://localhost:8002/api/apiKeys", { + method: "GET", + headers: { + Authorization: `Bearer ${Cookies.get("token")}`, + }, + }); + const data = await response.json(); + return data; + } catch (error) { + setError("error", "Failed to fetch items", "There is an error"); + } finally { + setIsLoading(false); + } + }; + fetchData().then((data) => { + if (Array.isArray(data)) { + setItems(data); + } + }); + }, [reload]); + + return ( + <> + {/* Action toolbar */} + + + setReload(!reload)} + > + + + + + + + + + {/* End action toolbar */} + + + Gegenstände + + {isError && ( + + )} + {isLoading && ( + + + Loading... + + )} + {addAPIForm && ( + { + setAddAPIForm(false); + setReload(!reload); + }} + alert={setError} + /> + )} + + + + + + # + + + API Key + + + Benutzer + + + Eintrag erstellt am + + + Aktionen + + + + + {items.map((apiKey) => ( + + {apiKey.id} + {apiKey.apiKey} + {apiKey.user} + {formatDateTime(apiKey.entry_created_at)} + + + + + ))} + + + + ); +}; + +export default APIKeyTable; diff --git a/admin/src/components/AddAPIKey.tsx b/admin/src/components/AddAPIKey.tsx new file mode 100644 index 0000000..c710397 --- /dev/null +++ b/admin/src/components/AddAPIKey.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { Button, Card, Field, Input, Stack } from "@chakra-ui/react"; +import { createAPIentry } from "@/utils/userActions"; + +type AddAPIKeyProps = { + onClose: () => void; + alert: ( + status: "success" | "error", + message: string, + description: string + ) => void; +}; + +const AddAPIKey: React.FC = ({ onClose, alert }) => { + return ( +
+ + + Neuen API Key erstellen + + Füllen Sie das folgende Formular aus, um einen API Key zu erstellen. + + + + + + API key + + + + Benutzer + + + + + + + + + +
+ ); +}; + +export default AddAPIKey; diff --git a/admin/src/utils/userActions.ts b/admin/src/utils/userActions.ts index a4b3043..93e6ec9 100644 --- a/admin/src/utils/userActions.ts +++ b/admin/src/utils/userActions.ts @@ -213,3 +213,44 @@ export const changeSafeState = async (itemId: number) => { return { success: false }; } }; + +export const createAPIentry = async (apiKey: string, user: string) => { + try { + const response = await fetch(`http://localhost:8002/api/createAPIentry`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Cookies.get("token")}`, + }, + body: JSON.stringify({ apiKey, user }), + }); + if (!response.ok) { + throw new Error("Failed to create API entry"); + } + return { success: true }; + } catch (error) { + console.error("Error creating API entry:", error); + return { success: false }; + } +}; + +export const deleteAPKey = async (apiKeyId: number) => { + try { + const response = await fetch( + `http://localhost:8002/api/deleteAPKey/${apiKeyId}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${Cookies.get("token")}`, + }, + } + ); + if (!response.ok) { + throw new Error("Failed to delete API key"); + } + return { success: true }; + } catch (error) { + console.error("Error deleting API key:", error); + return { success: false }; + } +}; diff --git a/backend/routes/api.js b/backend/routes/api.js index 2b08eed..05c6536 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -22,6 +22,9 @@ import { changeUserPasswordFRONTEND, changeInSafeStateV2, updateItemByID, + getAllApiKeys, + createAPIentry, + deleteAPKey, } from "../services/database.js"; import { authenticate, generateToken } from "../services/tokenService.js"; const router = express.Router(); @@ -330,4 +333,66 @@ router.put("/changeSafeState/:itemId", authenticate, async (req, res) => { return res.status(500).json({ message: "Failed to update item safe state" }); }); +router.get("/apiKeys", authenticate, async (req, res) => { + const result = await getAllApiKeys(); + if (result.success) { + return res.status(200).json(result.data); + } + return res.status(500).json({ message: "Failed to fetch API keys" }); +}); + +router.delete("/deleteAPKey/:id", authenticate, async (req, res) => { + const apiKeyId = req.params.id; + const result = await deleteAPKey(apiKeyId); + if (result.success) { + return res.status(200).json({ message: "API key deleted successfully" }); + } + return res.status(500).json({ message: "Failed to delete API key" }); +}); + +router.post("/createAPIentry", authenticate, async (req, res) => { + const apiKey = req.body.apiKey; + const user = req.body.user; + if (!apiKey || !user) { + return res.status(400).json({ message: "API key and user are required" }); + } + + // Ensure apiKey is a number + const apiKeyNum = Number(apiKey); + if (!Number.isFinite(apiKeyNum)) { + return res.status(400).json({ message: "API key must be a number" }); + } + + const result = await createAPIentry(apiKeyNum, user); + if (result.success) { + return res.status(201).json({ message: "API key created successfully" }); + } + if (result.code === "DUPLICATE") { + return res.status(409).json({ message: "API key already exists" }); + } + return res.status(500).json({ message: "Failed to create API key" }); +}); + +router.get("/apiKeys/validate/:key", async (req, res) => { + try { + const rawKey = req.params.key; + const result = await getAllApiKeys(); + if (!result.success || !Array.isArray(result.data)) { + return res.status(500).json({ valid: false }); + } + + const isValid = result.data.some((entry) => { + const val = String( + entry?.key ?? entry?.apiKey ?? entry?.api_key ?? entry + ); + return val === String(rawKey); + }); + + return res.status(200).json({ valid: isValid }); + } catch (err) { + console.error("validate api key error:", err); + return res.status(500).json({ valid: false }); + } +}); + export default router; diff --git a/backend/routes/apiV2.js b/backend/routes/apiV2.js index b43c02e..15f994d 100644 --- a/backend/routes/apiV2.js +++ b/backend/routes/apiV2.js @@ -7,30 +7,64 @@ import { setTakeDateV2, getLoanByCodeV2, getAllLoansV2, + getAPIkey, } from "../services/database.js"; dotenv.config(); const router = express.Router(); -// Route for API to get ALL items from the database -router.get("/items/:key", async (req, res) => { - if (req.params.key === process.env.ADMIN_ID) { - const result = await getItemsFromDatabaseV2(); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(500).json({ message: "Failed to fetch items" }); +async function validateAPIKey(apiKey) { + try { + if (!apiKey) return false; + const result = await getAPIkey(); + if (!result?.success || !Array.isArray(result.data)) return false; + return result.data.some((row) => String(row.apiKey) === String(apiKey)); + } catch (err) { + console.error("validateAPIKey error:", err); + return false; + } +} + +// Add a guard that returns Access Denied instead of hanging +const apiKeyGuard = async (req, res, next) => { + try { + const key = req.params.key; + if (!key) { + return res + .status(401) + .json({ message: "Access denied: missing API key" }); } + const ok = await validateAPIKey(key); + if (!ok) { + return res + .status(401) + .json({ message: "Access denied: invalid API key" }); + } + next(); + } catch (e) { + console.error("apiKeyGuard error:", e); + res.status(500).json({ message: "Internal server error" }); + } +}; + +// Route for API to get ALL items from the database +router.get("/items/:key", apiKeyGuard, async (req, res) => { + const result = await getItemsFromDatabaseV2(); + if (result.success) { + res.status(200).json({ data: result.data }); } else { - res.status(403).json({ message: "Access denied" }); + res.status(500).json({ message: "Failed to fetch items" }); } }); // Route for API to control the position of an item -router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => { - if (req.params.key === process.env.ADMIN_ID) { +router.post( + "/controlInSafe/:key/:itemId/:state", + apiKeyGuard, + async (req, res) => { const itemId = req.params.itemId; const state = req.params.state; + if (state === "1" || state === "0") { const result = await changeInSafeStateV2(itemId, state); if (result.success) { @@ -41,59 +75,44 @@ router.post("/controlInSafe/:key/:itemId/:state", async (req, res) => { } else { res.status(400).json({ message: "Invalid state value" }); } - } else { - res.status(403).json({ message: "Access denied" }); } -}); +); // Route for API to get a loan by its code -router.get("/getLoanByCode/:key/:loan_code", async (req, res) => { - if (req.params.key === process.env.ADMIN_ID) { - const loan_code = req.params.loan_code; - - const result = await getLoanByCodeV2(loan_code); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(404).json({ message: "Loan not found" }); - } +router.get("/getLoanByCode/:key/:loan_code", apiKeyGuard, async (req, res) => { + const loan_code = req.params.loan_code; + const result = await getLoanByCodeV2(loan_code); + if (result.success) { + res.status(200).json({ data: result.data }); + } else { + res.status(404).json({ message: "Loan not found" }); } }); // Route for API to set the return date by the loan code -router.post("/setReturnDate/:key/:loan_code", async (req, res) => { - if (req.params.key === process.env.ADMIN_ID) { - const loanCode = req.params.loan_code; - - const result = await setReturnDateV2(loanCode); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(500).json({ message: "Failed to set return date" }); - } +router.post("/setReturnDate/:key/:loan_code", apiKeyGuard, async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setReturnDateV2(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); } else { - res.status(403).json({ message: "Access denied" }); + res.status(500).json({ message: "Failed to set return date" }); } }); // Route for API to set the take away date by the loan code -router.post("/setTakeDate/:key/:loan_code", async (req, res) => { - if (req.params.key === process.env.ADMIN_ID) { - const loanCode = req.params.loan_code; - - const result = await setTakeDateV2(loanCode); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(500).json({ message: "Failed to set take date" }); - } +router.post("/setTakeDate/:key/:loan_code", apiKeyGuard, async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setTakeDateV2(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); } else { - res.status(403).json({ message: "Access denied" }); + res.status(500).json({ message: "Failed to set take date" }); } }); // Route for API to get ALL loans from the database without sensitive info -router.get("/allLoans", async (req, res) => { +router.get("/allLoans/:key", apiKeyGuard, async (req, res) => { const result = await getAllLoansV2(); if (result.success) { return res.status(200).json(result.data); @@ -101,8 +120,8 @@ router.get("/allLoans", async (req, res) => { return res.status(500).json({ message: "Failed to fetch loans" }); }); -// Route for API to get ALL items form the database without key -router.get("/allItems", async (req, res) => { +// Route for API to get ALL items form the database +router.get("/allItems/:key", apiKeyGuard, async (req, res) => { const result = await getItemsFromDatabaseV2(); if (result.success) { res.status(200).json(result.data); diff --git a/backend/scheme.sql b/backend/scheme.sql index c7b368b..505c1b6 100644 --- a/backend/scheme.sql +++ b/backend/scheme.sql @@ -58,6 +58,15 @@ CREATE TABLE `lockers` ( UNIQUE KEY `locker_number` (`locker_number`) ); +CREATE TABLE `apiKeys` ( + `id` int NOT NULL AUTO_INCREMENT, + `apiKey` int NOT NULL UNIQUE, + `user` VARCHAR(255) NOT NULL, + `entry_created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `apiKey` (`apiKey`) +); + INSERT INTO `items` (`item_name`, `can_borrow_role`, `inSafe`) VALUES ('DJI 1er Mikro', 4, 1), ('DJI 2er Mikro 1', 4, 1), diff --git a/backend/services/database.js b/backend/services/database.js index a102c2d..e42ca66 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -458,3 +458,36 @@ export const getAllLoansV2 = async () => { } return { success: false }; }; + +export const getAllApiKeys = async () => { + const [rows] = await pool.query("SELECT * FROM apiKeys"); + if (rows.length > 0) { + return { success: true, data: rows }; + } + return { success: false }; +}; + +export const createAPIentry = async (apiKey, user) => { + const [result] = await pool.query( + "INSERT INTO apiKeys (apiKey, user) VALUES (?, ?)", + [apiKey, user] + ); + if (result.affectedRows > 0) return { success: true }; + return { success: false }; +}; + +export const deleteAPKey = async (apiKeyId) => { + const [result] = await pool.query("DELETE FROM apiKeys WHERE id = ?", [ + apiKeyId, + ]); + if (result.affectedRows > 0) return { success: true }; + return { success: false }; +}; + +export const getAPIkey = async () => { + const [rows] = await pool.query("SELECT apiKey FROM apiKeys"); + if (rows.length > 0) { + return { success: true, data: rows }; + } + return { success: false }; +};