add: backend api auth by apikey/apikey gen/apikey revoke

This commit is contained in:
h z
2025-05-06 18:54:10 +01:00
parent 1ce2eebbfa
commit 87b4246a9b
5 changed files with 375 additions and 88 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
summerizer.py summerizer.py
node_modules

View File

@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { useCreateApiKey } from '../../utils/queries/apikey-queries';
const AVAILABLE_ROLES = ['guest', 'creator', 'admin'];
const ApiKeyCreationModal = ({ isOpen, onClose }) => {
const [name, setName] = useState('');
const [roles, setRoles] = useState(['guest']);
const [generatedKey, setGeneratedKey] = useState(null);
const createApiKeyMutation = useCreateApiKey();
const handleAddRole = () => {
const availableRoles = AVAILABLE_ROLES.filter(role => !roles.includes(role));
if (availableRoles.length > 0) {
setRoles([...roles, availableRoles[0]]);
}
};
const handleRoleChange = (index, value) => {
if (roles.includes(value) && roles.findIndex(r => r === value) !== index) {
return;
}
const newRoles = [...roles];
newRoles[index] = value;
setRoles(newRoles);
};
const handleRemoveRole = (index) => {
const newRoles = roles.filter((_, i) => i !== index);
setRoles(newRoles);
};
const handleGenerate = async () => {
if (!name.trim()) {
alert('API key name is required');
return;
}
try {
const result = await createApiKeyMutation.mutateAsync({
name: name.trim(),
roles: roles
});
setGeneratedKey(result);
} catch (error) {
console.error('failed to create api key', error);
alert('failed to create api key');
}
};
const handleCopy = () => {
navigator.clipboard.writeText(generatedKey)
.then(() => alert('API key copied to clipboard'))
.catch(err => console.error('failed to copy api key:', err));
};
const getRemainingRoles = (currentIndex) => {
return AVAILABLE_ROLES.filter(role =>
!roles.find((r, i) => r === role && i !== currentIndex)
);
};
if (!isOpen) return null;
return (
<div className="modal is-active">
<div className="modal-background" onClick={onClose}></div>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Create API Key</p>
<button className="delete" aria-label="close" onClick={onClose}></button>
</header>
<section className="modal-card-body">
{!generatedKey ? (
<div>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
placeholder="API key name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
<div className="field">
<label className="label">Roles</label>
{roles.map((role, index) => (
<div key={index} className="field has-addons">
<div className="control">
<div className="select">
<select
value={role}
onChange={(e) => handleRoleChange(index, e.target.value)}
>
{getRemainingRoles(index).map(availableRole => (
<option key={availableRole} value={availableRole}>
{availableRole}
</option>
))}
</select>
</div>
</div>
<div className="control">
<button
className="button is-danger"
onClick={() => handleRemoveRole(index)}
disabled={roles.length === 1}
>
Delete
</button>
</div>
</div>
))}
<button
className="button is-info mt-2"
onClick={handleAddRole}
disabled={roles.length === AVAILABLE_ROLES.length}
>
Add Role
</button>
</div>
</div>
) : (
<div>
<div className="notification is-warning">
Please copy your API key immediately! It will only be displayed once!
</div>
<div className="field">
<label className="label">Your API Key:</label>
<div className="control">
<input
className="input"
type="text"
value={generatedKey.key}
readOnly
/>
</div>
</div>
<button className="button is-info" onClick={handleCopy}>
Copy
</button>
</div>
)}
</section>
<footer className="modal-card-foot">
{!generatedKey && (
<button
className="button is-primary"
onClick={handleGenerate}
disabled={createApiKeyMutation.isLoading || !name.trim()}
>
Generate
</button>
)}
<button className="button" onClick={onClose}>Close</button>
</footer>
</div>
</div>
);
};
export default ApiKeyCreationModal;

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { useRevokeApiKey } from '../../utils/queries/apikey-queries';
const ApiKeyRevokeModal = ({ isOpen, onClose }) => {
const [apiKey, setApiKey] = useState('');
const revokeApiKeyMutation = useRevokeApiKey();
const handleRevoke = async () => {
if (!apiKey.trim()) {
alert('Please enter an API key');
return;
}
try {
await revokeApiKeyMutation.mutateAsync(apiKey);
alert('API key revoked successfully');
onClose();
} catch (error) {
console.error('Failed to revoke API key:', error);
alert('Failed to revoke API key');
}
};
if (!isOpen) return null;
return (
<div className="modal is-active">
<div className="modal-background" onClick={onClose}></div>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Revoke API Key</p>
<button className="delete" aria-label="close" onClick={onClose}></button>
</header>
<section className="modal-card-body">
<div className="field">
<label className="label">API Key</label>
<div className="control">
<input
className="input"
type="text"
placeholder="Enter API key to revoke"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</div>
</div>
</section>
<footer className="modal-card-foot">
<button
className="button is-danger"
onClick={handleRevoke}
disabled={revokeApiKeyMutation.isLoading}
>
Revoke
</button>
<button className="button" onClick={onClose}>Cancel</button>
</footer>
</div>
</div>
);
};
export default ApiKeyRevokeModal;

View File

