Loading src/app/Admin/APIKEY/ApiKeyListClient.tsx 0 → 100644 +227 −0 Original line number Diff line number Diff line 'use client'; import { useEffect, useRef, useState } from "react"; import { deleteApiKey, saveEnabledApiKey } from "./actions"; import { useRouter } from "next/navigation"; export default function ApiKeyListClient({ data }: { data: any }) { const router = useRouter(); const ApiKeys = Array.isArray(data?.apikeys) ? data.apikeys : []; const [selectedApiKeys, setselectedApiKeys] = useState<Record<number, boolean>>({}); const handleSelect = (id: number, checked: boolean) => { setselectedApiKeys((prev) => ({ ...prev, [id]: checked })); }; const [enabledStates, setEnabledStates] = useState<Record<number, boolean>>(() => { const state: Record<number, boolean> = {}; ApiKeys.forEach((ApiKey: any) => { state[ApiKey.id] = !!ApiKey.is_valid; }); return state; }); const lastToggled = useRef<{ id: number; value: boolean } | null>(null); useEffect(() => { if (lastToggled.current) { const toggledId = lastToggled.current.id; const apiKeysExists = ApiKeys.some((ApiKey: any) => ApiKey.id === toggledId); if (!apiKeysExists) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: `Api Key with id ${toggledId} not found.`, type: 'danger' }]); lastToggled.current = null; return; } (async () => { try { await saveEnabledApiKey(toggledId, lastToggled.current!.value); setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: `Api Key ${toggledId} updated successfully.`, type: 'success' }]); } catch (err: any) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: err.message || `Failed to update api key ${toggledId}.`, type: 'danger' }]); } finally { lastToggled.current = null; } })(); } }, [enabledStates]); const handleToggle = (id: number) => { setEnabledStates((prev) => { const updated = { ...prev, [id]: !prev[id] }; lastToggled.current = { id, value: updated[id] }; return updated; }); }; const [showDeleteModal, setShowDeleteModal] = useState(false); // Toasts state for multiple simultaneous toasts const [toasts, setToasts] = useState<Array<{ id: number; message: string; type: 'success' | 'danger' }>>([]); const confirmDelete = async () => { const ApiKeyIds = Object.keys(selectedApiKeys).filter(id => selectedApiKeys[Number(id)]); if (ApiKeyIds.length === 0) { console.warn("No ApiKeys selected for deletion."); return; } setShowDeleteModal(false); console.log("Confirmed deletion for Api Key IDs:", ApiKeyIds); try { await Promise.all(ApiKeyIds.map(id => deleteApiKey(Number(id)))); setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Selected Api Key deleted successfully.', type: 'success' }]); setselectedApiKeys({}); router.refresh(); } catch (err) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Failed to delete selected languages.', type: 'danger' }]); setselectedApiKeys({}); } }; const handleDelete = async () => { const ApiKeyIds = Object.keys(selectedApiKeys).filter(id => selectedApiKeys[Number(id)]); if (ApiKeyIds.length === 0) { console.warn("No ApiKeys selected for deletion."); return; } setShowDeleteModal(true); console.log("Selected Api Key IDs for deletion:", ApiKeyIds); }; // Auto-hide each toast after 3 seconds useEffect(() => { if (toasts.length > 0) { const timers = toasts.map((toast) => setTimeout(() => { setToasts(ts => ts.filter(t => t.id !== toast.id)); }, 3000) ); return () => timers.forEach(timer => clearTimeout(timer)); } }, [toasts]); if (!Array.isArray(ApiKeys) || ApiKeys.length === 0) { return ( <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 apikey-list-container"> <h2 className="text-2xl font-semibold mb-4">User</h2> <div>No Api Key found.</div> </div> ); } console.log("ApiKeys:", ApiKeys); return ( <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 apikey-list-container"> <h2 className="text-2xl font-semibold mb-4">API Key List</h2> <table className="table table-striped"> <thead> <tr> <th></th> <th>ID</th> <th>label</th> <th>Api Key</th> <th>Is Valid</th> <th>Valid Until</th> <th>last usage</th> <th>Edit</th> </tr> </thead> <tbody> {ApiKeys && ApiKeys.map((apikey: any) => ( <tr key={apikey.id}> <td> <input type="checkbox" className="form-check-input" checked={!!selectedApiKeys[apikey.id]} onChange={e => handleSelect(apikey.id, e.target.checked)} /> </td> <td>{apikey.id}</td> <td>{apikey.label}</td> <td>{apikey.api_key}</td> <td> <div className="form-check form-switch"> <input className="form-check-input" type="checkbox" role="switch" id={`enabled-switch-${apikey.id}`} checked={!!enabledStates[apikey.id]} onChange={() => handleToggle(apikey.id)} /> </div> </td> <td>{apikey.valid_until}</td> <td>{apikey.last_usage}</td> <td> <a href={`/Admin/APIKEY/edit/${apikey.id}`} className="btn btn-primary"> Edit </a> </td> </tr> ))} </tbody> </table> {/* Action buttons at the bottom of the grid */} <div className="d-flex justify-content-end gap-2 mt-3"> <button className="btn btn-danger" style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 24 }} title="Delete" onClick={handleDelete} disabled={Object.keys(selectedApiKeys).length === 0} > <i className="bi bi-trash"></i> </button> <a href="/Admin/APIKEY/edit/new" className="btn btn-primary" style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 28 }} title="Add"> <i className="bi bi-plus"></i> </a> </div> {/* Delete Confirmation Modal */} <div className={`modal fade${showDeleteModal ? ' show d-block' : ''}`} tabIndex={-1} role="dialog" style={{ background: showDeleteModal ? 'rgba(0,0,0,0.5)' : undefined }}> <div className="modal-dialog" role="document"> <div className="modal-content"> <div className="modal-header"> <h5 className="modal-title">Confirm Delete</h5> <button type="button" className="btn-close" aria-label="Close" onClick={() => setShowDeleteModal(false)}></button> </div> <div className="modal-body"> <p>Are you sure you want to delete {Object.values(selectedApiKeys).map(v => !v).length} API key(s)? This action cannot be undone.</p> </div> <div className="modal-footer"> <button type="button" className="btn btn-secondary" onClick={() => setShowDeleteModal(false)}>Cancel</button> <button type="button" className="btn btn-danger" onClick={confirmDelete}>Delete</button> </div> </div> </div> </div> {/* Toast messages (multiple, simultaneous) */} <div style={{ position: 'fixed', top: 20, right: 20, zIndex: 2000 }}> {toasts.map((toast) => ( <div key={toast.id} className={`toast align-items-center text-bg-${toast.type} border-0 mb-2 show`} role="alert" aria-live="assertive" aria-atomic="true" style={{ minWidth: 250 }} > <div className="d-flex"> <div className="toast-body">{toast.message}</div> <button type="button" className="btn-close btn-close-white me-2 m-auto" aria-label="Close" onClick={() => setToasts(ts => ts.filter(t => t.id !== toast.id))}></button> </div> </div> ))} </div> </div> ); } No newline at end of file src/app/Admin/APIKEY/actions.ts 0 → 100644 +56 −0 Original line number Diff line number Diff line "use server"; import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; export async function deleteApiKey(id: number) { console.log("Deleting ApiKey:", id); const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await res.json(); if (!res.ok) { console.error("Error deleting ApiKey:", data); throw new Error(`Failed to delete ApiKey: ${JSON.stringify(data)}`); } revalidatePath("/Admin/APIKEY"); } export async function saveEnabledApiKey(id: number, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; console.log("Saving ApiKey:", id); console.log(JSON.stringify({ is_valid })); const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ is_valid }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to save ApiKey: ${errorText}`); } revalidatePath("/Admin/APIKEY"); } src/app/Admin/APIKEY/edit/[id]/ApiKeyEditPageClient.css 0 → 100644 +43 −0 Original line number Diff line number Diff line .copy-button { position: relative; display: inline-block; cursor: pointer; padding: 10px; border-radius: 5px; } .copy-button:hover .tooltiptext { visibility: visible; display: initial; opacity: 1; } .tooltiptext { visibility: hidden; display: none; position: absolute; width: 150px; top: -40px; left: -56px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px; z-index: 1; opacity: 0; transition: opacity 0.3s; } .tooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } No newline at end of file src/app/Admin/APIKEY/edit/[id]/ApiKeyEditPageClient.tsx 0 → 100644 +180 −0 Original line number Diff line number Diff line "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { saveAPIKey, saveNewAPIKey } from "./actions"; import "./ApiKeyEditPageClient.css"; export default function ApiKeyEditPageClient({ data }: { data: { id: number, api_key: string, is_valid: boolean, label: string, last_usage: Date | null, valid_until: Date } }) { const router = useRouter(); const [is_valid, setIsValid] = useState(!!data.is_valid); const [ApiKeyID, setApiKeyID] = useState(data.id); const [label, setLabel] = useState(data.label || ""); const [valid_until, setValidUntil] = useState(new Date(data.valid_until)); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState(false); const [NewAPIKEY, setNewAPIKEY] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(false); setLoading(true); if (!valid_until) { setError("Valid until can't be empty"); setLoading(false); return; } try { if (ApiKeyID === 0) { const { id, api_key } = await saveNewAPIKey(label, valid_until, is_valid); setNewAPIKEY(api_key); setApiKeyID(id); /*const id = await saveNewUser(login, password, is_active, force_jwt_reconnect); setTimeout(() => router.push(`/Admin/User/edit/${id}`), 1000);*/ } else { await saveAPIKey(ApiKeyID, label, valid_until, is_valid); //if it was a new API key, you stay on the crete page (to have time to copy the key) //if from the create page , you save again, it is now an edit and //it is considered that you had time to save the Key if (NewAPIKEY != "" && ApiKeyID !== 0) { router.push(`/Admin/APIKEY/edit/${ApiKeyID}`); } } setSuccess(true); //setTimeout(() => router.push("/Admin/Language"), 1000); } catch (err: any) { setError(err.message || "Failed to update Api Key"); } finally { setLoading(false); } setLoading(false); }; return <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100" > <Link href="/Admin/APIKEY" className="mb-3 align-self-start text-decoration-none"> ← Back to Api Key List </Link> {ApiKeyID !== 0 ? ( <h2>Edit Api Key {ApiKeyID}</h2> ) : ( <h2>Add New Api Key</h2> )} <form onSubmit={handleSubmit}> {ApiKeyID !== 0 && data.api_key && ( <div className="mb-3"> <label className="form-label">Api Key</label> <div> {data.api_key} </div> </div> )} {ApiKeyID !== 0 && data.last_usage && ( <div className="mb-3"> <label className="form-label">Last Connection</label> <div> {new Date(data.last_usage).toISOString()} </div> </div> )} <div className="mb-3"> <label htmlFor="label" className="form-label">label</label> <input id="label" className="form-control" value={label} onChange={e => setLabel(e.target.value)} /> </div> <div className="form-check form-switch mb-3"> <label className="form-check-label" htmlFor="is_valid"> Is Valid </label> <input className="form-check-input" role="switch" type="checkbox" id="is_valid" checked={is_valid} onChange={e => setIsValid(e.target.checked)} /> </div> <div className="mb-3"> <label className="form-label" htmlFor="is_valid"> Valid Until </label> <input className="form-control" type="date" id="valid_until" value={valid_until ? new Date(valid_until).toISOString().split('T')[0] : ""} required /* @ts-ignore */ onChange={e => setValidUntil(e.target.value ? new Date(e.target.value) : "")} /> </div> <button className="btn btn-primary" type="submit" disabled={loading}> Save </button> {error && <div className="alert alert-danger mt-2">{error}</div>} {success && <div className="alert alert-success mt-2">Saved!</div>} {NewAPIKEY && ( <div className="alert alert-success mt-2"> Your new API Key is <div className="fw-bold ms-2">{NewAPIKEY} <button type="button" className="btn btn-outline-secondary btn-sm ms-2 copy-button" title="Copy API Key" onClick={() => { navigator.clipboard.writeText(NewAPIKEY); let tooltip = document.getElementById("myTooltip"); if (tooltip) { tooltip.innerHTML = "Copied"; setTimeout(() => { tooltip.innerHTML = "Copy to clipboard"; }, 2000); } }} style={{ verticalAlign: "middle" }} > <span className="tooltiptext" id="myTooltip">Copy to clipboard</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-clipboard" viewBox="0 0 16 16"> <path d="M10 1.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1A1.5 1.5 0 0 0 4.5 3H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-.5A1.5 1.5 0 0 0 10 1.5zm-4 0A.5.5 0 0 1 6.5 1h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1z" /> </svg> </button> </div> <br /> Please save it, you won't be able to see it again. </div> )} </form> </div> } No newline at end of file src/app/Admin/APIKEY/edit/[id]/actions.ts 0 → 100644 +77 −0 Original line number Diff line number Diff line "use server"; import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { randomBytes } from 'crypto'; function generateApiKey(length: number = 32): string { return randomBytes(length).toString('hex'); // 64-character hex string } //YYYY-MM-DD HH:MM:SS function formatDate(date: Date): string { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' ' + "23:59:59"; // Set time to end of the day } export async function saveAPIKey(id: number, label: string, valid_until: Date, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; console.log(formatDate(valid_until)); console.log("Saving Api KEy:", id); console.log(JSON.stringify({ id, label, valid_until: formatDate(valid_until), is_valid })); const body = JSON.stringify({ label, valid_until: formatDate(valid_until), is_valid }); const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: body, }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to edit Api Key: ${errorText}`); } revalidatePath("/Admin/APIKEY/edit/" + id); } export async function saveNewAPIKey(label: string, valid_until: Date, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; const api_key = generateApiKey(); console.log(formatDate(valid_until)); console.log("Saving new Api Key:"); console.log(JSON.stringify({ api_key, label, valid_until: formatDate(valid_until), is_valid })); const res = await fetch(`${process.env.API_URL}/apikeys`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ api_key, label, valid_until: formatDate(valid_until), is_valid }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to create Api Key: ${errorText}`); } else { const id = await res.json().then(data => data.api_key_id); revalidatePath("/Admin/APIKEY/edit/" + id); return { id, api_key }; } } No newline at end of file Loading
src/app/Admin/APIKEY/ApiKeyListClient.tsx 0 → 100644 +227 −0 Original line number Diff line number Diff line 'use client'; import { useEffect, useRef, useState } from "react"; import { deleteApiKey, saveEnabledApiKey } from "./actions"; import { useRouter } from "next/navigation"; export default function ApiKeyListClient({ data }: { data: any }) { const router = useRouter(); const ApiKeys = Array.isArray(data?.apikeys) ? data.apikeys : []; const [selectedApiKeys, setselectedApiKeys] = useState<Record<number, boolean>>({}); const handleSelect = (id: number, checked: boolean) => { setselectedApiKeys((prev) => ({ ...prev, [id]: checked })); }; const [enabledStates, setEnabledStates] = useState<Record<number, boolean>>(() => { const state: Record<number, boolean> = {}; ApiKeys.forEach((ApiKey: any) => { state[ApiKey.id] = !!ApiKey.is_valid; }); return state; }); const lastToggled = useRef<{ id: number; value: boolean } | null>(null); useEffect(() => { if (lastToggled.current) { const toggledId = lastToggled.current.id; const apiKeysExists = ApiKeys.some((ApiKey: any) => ApiKey.id === toggledId); if (!apiKeysExists) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: `Api Key with id ${toggledId} not found.`, type: 'danger' }]); lastToggled.current = null; return; } (async () => { try { await saveEnabledApiKey(toggledId, lastToggled.current!.value); setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: `Api Key ${toggledId} updated successfully.`, type: 'success' }]); } catch (err: any) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: err.message || `Failed to update api key ${toggledId}.`, type: 'danger' }]); } finally { lastToggled.current = null; } })(); } }, [enabledStates]); const handleToggle = (id: number) => { setEnabledStates((prev) => { const updated = { ...prev, [id]: !prev[id] }; lastToggled.current = { id, value: updated[id] }; return updated; }); }; const [showDeleteModal, setShowDeleteModal] = useState(false); // Toasts state for multiple simultaneous toasts const [toasts, setToasts] = useState<Array<{ id: number; message: string; type: 'success' | 'danger' }>>([]); const confirmDelete = async () => { const ApiKeyIds = Object.keys(selectedApiKeys).filter(id => selectedApiKeys[Number(id)]); if (ApiKeyIds.length === 0) { console.warn("No ApiKeys selected for deletion."); return; } setShowDeleteModal(false); console.log("Confirmed deletion for Api Key IDs:", ApiKeyIds); try { await Promise.all(ApiKeyIds.map(id => deleteApiKey(Number(id)))); setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Selected Api Key deleted successfully.', type: 'success' }]); setselectedApiKeys({}); router.refresh(); } catch (err) { setToasts(ts => [...ts, { id: Date.now() + Math.random(), message: 'Failed to delete selected languages.', type: 'danger' }]); setselectedApiKeys({}); } }; const handleDelete = async () => { const ApiKeyIds = Object.keys(selectedApiKeys).filter(id => selectedApiKeys[Number(id)]); if (ApiKeyIds.length === 0) { console.warn("No ApiKeys selected for deletion."); return; } setShowDeleteModal(true); console.log("Selected Api Key IDs for deletion:", ApiKeyIds); }; // Auto-hide each toast after 3 seconds useEffect(() => { if (toasts.length > 0) { const timers = toasts.map((toast) => setTimeout(() => { setToasts(ts => ts.filter(t => t.id !== toast.id)); }, 3000) ); return () => timers.forEach(timer => clearTimeout(timer)); } }, [toasts]); if (!Array.isArray(ApiKeys) || ApiKeys.length === 0) { return ( <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 apikey-list-container"> <h2 className="text-2xl font-semibold mb-4">User</h2> <div>No Api Key found.</div> </div> ); } console.log("ApiKeys:", ApiKeys); return ( <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100 apikey-list-container"> <h2 className="text-2xl font-semibold mb-4">API Key List</h2> <table className="table table-striped"> <thead> <tr> <th></th> <th>ID</th> <th>label</th> <th>Api Key</th> <th>Is Valid</th> <th>Valid Until</th> <th>last usage</th> <th>Edit</th> </tr> </thead> <tbody> {ApiKeys && ApiKeys.map((apikey: any) => ( <tr key={apikey.id}> <td> <input type="checkbox" className="form-check-input" checked={!!selectedApiKeys[apikey.id]} onChange={e => handleSelect(apikey.id, e.target.checked)} /> </td> <td>{apikey.id}</td> <td>{apikey.label}</td> <td>{apikey.api_key}</td> <td> <div className="form-check form-switch"> <input className="form-check-input" type="checkbox" role="switch" id={`enabled-switch-${apikey.id}`} checked={!!enabledStates[apikey.id]} onChange={() => handleToggle(apikey.id)} /> </div> </td> <td>{apikey.valid_until}</td> <td>{apikey.last_usage}</td> <td> <a href={`/Admin/APIKEY/edit/${apikey.id}`} className="btn btn-primary"> Edit </a> </td> </tr> ))} </tbody> </table> {/* Action buttons at the bottom of the grid */} <div className="d-flex justify-content-end gap-2 mt-3"> <button className="btn btn-danger" style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 24 }} title="Delete" onClick={handleDelete} disabled={Object.keys(selectedApiKeys).length === 0} > <i className="bi bi-trash"></i> </button> <a href="/Admin/APIKEY/edit/new" className="btn btn-primary" style={{ borderRadius: "50%", width: 56, height: 56, fontSize: 28 }} title="Add"> <i className="bi bi-plus"></i> </a> </div> {/* Delete Confirmation Modal */} <div className={`modal fade${showDeleteModal ? ' show d-block' : ''}`} tabIndex={-1} role="dialog" style={{ background: showDeleteModal ? 'rgba(0,0,0,0.5)' : undefined }}> <div className="modal-dialog" role="document"> <div className="modal-content"> <div className="modal-header"> <h5 className="modal-title">Confirm Delete</h5> <button type="button" className="btn-close" aria-label="Close" onClick={() => setShowDeleteModal(false)}></button> </div> <div className="modal-body"> <p>Are you sure you want to delete {Object.values(selectedApiKeys).map(v => !v).length} API key(s)? This action cannot be undone.</p> </div> <div className="modal-footer"> <button type="button" className="btn btn-secondary" onClick={() => setShowDeleteModal(false)}>Cancel</button> <button type="button" className="btn btn-danger" onClick={confirmDelete}>Delete</button> </div> </div> </div> </div> {/* Toast messages (multiple, simultaneous) */} <div style={{ position: 'fixed', top: 20, right: 20, zIndex: 2000 }}> {toasts.map((toast) => ( <div key={toast.id} className={`toast align-items-center text-bg-${toast.type} border-0 mb-2 show`} role="alert" aria-live="assertive" aria-atomic="true" style={{ minWidth: 250 }} > <div className="d-flex"> <div className="toast-body">{toast.message}</div> <button type="button" className="btn-close btn-close-white me-2 m-auto" aria-label="Close" onClick={() => setToasts(ts => ts.filter(t => t.id !== toast.id))}></button> </div> </div> ))} </div> </div> ); } No newline at end of file
src/app/Admin/APIKEY/actions.ts 0 → 100644 +56 −0 Original line number Diff line number Diff line "use server"; import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; export async function deleteApiKey(id: number) { console.log("Deleting ApiKey:", id); const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }); const data = await res.json(); if (!res.ok) { console.error("Error deleting ApiKey:", data); throw new Error(`Failed to delete ApiKey: ${JSON.stringify(data)}`); } revalidatePath("/Admin/APIKEY"); } export async function saveEnabledApiKey(id: number, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; console.log("Saving ApiKey:", id); console.log(JSON.stringify({ is_valid })); const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ is_valid }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to save ApiKey: ${errorText}`); } revalidatePath("/Admin/APIKEY"); }
src/app/Admin/APIKEY/edit/[id]/ApiKeyEditPageClient.css 0 → 100644 +43 −0 Original line number Diff line number Diff line .copy-button { position: relative; display: inline-block; cursor: pointer; padding: 10px; border-radius: 5px; } .copy-button:hover .tooltiptext { visibility: visible; display: initial; opacity: 1; } .tooltiptext { visibility: hidden; display: none; position: absolute; width: 150px; top: -40px; left: -56px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px; z-index: 1; opacity: 0; transition: opacity 0.3s; } .tooltiptext::after { content: ""; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: #555 transparent transparent transparent; } No newline at end of file
src/app/Admin/APIKEY/edit/[id]/ApiKeyEditPageClient.tsx 0 → 100644 +180 −0 Original line number Diff line number Diff line "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { saveAPIKey, saveNewAPIKey } from "./actions"; import "./ApiKeyEditPageClient.css"; export default function ApiKeyEditPageClient({ data }: { data: { id: number, api_key: string, is_valid: boolean, label: string, last_usage: Date | null, valid_until: Date } }) { const router = useRouter(); const [is_valid, setIsValid] = useState(!!data.is_valid); const [ApiKeyID, setApiKeyID] = useState(data.id); const [label, setLabel] = useState(data.label || ""); const [valid_until, setValidUntil] = useState(new Date(data.valid_until)); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [success, setSuccess] = useState(false); const [NewAPIKEY, setNewAPIKEY] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(false); setLoading(true); if (!valid_until) { setError("Valid until can't be empty"); setLoading(false); return; } try { if (ApiKeyID === 0) { const { id, api_key } = await saveNewAPIKey(label, valid_until, is_valid); setNewAPIKEY(api_key); setApiKeyID(id); /*const id = await saveNewUser(login, password, is_active, force_jwt_reconnect); setTimeout(() => router.push(`/Admin/User/edit/${id}`), 1000);*/ } else { await saveAPIKey(ApiKeyID, label, valid_until, is_valid); //if it was a new API key, you stay on the crete page (to have time to copy the key) //if from the create page , you save again, it is now an edit and //it is considered that you had time to save the Key if (NewAPIKEY != "" && ApiKeyID !== 0) { router.push(`/Admin/APIKEY/edit/${ApiKeyID}`); } } setSuccess(true); //setTimeout(() => router.push("/Admin/Language"), 1000); } catch (err: any) { setError(err.message || "Failed to update Api Key"); } finally { setLoading(false); } setLoading(false); }; return <div className="container container-fluid d-flex flex-column align-items-center justify-content-center vh-100" > <Link href="/Admin/APIKEY" className="mb-3 align-self-start text-decoration-none"> ← Back to Api Key List </Link> {ApiKeyID !== 0 ? ( <h2>Edit Api Key {ApiKeyID}</h2> ) : ( <h2>Add New Api Key</h2> )} <form onSubmit={handleSubmit}> {ApiKeyID !== 0 && data.api_key && ( <div className="mb-3"> <label className="form-label">Api Key</label> <div> {data.api_key} </div> </div> )} {ApiKeyID !== 0 && data.last_usage && ( <div className="mb-3"> <label className="form-label">Last Connection</label> <div> {new Date(data.last_usage).toISOString()} </div> </div> )} <div className="mb-3"> <label htmlFor="label" className="form-label">label</label> <input id="label" className="form-control" value={label} onChange={e => setLabel(e.target.value)} /> </div> <div className="form-check form-switch mb-3"> <label className="form-check-label" htmlFor="is_valid"> Is Valid </label> <input className="form-check-input" role="switch" type="checkbox" id="is_valid" checked={is_valid} onChange={e => setIsValid(e.target.checked)} /> </div> <div className="mb-3"> <label className="form-label" htmlFor="is_valid"> Valid Until </label> <input className="form-control" type="date" id="valid_until" value={valid_until ? new Date(valid_until).toISOString().split('T')[0] : ""} required /* @ts-ignore */ onChange={e => setValidUntil(e.target.value ? new Date(e.target.value) : "")} /> </div> <button className="btn btn-primary" type="submit" disabled={loading}> Save </button> {error && <div className="alert alert-danger mt-2">{error}</div>} {success && <div className="alert alert-success mt-2">Saved!</div>} {NewAPIKEY && ( <div className="alert alert-success mt-2"> Your new API Key is <div className="fw-bold ms-2">{NewAPIKEY} <button type="button" className="btn btn-outline-secondary btn-sm ms-2 copy-button" title="Copy API Key" onClick={() => { navigator.clipboard.writeText(NewAPIKEY); let tooltip = document.getElementById("myTooltip"); if (tooltip) { tooltip.innerHTML = "Copied"; setTimeout(() => { tooltip.innerHTML = "Copy to clipboard"; }, 2000); } }} style={{ verticalAlign: "middle" }} > <span className="tooltiptext" id="myTooltip">Copy to clipboard</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-clipboard" viewBox="0 0 16 16"> <path d="M10 1.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1A1.5 1.5 0 0 0 4.5 3H4a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-.5A1.5 1.5 0 0 0 10 1.5zm-4 0A.5.5 0 0 1 6.5 1h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1z" /> </svg> </button> </div> <br /> Please save it, you won't be able to see it again. </div> )} </form> </div> } No newline at end of file
src/app/Admin/APIKEY/edit/[id]/actions.ts 0 → 100644 +77 −0 Original line number Diff line number Diff line "use server"; import { revalidatePath } from "next/cache"; import { cookies } from "next/headers"; import { randomBytes } from 'crypto'; function generateApiKey(length: number = 32): string { return randomBytes(length).toString('hex'); // 64-character hex string } //YYYY-MM-DD HH:MM:SS function formatDate(date: Date): string { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' ' + "23:59:59"; // Set time to end of the day } export async function saveAPIKey(id: number, label: string, valid_until: Date, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; console.log(formatDate(valid_until)); console.log("Saving Api KEy:", id); console.log(JSON.stringify({ id, label, valid_until: formatDate(valid_until), is_valid })); const body = JSON.stringify({ label, valid_until: formatDate(valid_until), is_valid }); const res = await fetch(`${process.env.API_URL}/apikeys/${id}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: body, }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to edit Api Key: ${errorText}`); } revalidatePath("/Admin/APIKEY/edit/" + id); } export async function saveNewAPIKey(label: string, valid_until: Date, is_valid: boolean) { const cookieStore = cookies(); const token = (await cookieStore).get("access_token")?.value; const api_key = generateApiKey(); console.log(formatDate(valid_until)); console.log("Saving new Api Key:"); console.log(JSON.stringify({ api_key, label, valid_until: formatDate(valid_until), is_valid })); const res = await fetch(`${process.env.API_URL}/apikeys`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ api_key, label, valid_until: formatDate(valid_until), is_valid }), }); if (!res.ok) { const errorText = await res.text(); throw new Error(`Failed to create Api Key: ${errorText}`); } else { const id = await res.json().then(data => data.api_key_id); revalidatePath("/Admin/APIKEY/edit/" + id); return { id, api_key }; } } No newline at end of file