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:
@@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import { Send, FileText, Clock, CheckCircle, XCircle, Plus, User, Loader2, AtSign, Radio, Trash2, Download, Eye, Pencil } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { api } from "@/lib/api";
|
||||
import { shortenPubkey } from "@/lib/nostr";
|
||||
import { shortenPubkey, fetchEventFromRelays, fetchLongformFromRelays } from "@/lib/nostr";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
@@ -61,9 +61,7 @@ export default function DashboardPage() {
|
||||
const [submissions, setSubmissions] = useState<Submission[]>([]);
|
||||
const [loadingSubs, setLoadingSubs] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [eventId, setEventId] = useState("");
|
||||
const [naddr, setNaddr] = useState("");
|
||||
const [noteInput, setNoteInput] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [formError, setFormError] = useState("");
|
||||
const [formSuccess, setFormSuccess] = useState("");
|
||||
@@ -125,31 +123,56 @@ export default function DashboardPage() {
|
||||
loadRelays();
|
||||
}, [loadSubmissions, loadRelays]);
|
||||
|
||||
const extractTitle = (event: any): string => {
|
||||
const titleTag = event?.tags?.find((t: string[]) => t[0] === "title");
|
||||
return titleTag?.[1]?.trim() || "Untitled";
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setFormError("");
|
||||
setFormSuccess("");
|
||||
|
||||
if (!title.trim()) {
|
||||
setFormError("Title is required");
|
||||
return;
|
||||
}
|
||||
if (!eventId.trim() && !naddr.trim()) {
|
||||
setFormError("Either an Event ID or naddr is required");
|
||||
const input = noteInput.trim();
|
||||
if (!input) {
|
||||
setFormError("A Note ID or naddr is required");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await api.createSubmission({
|
||||
title: title.trim(),
|
||||
eventId: eventId.trim() || undefined,
|
||||
naddr: naddr.trim() || undefined,
|
||||
});
|
||||
setFormSuccess("Submission sent for review!");
|
||||
setTitle("");
|
||||
setEventId("");
|
||||
setNaddr("");
|
||||
let payload: { title: string; eventId?: string; naddr?: string };
|
||||
|
||||
if (input.startsWith("naddr1")) {
|
||||
const event = await fetchLongformFromRelays(input);
|
||||
if (!event) {
|
||||
throw new Error("Could not find that article on the relays");
|
||||
}
|
||||
payload = { title: extractTitle(event), naddr: input };
|
||||
} else {
|
||||
let hexId = input;
|
||||
if (input.startsWith("note1")) {
|
||||
try {
|
||||
const { nip19 } = await import("nostr-tools");
|
||||
const decoded = nip19.decode(input);
|
||||
if (decoded.type !== "note") throw new Error();
|
||||
hexId = decoded.data as string;
|
||||
} catch {
|
||||
throw new Error("Invalid note id");
|
||||
}
|
||||
} else if (!/^[0-9a-f]{64}$/i.test(input)) {
|
||||
throw new Error("Enter a valid note id, naddr, or hex event id");
|
||||
}
|
||||
const event = await fetchEventFromRelays(hexId);
|
||||
if (!event) {
|
||||
throw new Error("Could not find that note on the relays");
|
||||
}
|
||||
payload = { title: extractTitle(event), eventId: hexId };
|
||||
}
|
||||
|
||||
await api.createSubmission(payload);
|
||||
setFormSuccess(`Submission "${payload.title}" sent for review!`);
|
||||
setNoteInput("");
|
||||
setShowForm(false);
|
||||
await loadSubmissions();
|
||||
} catch (err: any) {
|
||||
@@ -381,46 +404,20 @@ export default function DashboardPage() {
|
||||
className="bg-surface-container-low rounded-xl p-6 mb-8 space-y-4"
|
||||
>
|
||||
<p className="text-on-surface-variant text-sm mb-2">
|
||||
Submit a Nostr longform post for moderator review. Provide the
|
||||
event ID or naddr of the article you'd like published on the
|
||||
blog.
|
||||
Submit a Nostr longform post for moderator review. Paste the
|
||||
note ID or naddr of the article you'd like published on the
|
||||
blog. The title is pulled automatically from the note.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-on-surface-variant mb-2">
|
||||
Title
|
||||
Note ID or naddr
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="My Bitcoin Article"
|
||||
className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-on-surface-variant mb-2">
|
||||
Nostr Event ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={eventId}
|
||||
onChange={(e) => setEventId(e.target.value)}
|
||||
placeholder="note1... or hex event id"
|
||||
className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 font-mono text-sm placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase tracking-widest text-on-surface-variant mb-2">
|
||||
Or naddr
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={naddr}
|
||||
onChange={(e) => setNaddr(e.target.value)}
|
||||
placeholder="naddr1..."
|
||||
value={noteInput}
|
||||
onChange={(e) => setNoteInput(e.target.value)}
|
||||
placeholder="naddr1... or note1... or hex event id"
|
||||
className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 font-mono text-sm placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40"
|
||||
/>
|
||||
</div>
|
||||
@@ -438,7 +435,7 @@ export default function DashboardPage() {
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Send size={16} />
|
||||
{submitting ? "Submitting..." : "Submit for Review"}
|
||||
{submitting ? "Fetching & submitting..." : "Submit for Review"}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user