Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: permission presets #3103

Merged
merged 13 commits into from
Mar 25, 2024
114 changes: 96 additions & 18 deletions src/app/components/Enable/NostrEnable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import PublisherCard from "@components/PublisherCard";
import { PopiconsCheckLine } from "@popicons/react";
import {
PopiconsCheckLine,
PopiconsGlassesSolid,
PopiconsHeartLine,
PopiconsLikeLine,
} from "@popicons/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import Alert from "~/app/components/Alert";
import ScreenHeader from "~/app/components/ScreenHeader";
import toast from "~/app/components/Toast";
import { classNames } from "~/app/utils";
import { USER_REJECTED_ERROR } from "~/common/constants";
import msg from "~/common/lib/msg";
import type { OriginData } from "~/types";
import { NostrPermissionPreset, type OriginData } from "~/types";

type Props = {
origin: OriginData;
};
function NostrEnableComponent(props: Props) {
const [loading, setLoading] = useState(false);
const [selectedPreset, setSelectedPreset] = useState(
NostrPermissionPreset.REASONABLE
);
const hasHttp = props.origin.domain.startsWith("http://");
const { t } = useTranslation("translation", {
keyPrefix: "nostr_enable",
Expand All @@ -25,9 +34,11 @@ function NostrEnableComponent(props: Props) {
const enable = () => {
try {
setLoading(true);

msg.reply({
enabled: true,
remember: true,
preset: selectedPreset,
});
} catch (e) {
console.error(e);
Expand Down Expand Up @@ -61,28 +72,43 @@ function NostrEnableComponent(props: Props) {
title={props.origin.name}
image={props.origin.icon}
url={props.origin.host}
isSmall={false}
/>

<div className="pt-3">
{hasHttp && (
{hasHttp && (
<div className="pt-3 text-sm">
<Alert type="warn">
{tCommon("enable.insecure_domain_warn")}
</Alert>
)}
</div>

<div className="dark:text-white pt-6">
<p className="mb-2">{tCommon("enable.allow")}</p>

<div className="mb-2 flex items-center">
<PopiconsCheckLine className="w-5 h-5 mr-2" />
<p className="dark:text-white">{t("request1")}</p>
</div>
<div className="mb-2 flex items-center">
<PopiconsCheckLine className="w-5 h-5 mr-2" />
<p className="dark:text-white">{t("request2")}</p>
</div>
)}

<div className="flex flex-col gap-2 dark:text-white my-5">
<p className="text-base font-medium">{t("description")}</p>
<PermissionPreset
title={t("presets.trust_fully.title")}
description={t("presets.trust_fully.description")}
icon={<PopiconsHeartLine className="w-6 h-6" />}
onClick={() =>
setSelectedPreset(NostrPermissionPreset.TRUST_FULLY)
}
isSelected={selectedPreset === NostrPermissionPreset.TRUST_FULLY}
/>
<PermissionPreset
title={t("presets.reasonable.title")}
description={t("presets.reasonable.description")}
icon={<PopiconsLikeLine className="w-6 h-6" />}
onClick={() =>
setSelectedPreset(NostrPermissionPreset.REASONABLE)
}
isSelected={selectedPreset === NostrPermissionPreset.REASONABLE}
/>
<PermissionPreset
title={t("presets.paranoid.title")}
description={t("presets.paranoid.description")}
icon={<PopiconsGlassesSolid className="w-6 h-6" />}
onClick={() => setSelectedPreset(NostrPermissionPreset.PARANOID)}
isSelected={selectedPreset === NostrPermissionPreset.PARANOID}
/>
</div>
</div>
<div className="text-center flex flex-col">
Expand All @@ -106,4 +132,56 @@ function NostrEnableComponent(props: Props) {
);
}

type PermissionPresetProps = {
title: string;
description: string;
icon: React.ReactNode;
onClick: () => void;
isSelected: boolean;
};
function PermissionPreset({
icon,
title,
description,
onClick,
isSelected,
}: PermissionPresetProps) {
return (
<button
className={classNames(
"text-left border border-gray-200 dark:border-neutral-800 rounded-xl p-4 text-gray-800 dark:text-neutral-200 cursor-pointer flex flex-row items-center gap-3",
isSelected
? "bg-amber-50 dark:bg-surface-02dp ring-primary border-primary dark:border-primary ring-1"
: "bg-white dark:bg-surface-01dp hover:bg-gray-50 dark:hover:bg-surface-02dp"
)}
onClick={onClick}
>
<div
className={classNames(
"flex-shrink-0 flex justify-center md:px-3",
isSelected ? "text-amber-400" : "text-gray-400 dark:text-neutral-600"
)}
>
{icon}
</div>
<div className="flex-grow space-y-0.5">
<div className="font-medium leading-5 text-sm md:text-base">
{title}
</div>
<div className="text-gray-600 dark:text-neutral-400 text-xs leading-4 md:text-sm">
{description}
</div>
</div>
<div
className={classNames(
"flex-shrink-0 flex justify-end text-gray-400 dark:text-neutral-600",
isSelected ? "" : "hidden"
)}
>
<PopiconsCheckLine className="w-5" />
</div>
</button>
);
}

export default NostrEnableComponent;
4 changes: 2 additions & 2 deletions src/app/components/SitePreferences/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
label={
permission.method
.toLowerCase()
.startsWith("nostr/signmessage")
.startsWith("nostr/signmessage/")
? tNostr(
`kinds.${getPermissionKind(
permission
Expand All @@ -280,7 +280,7 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
description={
permission.method
.toLowerCase()
.startsWith("nostr/signmessage")
.startsWith("nostr/signmessage/")
? tNostr(
`kinds.${getPermissionKind(
permission
Expand Down
48 changes: 46 additions & 2 deletions src/extension/background-script/actions/nostr/enable.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import utils from "~/common/lib/utils";
import { getHostFromSender } from "~/common/utils/helpers";
import db from "~/extension/background-script/db";
import type { MessageAllowanceEnable, Sender } from "~/types";
import {
NostrPermissionPreset,
PermissionMethodNostr,
type MessageAllowanceEnable,
type Sender,
} from "~/types";

import { addPermissionFor } from "~/extension/background-script/permissions";
import state from "../../state";
import { ExtensionIcon, setIcon } from "../setup/setIcon";

Expand All @@ -13,7 +19,6 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => {
const isUnlocked = await state.getState().isUnlocked();
const account = await state.getState().getAccount();
const allowance = await db.allowances

.where("host")
.equalsIgnoreCase(host)
.first();
Expand All @@ -35,6 +40,7 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => {
const response = await utils.openPrompt<{
enabled: boolean;
remember: boolean;
preset: string;
}>(message);

if (response.data.enabled && sender.tab) {
Expand Down Expand Up @@ -72,6 +78,44 @@ const enable = async (message: MessageAllowanceEnable, sender: Sender) => {
tag: "",
});
}
if (response.data.preset === NostrPermissionPreset.REASONABLE) {
// Add permissions
const permissions: PermissionMethodNostr[] = [
PermissionMethodNostr.NOSTR_GETPUBLICKEY,
PermissionMethodNostr.NOSTR_NIP04ENCRYPT,
PermissionMethodNostr.NOSTR_NIP04DECRYPT,
PermissionMethodNostr.NOSTR_NIP44ENCRYPT,
PermissionMethodNostr.NOSTR_NIP44DECRYPT,
];
permissions.forEach(async (permission) => {
await addPermissionFor(permission, host);
});

// Add specific signing permissions
const reasonableEventKindIds = [
0, // Update profile
1, // Short text note
3, // Update follow list
4, // Encrypted direct messages
7, // Reaction
9734, // Zap request
10002, // Relay list metadata
22242, // Client relay authentication
30023, // Long-form content
30008, // Manage profile badges
30009, // Badge definition
];
reasonableEventKindIds.forEach(async (kindId) => {
await addPermissionFor(
PermissionMethodNostr.NOSTR_SIGNMESSAGE + "/" + kindId,
host
);
});
} else if (response.data.preset === NostrPermissionPreset.TRUST_FULLY) {
Object.values(PermissionMethodNostr).forEach(async (permission) => {
await addPermissionFor(permission, host);
});
}
await db.saveToStorage();
}
return {
Expand Down
23 changes: 18 additions & 5 deletions src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{

Check warning on line 1 in src/i18n/locales/en/translation.json

View workflow job for this annotation

GitHub Actions / Check source translation file for changes

Translation source translation.nostr.kinds.1984.description has changed

Consider running `node scripts/remove-outdated-translations.js translation.nostr.kinds.1984.description` to reset existing translations.

Check warning on line 1 in src/i18n/locales/en/translation.json

View workflow job for this annotation

GitHub Actions / Check source translation file for changes

Translation source translation.nostr.kinds.30023.description has changed

Consider running `node scripts/remove-outdated-translations.js translation.nostr.kinds.30023.description` to reset existing translations.

Check warning on line 1 in src/i18n/locales/en/translation.json

View workflow job for this annotation

GitHub Actions / Check source translation file for changes

Translation source translation.nostr.kinds.30078.description has changed

Consider running `node scripts/remove-outdated-translations.js translation.nostr.kinds.30078.description` to reset existing translations.
"translation": {
"welcome": {
"set_password": {
Expand Down Expand Up @@ -552,8 +552,21 @@
},
"nostr_enable": {
"title": "Connect to Nostr",
"request1": "Request to read your Nostr public key",
"request2": "Request to sign events using your private key"
"description": "How should this app's requests be handled?",
"presets": {
"trust_fully": {
"title": "I fully trust it",
"description": "Auto-sign all requests (except payments)"
},
"reasonable": {
"title": "Let's be reasonable",
"description": "Auto-approve most common requests"
},
"paranoid": {
"title": "I'm a bit paranoid",
"description": "Do not sign anything without asking me!"
}
}
},
"liquid_enable": {
"title": "Connect to Liquid",
Expand Down Expand Up @@ -980,7 +993,7 @@
},
"1984": {
"title": "report note",
"description": "Report a note for spam, illegal or explicit content "
"description": "Report a note for spam, illegal or explicit content"
},
"9734": {
"title": "request zap",
Expand Down Expand Up @@ -1012,11 +1025,11 @@
},
"30023": {
"title": "long note",
"description": "Sign a long-form notes like articles or blog posts"
"description": "Sign long-form notes like articles or blog posts"
},
"30078": {
"title": "app data",
"description": "Sign an application-specific data"
"description": "Sign application-specific data"
}
}
},
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,12 @@ export interface Payment extends Omit<DbPayment, "id"> {
id: number;
}

export enum NostrPermissionPreset {
TRUST_FULLY = "trust_fully",
REASONABLE = "reasonable",
PARANOID = "paranoid",
}

export enum PermissionOption {
ASK_EVERYTIME = "ask_everytime",
DONT_ASK_CURRENT = "dont_ask_current",
Expand Down
Loading