Commit 3af90502 authored by Anthony Jacob's avatar Anthony Jacob
Browse files

advance on different section

parent caa7c284
Loading
Loading
Loading
Loading
+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
+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");
}


+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
+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">
            &larr; 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
+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