@@ -4,11 +4,16 @@ import { AuthContext } from "../../AuthProvider";
import "bulma/css/bulma.min.css"; import "bulma/css/bulma.min.css";
import {useConfig} from "../../ConfigProvider"; import {useConfig} from "../../ConfigProvider";
import "./MainNavigation.css"; import "./MainNavigation.css";
import ApiKeyCreationModal from "../Modals/ApiKeyCreationModal";
import ApiKeyRevokeModal from "../Modals/ApiKeyRevokeModal";
const MainNavigation = () => { const MainNavigation = () => {
const { user, login, logout } = useContext(AuthContext); const { user, login, logout } = useContext(AuthContext);
const config = useConfig(); const config = useConfig();
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
const [isRevokeModalOpen, setIsRevokeModalOpen] = useState(false);
if (config===undefined) { if (config===undefined) {
return <div>Loading ...</div>; return <div>Loading ...</div>;
} }
@@ -88,102 +93,124 @@ const MainNavigation = () => {
}; };
return ( return (
<nav className="navbar is-dark" role="navigation" aria-label="main navigation"> <>
<div className="navbar-brand"> <nav className="navbar is-dark" role="navigation" aria-label="main navigation">
<Link className="navbar-item" to="/"> <div className="navbar-brand">
<img src="/icons/logo.png" alt="Logo" style={{ height: "40px", marginRight: "10px" }} /> <Link className="navbar-item" to="/">
Home <img src="/icons/logo.png" alt="Logo" style={{ height: "40px", marginRight: "10px" }} />
</Link> Home
<a </Link>
role="button"
className="navbar-burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
onClick={(e) => {
e.currentTarget.classList.toggle("is-active");
document
.getElementById("navbarBasicExample")
.classList.toggle("is-active");
}}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" className="navbar-menu">
<div className="navbar-start">
<a <a
href="https://mail.hangman-lab.top" role="button"
className="navbar-item" className="navbar-burger"
target="_blank" aria-label="menu"
rel="noopener noreferrer" aria-expanded="false"
data-target="navbarBasicExample"
onClick={(e) => {
e.currentTarget.classList.toggle("is-active");
document
.getElementById("navbarBasicExample")
.classList.toggle("is-active");
}}
> >
MailBox <span aria-hidden="true"></span>
</a> <span aria-hidden="true"></span>
<a <span aria-hidden="true"></span>
href="https://git.hangman-lab.top"
className="navbar-item"
target="_blank"
rel="noopener noreferrer"
>
Git
</a> </a>
</div> </div>
<div className="navbar-end"> <div id="navbarBasicExample" className="navbar-menu">
{user && user.profile ? ( <div className="navbar-start">
<div className="navbar-item has-dropdown is-hoverable"> <a
<div className="buttons"> href="https://mail.hangman-lab.top"
<span className="navbar-item"
className="button is-primary is-light" target="_blank"
onClick={() => setIsDropdownOpen(!isDropdownOpen)} rel="noopener noreferrer"
> >
{user.profile.name} MailBox
</span> </a>
<div className={`navbar-dropdown ${isDropdownOpen ? "is-active" : ""}`}> <a
<button href="https://git.hangman-lab.top"
className="button is-primary dropdown-option" className="navbar-item"
onClick={handleGetBackup} target="_blank"
type="button" rel="noopener noreferrer"
> >
Get Backup Git
</button> </a>
<button </div>
className="button is-primary dropdown-option"
onClick={handleLoadBackup}
type="button"
>
Load Backup
</button>
<button
className="button is-danger dropdown-option"
onClick={logout}
type="button"
>
Logout
</button>
<div className="navbar-end">
{user && user.profile ? (
<div className="navbar-item has-dropdown is-hoverable">
<div className="buttons">
<span
className="button is-primary is-light"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
{user.profile.name}
</span>
<div className={`navbar-dropdown ${isDropdownOpen ? "is-active" : ""}`}>
<button
className="button is-primary dropdown-option"
onClick={handleGetBackup}
type="button"
>
Get Backup
</button>
<button
className="button is-primary dropdown-option"
onClick={handleLoadBackup}
type="button"
>
Load Backup
</button>
<button
className="button is-info dropdown-option"
onClick={() => setIsApiKeyModalOpen(true)}
type="button"
>
Create API Key
</button>
<button
className="button is-warning dropdown-option"
onClick={() => setIsRevokeModalOpen(true)}
type="button"
>
Revoke API Key
</button>
<button
className="button is-danger dropdown-option"
onClick={logout}
type="button"
>
Logout
</button>
</div>
</div> </div>
</div> </div>
</div> ) : (
) : ( <div className="navbar-item">
<div className="navbar-item"> <button
<button className="button is-primary"
className="button is-primary" onClick={login}
onClick={login} type="button"
type="button" >
> Login
Login </button>
</button> </div>
</div> )}
)} </div>
</div> </div>
</div> </nav>
</nav> <ApiKeyCreationModal
isOpen={isApiKeyModalOpen}
onClose={() => setIsApiKeyModalOpen(false)}
/>
<ApiKeyRevokeModal
isOpen={isRevokeModalOpen}
onClose={() => setIsRevokeModalOpen(false)}
/>
</>
); );
}; };

View File

@@ -0,0 +1,32 @@
import { useConfig } from "../../ConfigProvider";
import { useMutation } from "@tanstack/react-query";
import { fetch_ } from "../request-utils";
export const useCreateApiKey = () => {
const config = useConfig();
return useMutation({
mutationFn: async ({ name, roles }) => {
const response = await fetch_(`${config.BACKEND_HOST}/api/apikey/`, {
method: "POST",
body: JSON.stringify({ name, roles }),
});
console.log("response", response);
return response;
},
cacheTime: 0,
});
};
export const useRevokeApiKey = () => {
const config = useConfig();
return useMutation({
mutationFn: async (apiKey) => {
const response = await fetch_(`${config.BACKEND_HOST}/api/apikey/revoke`, {
method: "POST",
body: JSON.stringify({ apiKey }),
});
return response;
},
cacheTime: 0,
});
};