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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user