feat: add scoped API keys for programmatic site access

Introduce ApiKey model, CRUD endpoints, and admin UI so agents can
authenticate with permission-scoped keys. Normalize pubkeys to hex on login,
dedupe legacy npub/hex user rows, and ignore .cursor in git.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bbe
2026-06-23 09:29:30 +02:00
co-authored by Cursor
parent 70e3e0633d
commit a6a2b113ee
17 changed files with 836 additions and 100 deletions
+52 -1
View File
@@ -7,7 +7,7 @@ import { useAuth } from "@/hooks/useAuth";
import { useNostrProfile } from "@/hooks/useNostrProfile";
import { NostrAvatar } from "@/components/nostr/NostrAvatar";
import { formatDate } from "@/lib/utils";
import { Lock } from "lucide-react";
import { Lock, Plus } from "lucide-react";
const ROLE_RANK: Record<string, number> = {
superadmin: 4,
@@ -208,6 +208,9 @@ export default function UsersPage() {
const [usernameDrafts, setUsernameDrafts] = useState<Record<string, string>>({});
const [savingPubkey, setSavingPubkey] = useState<string | null>(null);
const [copiedPubkey, setCopiedPubkey] = useState<string | null>(null);
const [newUserPubkey, setNewUserPubkey] = useState("");
const [addingUser, setAddingUser] = useState(false);
const [addUserSuccess, setAddUserSuccess] = useState("");
const loadUsers = async () => {
try {
@@ -286,6 +289,24 @@ export default function UsersPage() {
setUsernameDrafts((prev) => ({ ...prev, [pubkey]: value }));
};
const handleAddUser = async () => {
const value = newUserPubkey.trim();
if (!value) return;
setAddingUser(true);
setError("");
setAddUserSuccess("");
try {
await api.createUser(value);
setNewUserPubkey("");
setAddUserSuccess("User added.");
await loadUsers();
} catch (err: any) {
setError(err.message);
} finally {
setAddingUser(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
@@ -300,6 +321,36 @@ export default function UsersPage() {
{error && <p className="text-error text-sm">{error}</p>}
<div className="bg-surface-container-low rounded-xl p-6">
<h2 className="text-sm font-semibold text-on-surface/70 mb-3">Add User</h2>
<p className="text-on-surface-variant text-xs mb-3">
Pre-register a user by their npub or hex pubkey so you can assign a role before they log in.
</p>
<div className="flex flex-col sm:flex-row gap-3">
<input
placeholder="npub1... or hex pubkey"
value={newUserPubkey}
onChange={(e) => setNewUserPubkey(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleAddUser();
}}
disabled={addingUser}
className="bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 w-full font-mono text-sm focus:outline-none focus:ring-1 focus:ring-primary/40 flex-1 disabled:opacity-50"
/>
<button
onClick={handleAddUser}
disabled={!newUserPubkey.trim() || addingUser}
className="flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-primary to-primary-container text-on-primary font-semibold text-sm hover:opacity-90 transition-opacity disabled:opacity-50 whitespace-nowrap"
>
<Plus size={16} />
{addingUser ? "Adding…" : "Add User"}
</button>
</div>
{addUserSuccess && (
<p className="text-green-400 text-sm mt-3">{addUserSuccess}</p>
)}
</div>
<div className="space-y-3">
{users.length === 0 ? (
<p className="text-on-surface/50 text-sm">No users found.</p>