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:
@@ -45,7 +45,7 @@ export default function RolesPage() {
|
||||
const [original, setOriginal] = useState<Matrix>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [savingRole, setSavingRole] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [notice, setNotice] = useState("");
|
||||
|
||||
const load = async () => {
|
||||
@@ -101,19 +101,26 @@ export default function RolesPage() {
|
||||
}));
|
||||
};
|
||||
|
||||
const saveRole = async (role: string) => {
|
||||
setSavingRole(role);
|
||||
const saveAll = async () => {
|
||||
if (dirtyRoles.length === 0) return;
|
||||
setSaving(true);
|
||||
setError("");
|
||||
setNotice("");
|
||||
try {
|
||||
const keys = permissions.filter((p) => matrix[role]?.[p.key]).map((p) => p.key);
|
||||
await api.updateRolePermissions(role, keys);
|
||||
setOriginal((prev) => ({ ...prev, [role]: { ...matrix[role] } }));
|
||||
setNotice(`${ROLE_LABELS[role] ?? role} permissions saved.`);
|
||||
for (const role of dirtyRoles) {
|
||||
const keys = permissions.filter((p) => matrix[role]?.[p.key]).map((p) => p.key);
|
||||
await api.updateRolePermissions(role, keys);
|
||||
}
|
||||
setOriginal(() => {
|
||||
const next: Matrix = {};
|
||||
for (const role of roles) next[role] = { ...matrix[role] };
|
||||
return next;
|
||||
});
|
||||
setNotice("Permissions saved.");
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setSavingRole(null);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -207,23 +214,20 @@ export default function RolesPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{roles.map((role) => {
|
||||
const dirty = dirtyRoles.includes(role);
|
||||
return (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => saveRole(role)}
|
||||
disabled={!dirty || savingRole === role}
|
||||
className="flex items-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"
|
||||
>
|
||||
<Save size={16} />
|
||||
{savingRole === role
|
||||
? "Saving..."
|
||||
: `Save ${ROLE_LABELS[role] ?? role}${dirty ? " *" : ""}`}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={saveAll}
|
||||
disabled={dirtyRoles.length === 0 || saving}
|
||||
className="flex items-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"
|
||||
>
|
||||
<Save size={16} />
|
||||
{saving ? "Saving..." : "Save changes"}
|
||||
</button>
|
||||
{dirtyRoles.length > 0 && !saving && (
|
||||
<span className="text-on-surface/50 text-sm">
|
||||
Unsaved changes to {dirtyRoles.map((r) => ROLE_LABELS[r] ?? r).join(", ")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user