add: backend api auth by apikey/apikey gen/apikey revoke
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
summerizer.py
|
||||
node_modules
|
||||
|
||||
164
src/components/Modals/ApiKeyCreationModal.js
Normal file
164
src/components/Modals/ApiKeyCreationModal.js
Normal 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;
|
||||
63
src/components/Modals/ApiKeyRevokeModal.js
Normal file
63
src/components/Modals/ApiKeyRevokeModal.js
Normal 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;
|
||||
@@ -4,11 +4,16 @@ import { AuthContext } from "../../AuthProvider";
|
||||
import "bulma/css/bulma.min.css";
|
||||
import {useConfig} from "../../ConfigProvider";
|
||||
import "./MainNavigation.css";
|
||||
import ApiKeyCreationModal from "../Modals/ApiKeyCreationModal";
|
||||
import ApiKeyRevokeModal from "../Modals/ApiKeyRevokeModal";
|
||||
|
||||
const MainNavigation = () => {
|
||||
const { user, login, logout } = useContext(AuthContext);
|
||||
const config = useConfig();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
|
||||
const [isRevokeModalOpen, setIsRevokeModalOpen] = useState(false);
|
||||
|
||||
if (config===undefined) {
|
||||
return <div>Loading ...</div>;
|
||||
}
|
||||
@@ -88,6 +93,7 @@ const MainNavigation = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||
<div className="navbar-brand">
|
||||
<Link className="navbar-item" to="/">
|
||||
@@ -158,6 +164,20 @@ const MainNavigation = () => {
|
||||
>
|
||||
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}
|
||||
@@ -165,9 +185,7 @@ const MainNavigation = () => {
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -184,6 +202,15 @@ const MainNavigation = () => {
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<ApiKeyCreationModal
|
||||
isOpen={isApiKeyModalOpen}
|
||||
onClose={() => setIsApiKeyModalOpen(false)}
|
||||
/>
|
||||
<ApiKeyRevokeModal
|
||||
isOpen={isRevokeModalOpen}
|
||||
onClose={() => setIsRevokeModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
32
src/utils/queries/apikey-queries.js
Normal file
32
src/utils/queries/apikey-queries.js
Normal 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,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user