├── .gitignore
├── popup-screenshot.png
├── dist
├── assets
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.ttf
│ │ ├── icomoon.woff
│ │ └── icomoon.svg
│ ├── icons
│ │ ├── 128x128.png
│ │ ├── 16x16.png
│ │ ├── 32x32.png
│ │ └── 48x48.png
│ ├── img
│ │ └── store-banner.jpeg
│ └── css
│ │ ├── style.build.css.map
│ │ └── style.build.css
├── options.html
├── popup.html
├── prompt.html
├── manifest.json
└── nostr-provider.js
├── src
├── helpers
│ ├── copyToClipboard.jsx
│ ├── identicon.jsx
│ └── hideStringMiddle.jsx
├── pages
│ ├── NotFoundPage.jsx
│ ├── HomePage.jsx
│ ├── SigninPage.jsx
│ ├── SignupPage.jsx
│ ├── GeneratorPage.jsx
│ └── VaultPage.jsx
├── middlewares
│ ├── PrivateRoute.jsx
│ └── AuthContext.jsx
├── components
│ ├── Secrets.jsx
│ ├── ResetVault.jsx
│ ├── HeaderVault.jsx
│ ├── Navbar.jsx
│ ├── ExportVault.jsx
│ ├── LockedVault.jsx
│ ├── ImportVault.jsx
│ ├── Relays.jsx
│ ├── ChangePassword.jsx
│ └── Accounts.jsx
├── utils.js
├── modals
│ ├── Modal.jsx
│ ├── SecretsModal.jsx
│ ├── DeriveAccountModal.jsx
│ ├── AccountDetailsModal.jsx
│ ├── GenerateRandomAccountModal.jsx
│ ├── ImportAccountModal.jsx
│ └── EditAccountModal.jsx
├── content-script.jsx
├── popup.jsx
├── layouts
│ └── MainLayout.jsx
├── options.jsx
├── prompt.jsx
├── assets
│ └── css
│ │ ├── _fonts.scss
│ │ ├── style.build.css.map
│ │ ├── style.scss
│ │ └── style.build.css
├── contexts
│ └── MainContext.jsx
├── common.js
└── background.jsx
├── package.json
├── privacy.txt
├── README.md
└── nostrame.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.build.js
3 | *.zip
4 |
--------------------------------------------------------------------------------
/popup-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/popup-screenshot.png
--------------------------------------------------------------------------------
/dist/assets/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.eot
--------------------------------------------------------------------------------
/dist/assets/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/dist/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/128x128.png
--------------------------------------------------------------------------------
/dist/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/16x16.png
--------------------------------------------------------------------------------
/dist/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/32x32.png
--------------------------------------------------------------------------------
/dist/assets/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/icons/48x48.png
--------------------------------------------------------------------------------
/dist/assets/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/fonts/icomoon.woff
--------------------------------------------------------------------------------
/dist/assets/img/store-banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anderson-Juhasc/nostrame/HEAD/dist/assets/img/store-banner.jpeg
--------------------------------------------------------------------------------
/src/helpers/copyToClipboard.jsx:
--------------------------------------------------------------------------------
1 | import { toast } from 'react-toastify'
2 |
3 | export default function copyToClipboard(e, text) {
4 | e.preventDefault()
5 | navigator.clipboard.writeText(text)
6 | toast.success("Copied with success")
7 | }
--------------------------------------------------------------------------------
/src/helpers/identicon.jsx:
--------------------------------------------------------------------------------
1 | import Identicon from "identicon.js"
2 |
3 | export default async function getIdenticon(pubkey) {
4 | if (pubkey.length < 15) return ""
5 |
6 | const svg = new Identicon(pubkey, { format: "svg" }).toString()
7 | return svg
8 | }
9 |
--------------------------------------------------------------------------------
/dist/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | "],
25 | "js": ["content-script.build.js"],
26 | "all_frames": true
27 | }
28 | ],
29 | "permissions": ["storage", "windows"],
30 | "optional_permissions": ["notifications"],
31 | "web_accessible_resources": [
32 | {
33 | "resources": ["nostr-provider.js"],
34 | "matches": [
35 | "https://*/*",
36 | "http://localhost:*/*",
37 | "http://0.0.0.0:*/*",
38 | "http://127.0.0.1:*/*",
39 | "http://*.localhost/*"
40 | ]
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React from 'react'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Navbar = () => {
6 | const openOptionsButton = async () => {
7 | if (browser.runtime.openOptionsPage) {
8 | browser.runtime.openOptionsPage()
9 | } else {
10 | window.open(browser.runtime.getURL('options.html'))
11 | }
12 | }
13 |
14 | return (
15 |
40 | )
41 | }
42 |
43 | export default Navbar
--------------------------------------------------------------------------------
/src/components/ExportVault.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React from 'react'
3 |
4 | const ExportVault = () => {
5 | const handleVaultExport = async () => {
6 | const storage = await browser.storage.local.get(['encryptedVault'])
7 | const jsonData = JSON.stringify({ vault: storage.encryptedVault }, null, 2)
8 | const blob = new Blob([jsonData], { type: 'application/json' })
9 | const url = URL.createObjectURL(blob)
10 | const a = document.createElement('a')
11 |
12 | const currentDate = new Date()
13 | const year = currentDate.getFullYear()
14 | const month = ('0' + (currentDate.getMonth() + 1)).slice(-2) // Adding 1 to month since it's zero-based
15 | const day = ('0' + currentDate.getDate()).slice(-2)
16 | const hours = ('0' + currentDate.getHours()).slice(-2)
17 | const minutes = ('0' + currentDate.getMinutes()).slice(-2)
18 | const seconds = ('0' + currentDate.getSeconds()).slice(-2)
19 |
20 | a.href = url
21 | a.download = `NostrameVaultData.${year}_${month}_${day}_${hours}_${minutes}_${seconds}.json`
22 | a.click()
23 | }
24 |
25 | return (
26 | <>
27 | Export Vault
28 | Export backup
29 | >
30 | )
31 | }
32 | export default ExportVault
33 |
--------------------------------------------------------------------------------
/src/popup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { HashRouter as Router, Route, Routes } from 'react-router-dom'
3 | import { createRoot } from 'react-dom/client'
4 | import { AuthProvider } from './middlewares/AuthContext'
5 | import PrivateRoute from './middlewares/PrivateRoute'
6 | import MainLayout from './layouts/MainLayout'
7 | import HomePage from './pages/HomePage'
8 | import SigninPage from './pages/SigninPage'
9 | import SignupPage from './pages/SignupPage'
10 | import VaultPage from './pages/VaultPage'
11 | import NotFoundPage from './pages/NotFoundPage'
12 | import GeneratorPage from './pages/GeneratorPage'
13 |
14 | const App = () => {
15 | return (
16 |
17 |
18 |
19 | }>
20 | } />
21 |
22 | }>
23 | } />
24 | } />
25 | } />
26 | } />
27 | } />
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
35 | const container = document.getElementById('main')
36 | const root = createRoot(container)
37 | root.render( )
38 |
--------------------------------------------------------------------------------
/src/components/LockedVault.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState } from 'react'
3 | import { decrypt } from '../common'
4 |
5 | const LockedVault = () => {
6 | const [password, setPassword] = useState('')
7 |
8 | const unlockVault = async (e) => {
9 | e.preventDefault()
10 | const storage = await browser.storage.local.get(['encryptedVault'])
11 | const vaultData = decrypt(storage.encryptedVault, password)
12 | await browser.storage.local.set({
13 | isLocked: false,
14 | vault: vaultData,
15 | password,
16 | })
17 | setPassword('')
18 | window.location.reload()
19 | }
20 |
21 | return (
22 |
23 |
24 |
Nostrame
25 |
26 |
27 |
28 |
29 |
48 |
49 |
50 | )
51 | }
52 | export default LockedVault
53 |
--------------------------------------------------------------------------------
/src/layouts/MainLayout.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 | import { Outlet } from 'react-router-dom'
4 | import { ToastContainer } from 'react-toastify'
5 | import HeaderVault from '../components/HeaderVault'
6 | import Navbar from '../components/Navbar'
7 | import LockedVault from '../components/LockedVault'
8 | import { MainProvider } from '../contexts/MainContext'
9 |
10 | const MainLayout = () => {
11 | const [isLocked, setIsLocked] = useState(false)
12 | const [isAuthenticated, setIsAuthenticated] = useState(false)
13 |
14 | useEffect(() => {
15 | fetchData()
16 |
17 | browser.storage.onChanged.addListener(function(changes, area) {
18 | fetchData()
19 | })
20 | }, [])
21 |
22 | const fetchData = async () => {
23 | const storage = await browser.storage.local.get(['isLocked', 'isAuthenticated'])
24 |
25 | if (storage.isLocked) {
26 | setIsLocked(true)
27 | }
28 |
29 | if (storage.isAuthenticated) {
30 | setIsAuthenticated(true)
31 | }
32 | }
33 |
34 | return (
35 | <>
36 | {isLocked ? (
37 | <>
38 |
39 | >
40 | ) : (
41 | <>
42 |
43 |
44 |
45 | {isAuthenticated && (
46 |
47 | )}
48 |
49 |
50 | >
51 | )}
52 | >
53 | )
54 | }
55 |
56 | export default MainLayout
57 |
--------------------------------------------------------------------------------
/src/components/ImportVault.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState } from 'react'
3 | import { decrypt } from '../common'
4 |
5 | const ImportVault = ({ fetchData }) => {
6 | const [file, setFile] = useState(null)
7 | const [password, setPassword] = useState('')
8 |
9 | const handleFileChange = (e) => {
10 | e.preventDefault()
11 | const file = e.target.files[0]
12 | setFile(file)
13 | }
14 |
15 | const handleVaultImport = (e) => {
16 | e.preventDefault()
17 | if (file) {
18 | const reader = new FileReader()
19 | reader.onload = async () => {
20 | const encryptedVault = (JSON.parse(reader.result)).vault
21 | try {
22 | const vaultData = decrypt(encryptedVault, password)
23 | await browser.storage.local.set({
24 | vault: vaultData,
25 | encryptedVault,
26 | isAuthenticated: true,
27 | password
28 | })
29 | setPassword('')
30 | fetchData()
31 | } catch (e) {
32 | console.log(e)
33 | }
34 | }
35 | reader.readAsText(file)
36 | }
37 | }
38 |
39 | return (
40 | <>
41 | Import Vault
42 |
43 |
57 | >
58 | )
59 | }
60 | export default ImportVault
61 |
--------------------------------------------------------------------------------
/src/pages/HomePage.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import { Link, Navigate, useNavigate } from 'react-router-dom'
3 | import React, { useEffect, useContext } from 'react'
4 | import { useAuth } from '../middlewares/AuthContext';
5 | import MainContext from '../contexts/MainContext'
6 |
7 | const HomePage = () => {
8 | const { isAuthenticated } = useAuth()
9 |
10 | if (isAuthenticated) return
11 |
12 | const { updateAccounts } = useContext(MainContext)
13 |
14 | const { login } = useAuth()
15 | const navigate = useNavigate()
16 |
17 | useEffect(() => {
18 | browser.storage.onChanged.addListener(async function(changes, area) {
19 | if (changes.isAuthenticated) {
20 | await login()
21 | await updateAccounts()
22 | navigate('/vault')
23 | }
24 | })
25 | }, [])
26 |
27 | return (
28 | <>
29 |
30 |
31 |
Welcome to Nostrame
32 |
33 |
34 |
35 |
36 | Import existing Vault
37 |
38 | Already have a Vault? Import it using your seed phrase or encrypted keystore file
39 |
40 |
41 |
42 |
43 |
44 | Create new Vault
45 |
46 | New to Nostrame Vault? Let's set it up! This will create a new vault and seed phrase
47 |
48 |
49 |
50 | >
51 | )
52 | }
53 |
54 | export default HomePage
55 |
--------------------------------------------------------------------------------
/src/components/Relays.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 |
4 | const Relays = () => {
5 | const [relay, setRelay] = useState('')
6 | const [relays, setRelays] = useState([])
7 |
8 | useEffect(() => {
9 | fetchData()
10 |
11 | browser.storage.onChanged.addListener(function() {
12 | fetchData()
13 | })
14 | }, [])
15 |
16 | const fetchData = async () => {
17 | const storage = await browser.storage.local.get(['relays'])
18 | setRelays(storage.relays)
19 | }
20 |
21 | const addNewRelay = async (e) => {
22 | e.preventDefault()
23 |
24 | const relayExist = relays.find(item => item === relay)
25 | if (relayExist) {
26 | alert('Please provide a not existing relay')
27 | setRelay('')
28 | return false
29 | }
30 |
31 | relays.push(relay)
32 | setRelays(relays)
33 | setRelay('')
34 | await browser.storage.local.set({
35 | relays: relays,
36 | })
37 | }
38 |
39 | const removeRelay = async (index) => {
40 | const newRelays = [...relays]
41 | if (index !== -1) {
42 | newRelays.splice(index, 1)
43 | setRelays(newRelays)
44 | }
45 | await browser.storage.local.set({
46 | relays: newRelays,
47 | })
48 | }
49 |
50 | return (
51 | <>
52 |
65 |
66 |
67 | {relays.map((relay, index) => (
68 |
69 | {relay}
70 |
71 | removeRelay(index)}>×
72 |
73 | ))}
74 |
75 | >
76 | )
77 | }
78 | export default Relays
79 |
--------------------------------------------------------------------------------
/src/modals/SecretsModal.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 | import Modal from './Modal'
4 | import { decrypt } from '../common'
5 |
6 | const SecretsModal = ({ isOpen, onClose }) => {
7 | const [showModal, setShowModal] = useState(isOpen)
8 | const [password, setPassword] = useState('')
9 | const [vault, setVault] = useState({})
10 | const [isDecrypted, setIsDecrypted] = useState(false)
11 |
12 | useEffect(() => {
13 | setShowModal(isOpen)
14 | if (!isOpen) {
15 | setIsDecrypted(false)
16 | setVault({})
17 | }
18 | }, [isOpen])
19 |
20 | const closeModal = () => {
21 | setShowModal(false)
22 | onClose()
23 | }
24 |
25 | const decryptVault = async (e) => {
26 | e.preventDefault()
27 |
28 | const storage = await browser.storage.local.get(['encryptedVault'])
29 | const decryptedVault = decrypt(storage.encryptedVault, password)
30 | setIsDecrypted(true)
31 | setPassword('')
32 | setVault(decryptedVault)
33 | }
34 |
35 | return (
36 |
37 |
38 | {!isDecrypted ? (
39 |
54 | ) : (
55 | <>
56 | Secrets
57 | Mnemonic: {vault.mnemonic}
58 | { vault.passphrase && (Passphrase: {vault.passphrase}
) }
59 | Account index: {vault.accountIndex}
60 | >
61 | )}
62 |
63 |
64 | )
65 | }
66 |
67 | export default SecretsModal
68 |
--------------------------------------------------------------------------------
/src/options.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import { createRoot } from 'react-dom/client'
3 | import React, { useState, useEffect } from 'react'
4 | import { ToastContainer } from 'react-toastify'
5 |
6 | import ChangePassword from './components/ChangePassword'
7 | import ResetVault from './components/ResetVault'
8 | import Relays from './components/Relays'
9 | import ImportVault from './components/ImportVault'
10 | import ExportVault from './components/ExportVault'
11 | import Secrets from './components/Secrets'
12 |
13 | function Options() {
14 | const [isAuthenticated, setIsAuthenticated] = useState(false)
15 | const [isLocked, setIsLocked] = useState(false)
16 |
17 | useEffect(() => {
18 | fetchData()
19 |
20 | browser.storage.onChanged.addListener(function() {
21 | fetchData()
22 | });
23 | }, [])
24 |
25 | const fetchData = async () => {
26 | const storage = await browser.storage.local.get(['isAuthenticated', 'isLocked'])
27 |
28 | setIsLocked(storage.isLocked)
29 | setIsAuthenticated(storage.isAuthenticated)
30 | }
31 |
32 | return (
33 |
34 |
35 |
Options
36 |
37 | {!isLocked ? (
38 | <>
39 | { isAuthenticated && (
40 | <>
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | >
55 | )}
56 |
57 | { !isAuthenticated && (
58 | <>
59 |
60 |
61 | >
62 | )}
63 |
64 | { isAuthenticated && (
65 |
66 | )}
67 | >
68 | ) : (
69 | Vault is locked
70 | )}
71 |
72 |
73 |
74 |
75 | )
76 |
77 | }
78 |
79 | const container = document.getElementById('main')
80 | const root = createRoot(container) // createRoot(container!) if you use TypeScript
81 | root.render( )
82 |
--------------------------------------------------------------------------------
/src/modals/DeriveAccountModal.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { privateKeyFromSeedWords } from 'nostr-tools/nip06'
4 | import { SimplePool } from 'nostr-tools/pool'
5 | import { finalizeEvent } from 'nostr-tools/pure'
6 | import { encrypt } from '../common'
7 | import Modal from './Modal'
8 | import MainContext from '../contexts/MainContext'
9 |
10 | const DeriveAccountModal = ({ isOpen, onClose, callBack }) => {
11 | const { updateAccounts } = useContext(MainContext)
12 |
13 | const [showModal, setShowModal] = useState(isOpen)
14 | const [name, setName] = useState('')
15 |
16 | useEffect(() => {
17 | setShowModal(isOpen)
18 | }, [isOpen])
19 |
20 | const closeModal = () => {
21 | setShowModal(false)
22 | onClose()
23 | }
24 |
25 | const addAccount = async (e) => {
26 | e.preventDefault()
27 |
28 | const storage = await browser.storage.local.get(['vault', 'password', 'relays'])
29 | const vault = storage.vault
30 |
31 | vault.accountIndex++
32 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vault.accountIndex)
33 | vault.accounts.push({
34 | prvKey,
35 | })
36 | vault.accountDefault = prvKey
37 |
38 | if (name || name !== '') {
39 | const pool = new SimplePool()
40 | const relays = storage.relays
41 | const event = {
42 | kind: 0,
43 | created_at: Math.floor(Date.now() / 1000),
44 | tags: [],
45 | content: JSON.stringify({
46 | name: name,
47 | display_name: name,
48 | }),
49 | }
50 |
51 | const signedEvent = finalizeEvent(event, prvKey)
52 | await Promise.any(pool.publish(relays, signedEvent))
53 | }
54 |
55 | const encryptedVault = encrypt(vault, storage.password)
56 | await browser.storage.local.set({
57 | vault,
58 | encryptedVault,
59 | })
60 | await updateAccounts()
61 |
62 | setName('')
63 | callBack()
64 | }
65 |
66 | return (
67 |
68 |
69 |
83 |
84 |
85 | )
86 | }
87 |
88 | export default DeriveAccountModal
89 |
--------------------------------------------------------------------------------
/nostrame.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
36 |
38 |
42 |
51 |
58 | N
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/components/ChangePassword.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState } from 'react'
3 | import { toast } from 'react-toastify'
4 | import { encrypt, decrypt } from '../common'
5 |
6 | const ChangePassword = ({ fetchData }) => {
7 | const [changePassword, setChangePassword] = useState({
8 | currentPassword: '',
9 | newPassword: '',
10 | confirmNewPassword: '',
11 | })
12 |
13 | const changePasswordInput = (e) => {
14 | const { name, value } = e.target;
15 | setChangePassword(prev => ({
16 | ...prev,
17 | [name]: value
18 | }))
19 | }
20 |
21 | const submitChangePassword = async (e) => {
22 | e.preventDefault()
23 |
24 | const confirmChange = confirm("Are you sure you want to change the password?")
25 | if (!confirmChange) return
26 |
27 | if (changePassword.newPassword !== changePassword.confirmNewPassword) {
28 | alert('New password do not match!')
29 |
30 | setChangePassword({
31 | currentPassword: '',
32 | newPassword: '',
33 | confirmNewPassword: '',
34 | })
35 |
36 | return
37 | }
38 |
39 | const storage = await browser.storage.local.get(['encryptedVault'])
40 | try {
41 | const decryptedVault = decrypt(storage.encryptedVault, changePassword.currentPassword)
42 | const encryptedVault = encrypt(decryptedVault, changePassword.newPassword)
43 | await browser.storage.local.set({
44 | encryptedVault,
45 | password: changePassword.currentPassword
46 | })
47 | setChangePassword({
48 | currentPassword: '',
49 | newPassword: '',
50 | confirmNewPassword: '',
51 | })
52 | fetchData()
53 | toast.success("Your password was changed with success")
54 | } catch (e) {
55 | toast.error("Your password do not match", e)
56 | setChangePassword({
57 | currentPassword: '',
58 | newPassword: '',
59 | confirmNewPassword: '',
60 | })
61 | }
62 | }
63 |
64 | return (
65 |
99 | )
100 | }
101 | export default ChangePassword
102 |
--------------------------------------------------------------------------------
/src/prompt.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React from 'react'
3 | import { createRoot } from 'react-dom/client'
4 |
5 | import {PERMISSION_NAMES} from './common'
6 |
7 | function Prompt() {
8 | let qs = new URLSearchParams(location.search)
9 | let id = qs.get('id')
10 | let host = qs.get('host')
11 | let type = qs.get('type')
12 | let params, event
13 | try {
14 | params = JSON.parse(qs.get('params'))
15 | if (Object.keys(params).length === 0) params = null
16 | else if (params.event) event = params.event
17 | } catch (err) {
18 | params = null
19 | }
20 |
21 | return (
22 | <>
23 |
24 |
25 | {host}
26 | {' '}
27 |
28 | is requesting your permission to {PERMISSION_NAMES[type]}:
29 |
30 |
31 | {params && (
32 | <>
33 | now acting on
34 |
35 | {JSON.stringify(event || params, null, 2)}
36 |
37 | >
38 | )}
39 |
46 |
53 | authorize forever
54 |
55 | {event?.kind !== undefined && (
56 |
63 | authorize kind {event.kind} forever
64 |
65 | )}
66 |
67 | authorize just this
68 |
69 | {event?.kind !== undefined ? (
70 |
77 | reject kind {event.kind} forever
78 |
79 | ) : (
80 |
87 | reject forever
88 |
89 | )}
90 |
91 | reject
92 |
93 |
94 | >
95 | )
96 |
97 | function authorizeHandler(accept, conditions) {
98 | return function (ev) {
99 | ev.preventDefault()
100 | browser.runtime.sendMessage({
101 | prompt: true,
102 | id,
103 | host,
104 | type,
105 | accept,
106 | conditions
107 | })
108 | }
109 | }
110 | }
111 |
112 | const container = document.getElementById('main')
113 | const root = createRoot(container)
114 | root.render( )
115 |
--------------------------------------------------------------------------------
/src/modals/AccountDetailsModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import {QRCodeSVG} from 'qrcode.react'
3 | import Modal from './Modal'
4 |
5 | const AccountDetailsModal = ({ isOpen, onClose, accountData }) => {
6 | const [showModal, setShowModal] = useState(isOpen)
7 | const [account, setAccount] = useState({})
8 | const [format, setFormat] = useState('bech32')
9 | const [showSecret, setShowSecret] = useState(false)
10 |
11 | useEffect(() => {
12 | setShowModal(isOpen)
13 | }, [isOpen])
14 |
15 | useEffect(() => {
16 | setAccount(accountData)
17 | }, [accountData])
18 |
19 | const closeModal = () => {
20 | setShowModal(false)
21 | setShowSecret(false)
22 | onClose()
23 | }
24 |
25 | const copyToClipboard = (e, text) => {
26 | e.preventDefault()
27 | navigator.clipboard.writeText(text)
28 | }
29 |
30 | async function convertFormat(e) {
31 | e.preventDefault()
32 | setFormat(format === 'bech32' ? 'hex' : 'bech32')
33 | }
34 |
35 | return (
36 |
93 | )
94 | }
95 |
96 | export default AccountDetailsModal
97 |
--------------------------------------------------------------------------------
/src/modals/GenerateRandomAccountModal.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 | import * as nip19 from 'nostr-tools/nip19'
4 | import { hexToBytes, bytesToHex } from '@noble/hashes/utils'
5 | import { generateSecretKey, getPublicKey } from 'nostr-tools/pure'
6 | import { encrypt } from '../common'
7 | import Modal from './Modal'
8 |
9 | const GenerateRandomAccountModal = ({ isOpen, onClose, callBack }) => {
10 | const [showModal, setShowModal] = useState(isOpen)
11 | const [format, setFormat] = useState('bech32')
12 | const [account, setAccount] = useState({})
13 |
14 | useEffect(() => {
15 | setShowModal(isOpen)
16 |
17 | if (isOpen) {
18 | generateRandomAccount()
19 | }
20 | }, [isOpen])
21 |
22 | const generateRandomAccount = () => {
23 | const prvKey = bytesToHex(generateSecretKey())
24 | const nsec = nip19.nsecEncode(hexToBytes(prvKey))
25 | const pubKey = getPublicKey(prvKey)
26 | const npub = nip19.npubEncode(pubKey)
27 |
28 | setAccount({
29 | prvKey,
30 | nsec,
31 | pubKey,
32 | npub
33 | })
34 | }
35 |
36 | const importAccount = async () => {
37 | const storage = await browser.storage.local.get(['vault', 'password'])
38 | const vault = storage.vault
39 | vault.importedAccounts.push({ prvKey: account.prvKey })
40 | const encryptedVault = encrypt(vault, storage.password)
41 | await browser.storage.local.set({
42 | vault,
43 | encryptedVault,
44 | })
45 |
46 | callBack()
47 | closeModal()
48 | }
49 |
50 | const closeModal = () => {
51 | setShowModal(false)
52 | onClose()
53 | }
54 |
55 | const convertFormat = (e) => {
56 | e.preventDefault()
57 | setFormat(format === 'bech32' ? 'hex' : 'bech32')
58 | }
59 |
60 | const copyToClipboard = (e, text) => {
61 | e.preventDefault()
62 | navigator.clipboard.writeText(text)
63 | }
64 |
65 | return (
66 |
102 | )
103 | }
104 |
105 | export default GenerateRandomAccountModal
106 |
--------------------------------------------------------------------------------
/dist/nostr-provider.js:
--------------------------------------------------------------------------------
1 | window.nostr = {
2 | _requests: {},
3 | _pubkey: null,
4 |
5 | async getPublicKey() {
6 | if (this._pubkey) return this._pubkey
7 | this._pubkey = await this._call('getPublicKey', {})
8 | return this._pubkey
9 | },
10 |
11 | async signEvent(event) {
12 | return this._call('signEvent', {event})
13 | },
14 |
15 | async getRelays() {
16 | return {}
17 | },
18 |
19 | nip04: {
20 | async encrypt(peer, plaintext) {
21 | return window.nostr._call('nip04.encrypt', {peer, plaintext})
22 | },
23 |
24 | async decrypt(peer, ciphertext) {
25 | return window.nostr._call('nip04.decrypt', {peer, ciphertext})
26 | }
27 | },
28 |
29 | nip44: {
30 | async encrypt(peer, plaintext) {
31 | return window.nostr._call('nip44.encrypt', {peer, plaintext})
32 | },
33 |
34 | async decrypt(peer, ciphertext) {
35 | return window.nostr._call('nip44.decrypt', {peer, ciphertext})
36 | }
37 | },
38 |
39 | _call(type, params) {
40 | let id = Math.random().toString().slice(-4)
41 | console.log(
42 | '%c[Nostrame:%c' +
43 | id +
44 | '%c]%c calling %c' +
45 | type +
46 | '%c with %c' +
47 | JSON.stringify(params || {}),
48 | 'background-color:#f1b912;font-weight:bold;color:white',
49 | 'background-color:#f1b912;font-weight:bold;color:#a92727',
50 | 'background-color:#f1b912;color:white;font-weight:bold',
51 | 'color:auto',
52 | 'font-weight:bold;color:#08589d;font-family:monospace',
53 | 'color:auto',
54 | 'font-weight:bold;color:#90b12d;font-family:monospace'
55 | )
56 | return new Promise((resolve, reject) => {
57 | this._requests[id] = {resolve, reject}
58 | window.postMessage(
59 | {
60 | id,
61 | ext: 'Nostrame',
62 | type,
63 | params
64 | },
65 | '*'
66 | )
67 | })
68 | }
69 | }
70 |
71 | window.addEventListener('message', message => {
72 | if (
73 | !message.data ||
74 | message.data.response === null ||
75 | message.data.response === undefined ||
76 | message.data.ext !== 'Nostrame' ||
77 | !window.nostr._requests[message.data.id]
78 | )
79 | return
80 |
81 | if (message.data.response.error) {
82 | let error = new Error('Nostrame: ' + message.data.response.error.message)
83 | error.stack = message.data.response.error.stack
84 | window.nostr._requests[message.data.id].reject(error)
85 | } else {
86 | window.nostr._requests[message.data.id].resolve(message.data.response)
87 | }
88 |
89 | console.log(
90 | '%c[Nostrame:%c' +
91 | message.data.id +
92 | '%c]%c result: %c' +
93 | JSON.stringify(
94 | message?.data?.response || message?.data?.response?.error?.message || {}
95 | ),
96 | 'background-color:#f1b912;font-weight:bold;color:white',
97 | 'background-color:#f1b912;font-weight:bold;color:#a92727',
98 | 'background-color:#f1b912;color:white;font-weight:bold',
99 | 'color:auto',
100 | 'font-weight:bold;color:#08589d'
101 | )
102 |
103 | delete window.nostr._requests[message.data.id]
104 | })
105 |
106 | // hack to replace nostr:nprofile.../etc links with something else
107 | let replacing = null
108 | document.addEventListener('mousedown', replaceNostrSchemeLink)
109 | async function replaceNostrSchemeLink(e) {
110 | if (e.target.tagName !== 'A' || !e.target.href.startsWith('nostr:')) return
111 | if (replacing === false) return
112 |
113 | let response = await window.nostr._call('replaceURL', {url: e.target.href})
114 | if (response === false) {
115 | replacing = false
116 | return
117 | }
118 |
119 | e.target.href = response
120 | }
121 |
--------------------------------------------------------------------------------
/src/pages/SigninPage.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06'
4 | import { Link, Navigate, useNavigate } from 'react-router-dom'
5 | import { encrypt } from '../common'
6 | import { useAuth } from '../middlewares/AuthContext';
7 |
8 | const Signin = () => {
9 | const { isAuthenticated } = useAuth()
10 | if (isAuthenticated) return
11 |
12 | const [password, setPassword] = useState('')
13 | const [vault, setVault] = useState({
14 | mnemonic: '',
15 | passphrase: '',
16 | accountIndex: 0,
17 | accounts: [], // maybe change to derivedAccounts
18 | importedAccounts: [],
19 | })
20 |
21 | const { login } = useAuth()
22 | const navigate = useNavigate()
23 |
24 | useEffect(() => {
25 | browser.storage.onChanged.addListener(async function(changes, area) {
26 | if (changes.isAuthenticated) {
27 | await login()
28 | navigate('/vault')
29 | }
30 | })
31 | }, [])
32 |
33 | const handleVaultChange = (e) => {
34 | const { name, value } = e.target;
35 | setVault(prevVault => ({
36 | ...prevVault,
37 | [name]: value
38 | }))
39 | }
40 |
41 | async function saveAccount(e) {
42 | e.preventDefault()
43 |
44 | const vaultData = {
45 | mnemonic: vault.mnemonic,
46 | passphrase: vault.passphrase,
47 | accountIndex: 0,
48 | accounts: [],
49 | importedAccounts: [],
50 | }
51 |
52 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vaultData.accountIndex)
53 | vaultData.accounts.push({
54 | prvKey,
55 | })
56 |
57 | const encryptedVault = encrypt(vaultData, password)
58 | await browser.storage.local.set({
59 | vault: vaultData,
60 | encryptedVault,
61 | isAuthenticated: true,
62 | password
63 | })
64 |
65 | await login()
66 |
67 | return navigate('/vault')
68 | }
69 |
70 | const openOptionsButton = async () => {
71 | if (browser.runtime.openOptionsPage) {
72 | browser.runtime.openOptionsPage()
73 | } else {
74 | window.open(browser.runtime.getURL('options.html'))
75 | }
76 | }
77 |
78 | return (
79 | <>
80 |
122 | >
123 | )
124 | }
125 | export default Signin
126 |
--------------------------------------------------------------------------------
/src/pages/SignupPage.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { useNavigate } from 'react-router-dom'
4 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06'
5 | import { Link, Navigate } from 'react-router-dom'
6 | import { encrypt } from '../common'
7 | import { useAuth } from '../middlewares/AuthContext';
8 | import MainContext from '../contexts/MainContext'
9 |
10 | const Signup = () => {
11 | const { isAuthenticated } = useAuth()
12 | if (isAuthenticated) return
13 |
14 | const { updateAccounts } = useContext(MainContext)
15 |
16 | const [password, setPassword] = useState('')
17 | const [vault, setVault] = useState({
18 | mnemonic: '',
19 | passphrase: '',
20 | accountIndex: 0,
21 | accounts: [], // maybe change to derivedAccounts
22 | importedAccounts: [],
23 | })
24 |
25 | const { login } = useAuth()
26 | const navigate = useNavigate()
27 |
28 | useEffect(() => {
29 | handleGenerateSeedWords()
30 | }, [])
31 |
32 | const handleVaultChange = (e) => {
33 | const { name, value } = e.target;
34 | setVault(prevVault => ({
35 | ...prevVault,
36 | [name]: value
37 | }))
38 | }
39 |
40 | async function saveAccount(e) {
41 | e.preventDefault()
42 |
43 | const vaultData = {
44 | mnemonic: vault.mnemonic,
45 | passphrase: vault.passphrase,
46 | accountIndex: 0,
47 | accounts: [],
48 | importedAccounts: [],
49 | }
50 |
51 | const prvKey = privateKeyFromSeedWords(vault.mnemonic, vault.passphrase, vaultData.accountIndex)
52 | vaultData.accounts.push({
53 | prvKey,
54 | })
55 |
56 | vaultData.accountDefault = prvKey
57 |
58 | const encryptedVault = encrypt(vaultData, password)
59 | await browser.storage.local.set({
60 | vault: vaultData,
61 | encryptedVault,
62 | isAuthenticated: true,
63 | password
64 | })
65 |
66 | await login()
67 | await updateAccounts()
68 |
69 | return navigate('/vault')
70 | }
71 |
72 | const handleGenerateSeedWords = () => {
73 | let mnemonic = generateSeedWords()
74 | setVault({
75 | ...vault,
76 | mnemonic: mnemonic
77 | })
78 | }
79 |
80 | return (
81 | <>
82 |
124 | >
125 | )
126 | }
127 | export default Signup
128 |
--------------------------------------------------------------------------------
/src/assets/css/_fonts.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src: url('../onts/icomoon.eot?z8ca6h');
4 | src: url('../fonts/icomoon.eot?z8ca6h#iefix') format('embedded-opentype'),
5 | url('../fonts/icomoon.ttf?z8ca6h') format('truetype'),
6 | url('../fonts/icomoon.woff?z8ca6h') format('woff'),
7 | url('../fonts/icomoon.svg?z8ca6h#icomoon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | font-display: block;
11 | }
12 |
13 | [class^="icon-"], [class*=" icon-"] {
14 | /* use !important to prevent issues with browser extensions that change fonts */
15 | font-family: 'icomoon' !important;
16 | speak: never;
17 | font-style: normal;
18 | font-weight: normal;
19 | font-variant: normal;
20 | text-transform: none;
21 | line-height: 1;
22 |
23 | /* Better Font Rendering =========== */
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | .icon-wallet:before {
29 | content: "\e901";
30 | }
31 | .icon-money:before {
32 | content: "\e901";
33 | }
34 | .icon-wallet1:before {
35 | content: "\e902";
36 | }
37 | .icon-money1:before {
38 | content: "\e902";
39 | }
40 | .icon-cash:before {
41 | content: "\e902";
42 | }
43 | .icon-wallet2:before {
44 | content: "\e903";
45 | }
46 | .icon-dots-three-vertical:before {
47 | content: "\e900";
48 | }
49 | .icon-pencil:before {
50 | content: "\e905";
51 | }
52 | .icon-file-zip:before {
53 | content: "\e92b";
54 | }
55 | .icon-copy:before {
56 | content: "\e92c";
57 | }
58 | .icon-folder-plus:before {
59 | content: "\e931";
60 | }
61 | .icon-folder-download:before {
62 | content: "\e933";
63 | }
64 | .icon-folder-upload:before {
65 | content: "\e934";
66 | }
67 | .icon-qrcode:before {
68 | content: "\e938";
69 | }
70 | .icon-hour-glass:before {
71 | content: "\e979";
72 | }
73 | .icon-spinner3:before {
74 | content: "\e97c";
75 | }
76 | .icon-key:before {
77 | content: "\e98d";
78 | }
79 | .icon-lock:before {
80 | content: "\e98f";
81 | }
82 | .icon-unlocked:before {
83 | content: "\e990";
84 | }
85 | .icon-cog:before {
86 | content: "\e994";
87 | }
88 | .icon-lab:before {
89 | content: "\e9aa";
90 | }
91 | .icon-bin:before {
92 | content: "\e9ac";
93 | }
94 | .icon-bin2:before {
95 | content: "\e9ad";
96 | }
97 | .icon-switch:before {
98 | content: "\e9b6";
99 | }
100 | .icon-list:before {
101 | content: "\e9ba";
102 | }
103 | .icon-tree:before {
104 | content: "\e9bc";
105 | }
106 | .icon-menu:before {
107 | content: "\e9bd";
108 | }
109 | .icon-download:before {
110 | content: "\e9c5";
111 | }
112 | .icon-upload:before {
113 | content: "\e9c6";
114 | }
115 | .icon-download3:before {
116 | content: "\e9c7";
117 | }
118 | .icon-sphere:before {
119 | content: "\e9c9";
120 | }
121 | .icon-earth:before {
122 | content: "\e9ca";
123 | }
124 | .icon-eye:before {
125 | content: "\e9ce";
126 | }
127 | .icon-eye-blocked:before {
128 | content: "\e9d1";
129 | }
130 | .icon-warning:before {
131 | content: "\ea07";
132 | }
133 | .icon-notification:before {
134 | content: "\ea08";
135 | }
136 | .icon-plus:before {
137 | content: "\ea0a";
138 | }
139 | .icon-info:before {
140 | content: "\ea0c";
141 | }
142 | .icon-cancel-circle:before {
143 | content: "\ea0d";
144 | }
145 | .icon-cross:before {
146 | content: "\ea0f";
147 | }
148 | .icon-enter:before {
149 | content: "\ea13";
150 | }
151 | .icon-exit:before {
152 | content: "\ea14";
153 | }
154 | .icon-loop2:before {
155 | content: "\ea2e";
156 | }
157 | .icon-arrow-down:before {
158 | content: "\ea36";
159 | }
160 | .icon-tab:before {
161 | content: "\ea45";
162 | }
163 | .icon-move-down:before {
164 | content: "\ea47";
165 | }
166 | .icon-svg:before {
167 | content: "\eae9";
168 | }
169 | .icon-home:before {
170 | content: "\e904";
171 | }
172 | .icon-home3:before {
173 | content: "\e906";
174 | }
175 | .icon-user:before {
176 | content: "\e971";
177 | }
178 | .icon-user-plus:before {
179 | content: "\e973";
180 | }
--------------------------------------------------------------------------------
/src/modals/ImportAccountModal.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { useNavigate } from 'react-router-dom';
4 | import * as nip19 from 'nostr-tools/nip19'
5 | import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
6 | import { encrypt } from '../common'
7 | import Modal from './Modal'
8 | import MainContext from '../contexts/MainContext'
9 |
10 | const ImportAccountModal = ({ isOpen, onClose, callBack }) => {
11 | const { updateAccounts } = useContext(MainContext)
12 |
13 | const navigate = useNavigate()
14 |
15 | const [showModal, setShowModal] = useState(isOpen)
16 | const [prvKey, setPrvKey] = useState('')
17 |
18 | useEffect(() => {
19 | setShowModal(isOpen)
20 | }, [isOpen])
21 |
22 | const closeModal = () => {
23 | setShowModal(false)
24 | onClose()
25 | }
26 |
27 | const importAccount = async (e) => {
28 | e.preventDefault()
29 |
30 | const storage = await browser.storage.local.get(['vault', 'password'])
31 | const vault = storage.vault
32 |
33 | if (/^nsec/.test(prvKey)) {
34 | try {
35 | let {type, data} = nip19.decode(prvKey)
36 |
37 | if (type === 'nsec') {
38 | const prvKeyHex = bytesToHex(data)
39 | //if (!vault.importedAccounts) { vault.importedAccounts = [] }
40 | const prvKeyExist = vault.importedAccounts.find(obj => obj['prvKey'] === prvKeyHex)
41 | const prvKeyExistInDerived = vault.accounts.find(obj => obj['prvKey'] === prvKeyHex)
42 | if (prvKeyExist || prvKeyExistInDerived) {
43 | alert('Please provide a not existing private key')
44 | setPrvKey('')
45 | return false
46 | }
47 | vault.importedAccounts.push({ prvKey: prvKeyHex })
48 | vault.accountDefault = prvKeyHex
49 | const encryptedVault = encrypt(vault, storage.password)
50 | await browser.storage.local.set({
51 | vault,
52 | encryptedVault,
53 | })
54 | await updateAccounts()
55 |
56 | callBack()
57 |
58 | navigate('/vault')
59 | }
60 | } catch (e) {
61 | console.log(e)
62 | alert('Please provide a valid private key')
63 | setPrvKey('')
64 | }
65 | } else if (/^[0-9a-fA-F]+$/.test(prvKey)) {
66 | try {
67 | let prvKeyBytes = hexToBytes(prvKey)
68 | let prvKeyHex = bytesToHex(prvKeyBytes)
69 |
70 | const prvKeyExist = vault.importedAccounts.find(obj => obj['prvKey'] === prvKeyHex)
71 | const prvKeyExistInDerived = vault.accounts.find(obj => obj['prvKey'] === prvKeyHex)
72 | if (prvKeyExist || prvKeyExistInDerived) {
73 | alert('Please provide a not existing private key')
74 | setPrvKey('')
75 | return false
76 | }
77 |
78 | vault.importedAccounts.push({ prvKey: prvKeyHex })
79 | vault.accountDefault = prvKeyHex
80 | const encryptedVault = encrypt(vault, storage.password)
81 | await browser.storage.local.set({
82 | vault,
83 | encryptedVault,
84 | })
85 | await updateAccounts()
86 |
87 | callBack()
88 |
89 | navigate('/vault')
90 | } catch (e) {
91 | console.log(e)
92 | alert('Please provide a valid private key')
93 | setPrvKey('')
94 | }
95 | }
96 |
97 | setPrvKey('')
98 | }
99 |
100 | return (
101 |
102 |
103 |
104 | Import account
105 |
106 | setPrvKey(e.target.value)}
114 | />
115 |
116 | Import
117 |
118 |
119 |
120 | )
121 | }
122 |
123 | export default ImportAccountModal
124 |
--------------------------------------------------------------------------------
/src/modals/EditAccountModal.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect } from 'react'
3 | import { SimplePool } from 'nostr-tools/pool'
4 | import { finalizeEvent } from 'nostr-tools/pure'
5 | import Modal from './Modal'
6 |
7 | const EditAccountModal = ({ isOpen, onClose, accountData, callBack }) => {
8 | const pool = new SimplePool()
9 |
10 | const [showModal, setShowModal] = useState(isOpen);
11 | const [account, setAccount] = useState({
12 | name: '',
13 | about: '',
14 | picture: '',
15 | banner: '',
16 | nip05: '',
17 | lud16: '',
18 | });
19 |
20 | useEffect(() => {
21 | //setAccount(accountData)
22 | setAccount({
23 | name: accountData.name || '',
24 | about: accountData.about || '',
25 | picture: accountData.picture || '',
26 | banner: accountData.banner || '',
27 | nip05: accountData.nip05 || '',
28 | lud16: accountData.lud16 || '',
29 | })
30 | }, [accountData])
31 |
32 | useEffect(() => {
33 | setShowModal(isOpen)
34 | }, [isOpen])
35 |
36 | const closeModal = () => {
37 | setShowModal(false);
38 | //onClose()
39 | }
40 |
41 | const accountChange = (e) => {
42 | const { name, value } = e.target;
43 | setAccount(prevAccount => ({
44 | ...prevAccount,
45 | [name]: value
46 | }))
47 | }
48 |
49 | const saveAccount = async (e) => {
50 | e.preventDefault()
51 | const storage = await browser.storage.local.get(['relays'])
52 |
53 | let relays = storage.relays
54 |
55 | try {
56 | let event = {
57 | kind: 0,
58 | created_at: Math.floor(Date.now() / 1000),
59 | tags: [],
60 | content: JSON.stringify({
61 | name: account.name,
62 | display_name: account.name,
63 | about: account.about,
64 | picture: account.picture,
65 | banner: account.banner,
66 | nip05: account.nip05,
67 | lud16: account.lud16,
68 | }),
69 | }
70 |
71 | const signedEvent = finalizeEvent(event, accountData.prvKey)
72 | await Promise.any(pool.publish(relays, signedEvent))
73 |
74 | callBack()
75 | } catch (error) {
76 | console.log(error)
77 | }
78 | }
79 |
80 | return (
81 |
82 |
83 | Edit Account
84 |
85 |
93 |
94 |
102 |
103 |
111 |
112 |
120 |
121 |
129 |
130 |
138 |
139 | Save
140 |
141 |
142 |
143 | )
144 | }
145 |
146 | export default EditAccountModal
147 |
--------------------------------------------------------------------------------
/src/pages/GeneratorPage.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { useNavigate } from 'react-router-dom'
4 | import * as nip19 from 'nostr-tools/nip19'
5 | import { privateKeyFromSeedWords, generateSeedWords } from 'nostr-tools/nip06'
6 | import { hexToBytes } from '@noble/hashes/utils'
7 | import { getPublicKey } from 'nostr-tools/pure'
8 | import { encrypt } from '../common'
9 | import MainContext from '../contexts/MainContext'
10 |
11 | const GeneratorPage = () => {
12 | const { updateAccounts } = useContext(MainContext)
13 |
14 | const navigate = useNavigate()
15 |
16 | const [format, setFormat] = useState('bech32')
17 | const [account, setAccount] = useState({})
18 | const [loaded, setLoaded] = useState(false)
19 |
20 | useEffect(() => {
21 | fetchData()
22 | }, [])
23 |
24 | const fetchData = async () => {
25 | generateRandomAccount()
26 |
27 | setLoaded(true)
28 | }
29 |
30 | const generateRandomAccount = () => {
31 | const mnemonic = generateSeedWords()
32 | const prvKey = privateKeyFromSeedWords(mnemonic)
33 | const nsec = nip19.nsecEncode(hexToBytes(prvKey))
34 | const pubKey = getPublicKey(prvKey)
35 | const npub = nip19.npubEncode(pubKey)
36 |
37 | setAccount({
38 | mnemonic,
39 | prvKey,
40 | nsec,
41 | pubKey,
42 | npub
43 | })
44 | }
45 |
46 | const importAccount = async () => {
47 | const storage = await browser.storage.local.get(['vault', 'password'])
48 | const vault = storage.vault
49 | vault.importedAccounts.push({ prvKey: account.prvKey })
50 | vault.accountDefault = account.prvKey
51 | const encryptedVault = encrypt(vault, storage.password)
52 | await browser.storage.local.set({
53 | vault,
54 | encryptedVault,
55 | })
56 | await updateAccounts()
57 |
58 | navigate('/vault')
59 | }
60 |
61 | const convertFormat = (e) => {
62 | e.preventDefault()
63 | setFormat(format === 'bech32' ? 'hex' : 'bech32')
64 | }
65 |
66 | const copyToClipboard = (e, text) => {
67 | e.preventDefault()
68 | navigator.clipboard.writeText(text)
69 | }
70 |
71 | return (
72 |
130 | )
131 | }
132 |
133 | export default GeneratorPage
--------------------------------------------------------------------------------
/src/contexts/MainContext.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { createContext, useState, useEffect } from 'react'
3 | import * as nip19 from 'nostr-tools/nip19'
4 | import { hexToBytes } from '@noble/hashes/utils'
5 | import { getPublicKey } from 'nostr-tools/pure'
6 | import getIdenticon from '../helpers/identicon'
7 | import { SimplePool } from 'nostr-tools/pool'
8 |
9 | const MainContext = createContext()
10 |
11 | export const MainProvider = ({ children }) => {
12 | const [accounts, setAccounts] = useState([])
13 | const [defaultAccount, setDefaultAccount] = useState({ index: '', name: '', type: '' })
14 |
15 | const pool = new SimplePool()
16 |
17 | useEffect(() => {
18 | fetchData()
19 |
20 | browser.storage.onChanged.addListener(async (changes, area) => {
21 | if (changes.vault) {
22 | let { newValue, oldValue } = changes.vault
23 | if (newValue.accountDefault !== oldValue.accountDefault) {
24 | //fetchData()
25 | }
26 | }
27 | })
28 | }, [])
29 |
30 | useEffect(() => {
31 | browser.storage.onChanged.addListener(async (changes, area) => {
32 | if (changes.vault) {
33 | let { newValue, oldValue } = changes.vault
34 | if (newValue.accountDefault !== oldValue.accountDefault) {
35 | //const storage = await browser.storage.local.get()
36 | //const defaultAccount = accounts.find(key => key.prvKey === storage.vault.accountDefault)
37 | //setDefaultAccount(defaultAccount)
38 | //fetchData()
39 | }
40 | }
41 | })
42 | }, [accounts])
43 |
44 | const fetchData = async () => {
45 | const storage = await browser.storage.local.get()
46 |
47 | let loadAccounts = []
48 | let authors = []
49 |
50 | if (storage.isAuthenticated && !storage.isLocked) {
51 | let l = storage.vault.accounts.length
52 | for (let i = 0; i < l; i++) {
53 | const prvKey = storage.vault.accounts[i].prvKey
54 | const nsec = nip19.nsecEncode(hexToBytes(prvKey))
55 | const pubKey = getPublicKey(prvKey)
56 | const npub = nip19.npubEncode(pubKey)
57 | authors.push(pubKey)
58 | loadAccounts = [
59 | ...loadAccounts,
60 | {
61 | index: i,
62 | name: '',
63 | prvKey,
64 | nsec,
65 | pubKey,
66 | npub,
67 | picture: await UserIdenticon(pubKey),
68 | format: 'bech32',
69 | type: 'derived',
70 | }
71 | ]
72 | }
73 |
74 | let len = storage.vault.importedAccounts.length
75 | for (let i = 0; i < len; i++) {
76 | const prvKey = storage.vault.importedAccounts[i].prvKey
77 | const nsec = nip19.nsecEncode(hexToBytes(prvKey))
78 | const pubKey = getPublicKey(prvKey)
79 | const npub = nip19.npubEncode(pubKey)
80 | authors.push(pubKey)
81 | loadAccounts = [
82 | ...loadAccounts,
83 | {
84 | index: i,
85 | name: '',
86 | prvKey,
87 | nsec,
88 | pubKey,
89 | npub,
90 | picture: await UserIdenticon(pubKey),
91 | format: 'bech32',
92 | type: 'imported',
93 | }
94 | ]
95 | }
96 |
97 | let relays = storage.relays
98 | let events = await pool.querySync(relays, { kinds: [0], authors })
99 |
100 | events.forEach(async (item) => {
101 | let content = JSON.parse(item.content)
102 | let len = loadAccounts.length
103 | for (let i = 0; i < len; i++) {
104 | if (loadAccounts[i].pubKey === item.pubkey) {
105 | loadAccounts[i].name = content.display_name
106 | loadAccounts[i].about = content.about
107 | loadAccounts[i].picture = !content.picture || content.picture === '' ? loadAccounts[i].picture : content.picture
108 | loadAccounts[i].banner = !content.banner || content.banner === '' ? '' : content.banner
109 | loadAccounts[i].nip05 = content.nip05
110 | loadAccounts[i].lud16 = content.lud16
111 | }
112 | }
113 | })
114 |
115 | if (!storage.vault.accountDefault) {
116 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey
117 | }
118 | const defaultAccount = loadAccounts.find(key => key.prvKey === storage.vault.accountDefault)
119 | setDefaultAccount(defaultAccount)
120 |
121 | setAccounts(loadAccounts)
122 | }
123 | }
124 |
125 | const UserIdenticon = async ( pubkey ) => {
126 | const identicon = await getIdenticon(pubkey)
127 |
128 | return `data:image/svg+xml;base64,${identicon}`
129 | }
130 |
131 | const updateAccounts = async () => {
132 | await fetchData()
133 | }
134 |
135 | const updateDefaultAccount = async () => {
136 | const storage = await browser.storage.local.get(['vault'])
137 | const defaultAccount = accounts.find(key => key.prvKey === storage.vault.accountDefault)
138 | setDefaultAccount(defaultAccount)
139 | }
140 |
141 | return (
142 |
143 | {children}
144 |
145 | )
146 | }
147 |
148 | export default MainContext
149 |
--------------------------------------------------------------------------------
/src/assets/css/style.build.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sourceRoot":"","sources":["file:///home/ghost/projects/nostr-password-manager/extension/assets/css/style.scss","file:///home/ghost/projects/nostr-password-manager/extension/assets/css/_fonts.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_variables.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_toastContainer.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_toast.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_theme.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_closeButton.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_progressBar.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/_icons.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_bounce.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_zoom.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_flip.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_slide.scss","file:///home/ghost/projects/nostr-password-manager/node_modules/react-toastify/scss/animations/_spin.scss"],"names":[],"mappings":"CAAA,4EAaA,qBACE,sBAGF,KACE,iBACA,8BAUF,KACE,SAOF,KACE,cAQF,GACE,cACA,eAWF,GACE,uBACA,SACA,iBAQF,IACE,gCACA,cAUF,EACE,+BAQF,YACE,mBACA,0BACA,iCAOF,SAEE,mBAQF,cAGE,gCACA,cAOF,MACE,cAQF,QAEE,cACA,cACA,kBACA,wBAGF,IACE,eAGF,IACE,WAUF,IACE,kBAWF,sCAKE,oBACA,eACA,iBACA,SAQF,aAEE,iBAQF,cAEE,oBAOF,gDAIE,0BAOF,wHAIE,kBACA,UAOF,4GAIE,8BAOF,SACE,2BAUF,OACE,sBACA,cACA,cACA,eACA,UACA,mBAOF,SACE,wBAOF,SACE,cAQF,6BAEE,sBACA,UAOF,kFAEE,YAQF,cACE,6BACA,oBAOF,yCACE,wBAQF,6BACE,0BACA,aAUF,QACE,cAOF,QACE,kBAUF,SACE,aAOF,SACE,aAGF,OACE,yBACA,WACA,iBACA,YACA,kBACA,eACA,kBACA,qBACA,qBACA,qCAGF,aACE,yBAIF,EACE,cACA,qBACA,0BAGF,QACE,cC9XF,WACE,sBACA,uCACA,2OAIA,mBACA,kBACA,mBAGF,iCAEE,iCACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAGF,iCACE,YAEF,oBACE,YAEF,sBACE,YAEF,kBACE,YAEF,yBACE,YAEF,6BACE,YAEF,2BACE,YAEF,oBACE,YAEF,wBACE,YAEF,sBACE,YAEF,iBACE,YAEF,kBACE,YAEF,sBACE,YAEF,iBACE,YAEF,iBACE,YAEF,iBACE,YAEF,kBACE,YAEF,oBACE,YAEF,kBACE,YAEF,kBACE,YAEF,kBACE,YAEF,sBACE,YAEF,oBACE,YAEF,uBACE,YAEF,oBACE,YAEF,mBACE,YAEF,iBACE,YAEF,yBACE,YAEF,qBACE,YAEF,0BACE,YAEF,kBACE,YAEF,kBACE,YAEF,2BACE,YAEF,mBACE,YAEF,mBACE,YAEF,kBACE,YAEF,mBACE,YAEF,wBACE,YAEF,iBACE,YAEF,uBACE,YAEF,iBACE,YCjJF,MACE,6BACA,+BACA,+BACA,kCACA,kCACA,gCACA,uDAEA,uDACA,6DACA,6DACA,yDAEA,8BACA,8BACA,kFACA,sFACA,oFACA,wFACA,kCACA,kCACA,mCACA,gCACA,mCACA,yBAEA,qCACA,iCAGA,iCACA,oCACA,oCACA,kCAEA,kCACA,6CAGA,mHAUA,wCACA,2DACA,iEACA,iEACA,6DACA,mCC1DF,2BACE,gCACA,6DACA,eACA,YACA,kCACA,sBACA,WACA,qCACE,8BACA,gCAEF,uCACE,8BACA,SACA,2BAEF,sCACE,8BACA,kCAEF,wCACE,oCACA,gCAEF,0CACE,oCACA,SACA,2BAEF,yCACE,oCACA,kCAIJ,2CACE,2BACE,YACA,UACA,+BACA,SACA,kHAGE,6BACA,wBAEF,2HAGE,mCACA,wBAEF,gCACE,iCACA,cCxDN,iBACE,OACA,kBACA,kBACA,4CACA,sBACA,mBACA,YACA,8CACA,uCACA,aACA,8BACA,4CACA,wCACA,eACA,cAEA,UACA,gBAEA,0BACE,kBACA,WACA,sDACA,yBAEA,kIACE,uBAGF,gDACE,iBAGF,kEACE,UAGF,gCACE,WACA,kBACA,OACA,QACA,0BACA,YAGF,wCACE,MAGF,wCACE,SAGF,wEACE,qBAGF,wEACE,wBAGF,iCACE,WACA,kBACA,OACA,QACA,SACA,YACA,oBACA,WAIJ,sBACE,cAEF,iCACE,eAEF,sBACE,cACA,cACA,YACA,aACA,mBACA,qCACE,sBACA,OAGJ,sBACE,uBACA,WACA,cACA,aAIJ,mBACE,yBACA,uBAGF,wBACE,yBACA,uBAGF,2CACE,iBACE,gBACA,iBChHF,6BACE,sCACA,sCAEF,8BACE,uCACA,uCAEF,yDACE,uCACA,uCAEF,sDACE,sCACA,sCAEF,yDACE,yCACA,yCAEF,yDACE,yCACA,yCAEF,uDACE,uCACA,uCAKF,qCACE,gDAEF,oCACE,+CAEF,8BACE,+CAEF,iCACE,kDAEF,iCACE,kDAEF,+BACE,gDAEF,uRAIE,6CCtDJ,wBACE,WACA,yBACA,aACA,YACA,UACA,eACA,WACA,oBACA,sBACA,UACA,+BACE,WACA,WAGF,4BACE,kBACA,YACA,WAGF,4DAEE,UCxBJ,mCACE,GACE,oBAEF,KACE,qBAIJ,wBACE,kBACA,SACA,OACA,WACA,YACA,gCACA,WACA,sBACA,0DAEA,kCACE,oDAGF,oCACE,yBAGF,6BACE,QACA,aACA,uBACA,kCACA,2DAGF,6BACE,kBACA,SACA,OACA,WACA,WACA,0DAGF,+CACE,UAGF,4BACE,2CACA,WACA,YCpDJ,mBACE,WACA,YACA,sBACA,iBACA,mBACA,sDACA,iDACA,8CCJF,mCACE,oBAJA,8DAWA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gBAIJ,oCACE,IACE,UACA,0CAEF,GACE,UACA,4CAIJ,kCACE,oBA1CA,8DAiDA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,UACA,yCAEF,GACE,UACA,6CAIJ,gCACE,oBAhFA,8DAuFA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gCAIJ,iCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,sCAIJ,kCACE,oBA1HA,8DAiIA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,qCAKF,uEAEE,sCAEF,yEAEE,uCAEF,oCACE,sCAEF,uCACE,oCAKF,qEAEE,uCAEF,uEAEE,wCAEF,mCACE,qCAEF,sCACE,uCClMJ,4BACE,KACE,UACA,iCAEF,IACE,WAIJ,6BACE,KACE,UAEF,IACE,UACA,6DAEF,GACE,WAIJ,sBACE,gCAGF,qBACE,iCC5BF,4BACE,KACE,sDACA,kCACA,UAEF,IACE,uDACA,kCAEF,IACE,sDACA,UAEF,IACE,sDAEF,GACE,8BAIJ,6BACE,KACE,yDAEF,IACE,mFACA,UAEF,GACE,kFACA,WAIJ,sBACE,gCAGF,qBACE,iCCrCF,kCACE,KACE,kCACA,mBAEF,GARA,uCAaF,iCACE,KACE,mCACA,mBAEF,GAlBA,uCAuBF,+BACE,KACE,kCACA,mBAEF,GA5BA,uCAiCF,iCACE,KACE,mCACA,mBAEF,GAtCA,uCA2CF,mCACE,KA5CA,sCA+CA,GACE,kBACA,0CAIJ,kCACE,KAtDA,sCAyDA,GACE,kBACA,2CAIJ,kCACE,KAhEA,sCAmEA,GACE,kBACA,oCAIJ,gCACE,KA1EA,sCA6EA,GACE,kBACA,qCAUF,qEAEE,qCAEF,uEAEE,sCAEF,mCACE,qCAEF,sCACE,mCAKF,mEAEE,sCAxBF,kCACA,uBA0BA,qEAEE,uCA7BF,kCACA,uBA+BA,kCACE,oCAjCF,kCACA,uBAmCA,qCACE,sCArCF,kCACA,uBCtFF,0BACE,KACE,uBAEF,GACE,0BbgYJ,KACE,sBACA,WACA,6BACA,eAGF,QACE,sBACA,WACA,kBACA,aACA,8BACA,mBAEF,WACE,cACA,SAMF,kBACE,cAGF,oBACE,cACA,YAIF,MACE,gBACA,6BACA,aAGF,kBACE,0BAGF,WACE,aACA,8BACA,mBACA,kBAGF,cACE,qBACA,qBAGF,sCAEE,cACA,WAGF,SACE,cACA,WAGF,iBACE,cACA,WAGF,MACE,cAGF,KACE,cACA,WAGF,eACE,eACA,MACA,OACA,WACA,YACA,gCACA,aACA,uBACA,mBACA,UAGF,OACE,sBACA,WACA,aACA,kBACA,cACA,gBACA,WAGF,eACE,kBAGF,OACE,kBACA,MACA,QACA,eAIF,UACE,aACA,kBACA,qBAGF,cACE,qBACA,iBACA,WAIF,kBACE,aACA,kBACA,QACA,yBACA,gBACA,uCACA,UAIF,oBACE,WACA,kBACA,qBACA,cAIF,0BACE,sBAIF,kCACE","file":"style.build.css"}
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import CryptoJS from 'crypto-js'
3 |
4 | function deriveKey(password, salt) {
5 | const iterations = 10000
6 | const keyLength = 256
7 | return CryptoJS.PBKDF2(password, salt, { keySize: keyLength / 32, iterations: iterations })
8 | }
9 |
10 | export function encrypt(data, password) {
11 | const salt = CryptoJS.lib.WordArray.random(128 / 8)
12 | const derivedKey = deriveKey(password, salt)
13 | const iv = CryptoJS.lib.WordArray.random(128 / 8)
14 | const encrypted = CryptoJS.AES.encrypt(
15 | JSON.stringify(data),
16 | derivedKey,
17 | { iv: iv }
18 | )
19 |
20 | // Convert the salt, IV, and encrypted data to a single string
21 | return salt.toString() + iv.toString() + encrypted.toString()
22 | }
23 |
24 | export function decrypt(encryptedData, password) {
25 | const salt = CryptoJS.enc.Hex.parse(encryptedData.substring(0, 32))
26 | const iv = CryptoJS.enc.Hex.parse(encryptedData.substring(32, 64))
27 | const encrypted = encryptedData.substring(64)
28 | const derivedKey = deriveKey(password, salt)
29 | const decrypted = CryptoJS.AES.decrypt(encrypted, derivedKey, { iv: iv })
30 |
31 | return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
32 | }
33 |
34 | export const NO_PERMISSIONS_REQUIRED = {
35 | replaceURL: true
36 | }
37 |
38 | export const PERMISSION_NAMES = Object.fromEntries([
39 | ['getPublicKey', 'read your public key'],
40 | ['signEvent', 'sign events using your private key'],
41 | ['nip04.encrypt', 'encrypt messages to peers'],
42 | ['nip04.decrypt', 'decrypt messages from peers'],
43 | ['nip44.encrypt', 'encrypt messages to peers'],
44 | ['nip44.decrypt', 'decrypt messages from peers']
45 | ])
46 |
47 | function matchConditions(conditions, event) {
48 | if (conditions?.kinds) {
49 | if (event.kind in conditions.kinds) return true
50 | else return false
51 | }
52 |
53 | return true
54 | }
55 |
56 | export async function getPermissionStatus(host, type, event) {
57 | let {policies} = await browser.storage.local.get('policies')
58 |
59 | let answers = [true, false]
60 | for (let i = 0; i < answers.length; i++) {
61 | let accept = answers[i]
62 | let {conditions} = policies?.[host]?.[accept]?.[type] || {}
63 |
64 | if (conditions) {
65 | if (type === 'signEvent') {
66 | if (matchConditions(conditions, event)) {
67 | return accept // may be true or false
68 | } else {
69 | // if this doesn't match we just continue so it will either match for the opposite answer (reject)
70 | // or it will end up returning undefined at the end
71 | continue
72 | }
73 | } else {
74 | return accept // may be true or false
75 | }
76 | }
77 | }
78 |
79 | return undefined
80 | }
81 |
82 | export async function updatePermission(host, type, accept, conditions) {
83 | let {policies = {}} = await browser.storage.local.get('policies')
84 |
85 | // if the new conditions is "match everything", override the previous
86 | if (Object.keys(conditions).length === 0) {
87 | conditions = {}
88 | } else {
89 | // if we already had a policy for this, merge the conditions
90 | let existingConditions = policies[host]?.[accept]?.[type]?.conditions
91 | if (existingConditions) {
92 | if (existingConditions.kinds && conditions.kinds) {
93 | Object.keys(existingConditions.kinds).forEach(kind => {
94 | conditions.kinds[kind] = true
95 | })
96 | }
97 | }
98 | }
99 |
100 | // if we have a reverse policy (accept / reject) that is exactly equal to this, remove it
101 | let other = !accept
102 | let reverse = policies?.[host]?.[other]?.[type]
103 | if (
104 | reverse &&
105 | JSON.stringify(reverse.conditions) === JSON.stringify(conditions)
106 | ) {
107 | delete policies[host][other][type]
108 | }
109 |
110 | // insert our new policy
111 | policies[host] = policies[host] || {}
112 | policies[host][accept] = policies[host][accept] || {}
113 | policies[host][accept][type] = {
114 | conditions, // filter that must match the event (in case of signEvent)
115 | created_at: Math.round(Date.now() / 1000)
116 | }
117 |
118 | browser.storage.local.set({policies})
119 | }
120 |
121 | export async function removePermissions(host, accept, type) {
122 | let {policies = {}} = await browser.storage.local.get('policies')
123 | delete policies[host]?.[accept]?.[type]
124 | browser.storage.local.set({policies})
125 | }
126 |
127 | export async function showNotification(host, answer, type, params) {
128 | let {notifications} = await browser.storage.local.get('notifications')
129 | if (notifications) {
130 | let action = answer ? 'allowed' : 'denied'
131 | browser.notifications.create(undefined, {
132 | type: 'basic',
133 | title: `${type} ${action} for ${host}`,
134 | message: JSON.stringify(
135 | params?.event
136 | ? {
137 | kind: params.event.kind,
138 | content: params.event.content,
139 | tags: params.event.tags
140 | }
141 | : params,
142 | null,
143 | 2
144 | ),
145 | iconUrl: 'icons/48x48.png'
146 | })
147 | }
148 | }
149 |
150 | export async function getPosition(width, height) {
151 | let left = 0
152 | let top = 0
153 |
154 | try {
155 | const lastFocused = await browser.windows.getLastFocused()
156 |
157 | if (
158 | lastFocused &&
159 | lastFocused.top !== undefined &&
160 | lastFocused.left !== undefined &&
161 | lastFocused.width !== undefined &&
162 | lastFocused.height !== undefined
163 | ) {
164 | // Position window in the center of the lastFocused window
165 | top = Math.round(lastFocused.top + (lastFocused.height - height) / 2)
166 | left = Math.round(lastFocused.left + (lastFocused.width - width) / 2)
167 | } else {
168 | console.error('Last focused window properties are undefined.')
169 | }
170 | } catch (error) {
171 | console.error('Error getting window position:', error)
172 | }
173 |
174 | return {
175 | top,
176 | left
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/dist/assets/css/style.build.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sourceRoot":"","sources":["file:///home/pater/projects/nostrame/src/assets/css/style.scss","file:///home/pater/projects/nostrame/src/assets/css/_fonts.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_variables.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_toastContainer.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_toast.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_theme.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_closeButton.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_progressBar.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/_icons.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_bounce.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_zoom.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_flip.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_slide.scss","file:///home/pater/projects/nostrame/node_modules/react-toastify/scss/animations/_spin.scss"],"names":[],"mappings":"CAAA,4EAaA,qBACE,sBAGF,KACE,iBACA,8BAUF,KACE,SAOF,KACE,cAQF,GACE,cACA,eAWF,GACE,uBACA,SACA,iBAQF,IACE,gCACA,cAUF,EACE,+BAQF,YACE,mBACA,0BACA,iCAOF,SAEE,mBAQF,cAGE,gCACA,cAOF,MACE,cAQF,QAEE,cACA,cACA,kBACA,wBAGF,IACE,eAGF,IACE,WAUF,IACE,kBAWF,sCAKE,oBACA,eACA,iBACA,SAQF,aAEE,iBAQF,cAEE,oBAOF,gDAIE,0BAOF,wHAIE,kBACA,UAOF,4GAIE,8BAOF,SACE,2BAUF,OACE,sBACA,cACA,cACA,eACA,UACA,mBAOF,SACE,wBAOF,SACE,cAQF,6BAEE,sBACA,UAOF,kFAEE,YAQF,cACE,6BACA,oBAOF,yCACE,wBAQF,6BACE,0BACA,aAUF,QACE,cAOF,QACE,kBAUF,SACE,aAOF,SACE,aAGF,OACE,yBACA,WACA,iBACA,YACA,kBACA,eACA,kBACA,qBACA,qBACA,qCAGF,aACE,yBAIF,EACE,cACA,qBACA,0BAGF,QACE,cC9XF,WACE,sBACA,sCACA,2OAIA,mBACA,kBACA,mBAGF,iCAEE,iCACA,YACA,kBACA,mBACA,oBACA,oBACA,cAGA,mCACA,kCAGF,oBACE,YAEF,mBACE,YAEF,qBACE,YAEF,oBACE,YAEF,kBACE,YAEF,qBACE,YAEF,iCACE,YAEF,oBACE,YAEF,sBACE,YAEF,kBACE,YAEF,yBACE,YAEF,6BACE,YAEF,2BACE,YAEF,oBACE,YAEF,wBACE,YAEF,sBACE,YAEF,iBACE,YAEF,kBACE,YAEF,sBACE,YAEF,iBACE,YAEF,iBACE,YAEF,iBACE,YAEF,kBACE,YAEF,oBACE,YAEF,kBACE,YAEF,kBACE,YAEF,kBACE,YAEF,sBACE,YAEF,oBACE,YAEF,uBACE,YAEF,oBACE,YAEF,mBACE,YAEF,iBACE,YAEF,yBACE,YAEF,qBACE,YAEF,0BACE,YAEF,kBACE,YAEF,kBACE,YAEF,2BACE,YAEF,mBACE,YAEF,mBACE,YAEF,kBACE,YAEF,mBACE,YAEF,wBACE,YAEF,iBACE,YAEF,uBACE,YAEF,iBACE,YAEF,kBACE,YAEF,mBACE,YAEF,kBACE,YAEF,uBACE,YC/KF,MACE,6BACA,+BACA,+BACA,kCACA,kCACA,gCACA,uDAEA,uDACA,6DACA,6DACA,yDAEA,8BACA,8BACA,kFACA,sFACA,oFACA,wFACA,kCACA,kCACA,mCACA,gCACA,mCACA,yBAEA,qCACA,iCAGA,iCACA,oCACA,oCACA,kCAEA,kCACA,6CAGA,mHAUA,wCACA,2DACA,iEACA,iEACA,6DACA,mCC1DF,2BACE,gCACA,6DACA,eACA,YACA,kCACA,sBACA,WACA,qCACE,8BACA,gCAEF,uCACE,8BACA,SACA,2BAEF,sCACE,8BACA,kCAEF,wCACE,oCACA,gCAEF,0CACE,oCACA,SACA,2BAEF,yCACE,oCACA,kCAIJ,2CACE,2BACE,YACA,UACA,+BACA,SACA,kHAGE,6BACA,wBAEF,2HAGE,mCACA,wBAEF,gCACE,iCACA,cCxDN,iBACE,OACA,kBACA,kBACA,4CACA,sBACA,mBACA,YACA,8CACA,uCACA,aACA,8BACA,4CACA,wCACA,eACA,cAEA,UACA,gBAEA,0BACE,kBACA,WACA,sDACA,yBAEA,kIACE,uBAGF,gDACE,iBAGF,kEACE,UAGF,gCACE,WACA,kBACA,OACA,QACA,0BACA,YAGF,wCACE,MAGF,wCACE,SAGF,wEACE,qBAGF,wEACE,wBAGF,iCACE,WACA,kBACA,OACA,QACA,SACA,YACA,oBACA,WAIJ,sBACE,cAEF,iCACE,eAEF,sBACE,cACA,cACA,YACA,aACA,mBACA,qCACE,sBACA,OAGJ,sBACE,uBACA,WACA,cACA,aAIJ,mBACE,yBACA,uBAGF,wBACE,yBACA,uBAGF,2CACE,iBACE,gBACA,iBChHF,6BACE,sCACA,sCAEF,8BACE,uCACA,uCAEF,yDACE,uCACA,uCAEF,sDACE,sCACA,sCAEF,yDACE,yCACA,yCAEF,yDACE,yCACA,yCAEF,uDACE,uCACA,uCAKF,qCACE,gDAEF,oCACE,+CAEF,8BACE,+CAEF,iCACE,kDAEF,iCACE,kDAEF,+BACE,gDAEF,uRAIE,6CCtDJ,wBACE,WACA,yBACA,aACA,YACA,UACA,eACA,WACA,oBACA,sBACA,UACA,+BACE,WACA,WAGF,4BACE,kBACA,YACA,WAGF,4DAEE,UCxBJ,mCACE,GACE,oBAEF,KACE,qBAIJ,wBACE,kBACA,SACA,OACA,WACA,YACA,gCACA,WACA,sBACA,0DAEA,kCACE,oDAGF,oCACE,yBAGF,6BACE,QACA,aACA,uBACA,kCACA,2DAGF,6BACE,kBACA,SACA,OACA,WACA,WACA,0DAGF,+CACE,UAGF,4BACE,2CACA,WACA,YCpDJ,mBACE,WACA,YACA,sBACA,iBACA,mBACA,sDACA,iDACA,8CCJF,mCACE,oBAJA,8DAWA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gBAIJ,oCACE,IACE,UACA,0CAEF,GACE,UACA,4CAIJ,kCACE,oBA1CA,8DAiDA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,UACA,yCAEF,GACE,UACA,6CAIJ,gCACE,oBAhFA,8DAuFA,KACE,UACA,oCAEF,IACE,UACA,mCAEF,IACE,kCAEF,IACE,kCAEF,GACE,gCAIJ,iCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,sCAIJ,kCACE,oBA1HA,8DAiIA,GACE,UACA,qCAEF,IACE,UACA,kCAEF,IACE,mCAEF,IACE,iCAEF,GACE,gBAIJ,mCACE,IACE,mDAEF,QAEE,UACA,mDAEF,GACE,UACA,qCAKF,uEAEE,sCAEF,yEAEE,uCAEF,oCACE,sCAEF,uCACE,oCAKF,qEAEE,uCAEF,uEAEE,wCAEF,mCACE,qCAEF,sCACE,uCClMJ,4BACE,KACE,UACA,iCAEF,IACE,WAIJ,6BACE,KACE,UAEF,IACE,UACA,6DAEF,GACE,WAIJ,sBACE,gCAGF,qBACE,iCC5BF,4BACE,KACE,sDACA,kCACA,UAEF,IACE,uDACA,kCAEF,IACE,sDACA,UAEF,IACE,sDAEF,GACE,8BAIJ,6BACE,KACE,yDAEF,IACE,mFACA,UAEF,GACE,kFACA,WAIJ,sBACE,gCAGF,qBACE,iCCrCF,kCACE,KACE,kCACA,mBAEF,GARA,uCAaF,iCACE,KACE,mCACA,mBAEF,GAlBA,uCAuBF,+BACE,KACE,kCACA,mBAEF,GA5BA,uCAiCF,iCACE,KACE,mCACA,mBAEF,GAtCA,uCA2CF,mCACE,KA5CA,sCA+CA,GACE,kBACA,0CAIJ,kCACE,KAtDA,sCAyDA,GACE,kBACA,2CAIJ,kCACE,KAhEA,sCAmEA,GACE,kBACA,oCAIJ,gCACE,KA1EA,sCA6EA,GACE,kBACA,qCAUF,qEAEE,qCAEF,uEAEE,sCAEF,mCACE,qCAEF,sCACE,mCAKF,mEAEE,sCAxBF,kCACA,uBA0BA,qEAEE,uCA7BF,kCACA,uBA+BA,kCACE,oCAjCF,kCACA,uBAmCA,qCACE,sCArCF,kCACA,uBCtFF,0BACE,KACE,uBAEF,GACE,0BbgYJ,KACE,sBACA,WACA,6BACA,eACA,cACA,kBACA,aACA,YAGF,QACE,yBACA,WACA,aACA,aACA,8BACA,mBACA,WACE,SAGF,UACE,WAEA,gBACE,2BAQN,SACE,kBAGF,kBACE,mBACA,aACA,kBACA,QACA,aAIA,8BACE,6BAEF,gCACE,gBAIJ,eACE,sCACA,kBACA,aAGF,wBACE,mBACA,aACA,8BACA,mBAGF,sBACE,gBACA,eACA,gBAEA,yBACE,kBACA,oCACE,gBAKN,iBACE,aACA,2BACA,mBAGF,uBACE,OACA,iBAGF,wBACE,iBAGF,sBACE,kBACA,cACA,YACA,mBACA,WAGF,SACE,wBACA,kBAGF,iBACE,qDACA,2BACA,wBACA,aACA,kBACA,MACA,OACA,WAGF,cACE,kBACA,sBACA,mBACA,YACA,aAGF,eACE,oBACA,iBACA,kBAGF,cACE,gBACA,SACA,UACA,kBACA,QACA,SACA,kBAEA,iBACE,qBACA,iBACA,WAEA,mBACE,WACA,cAQN,kBACE,cAGF,oBACE,cACA,YAIF,MACE,gBACA,6BACA,aAGF,kBACE,0BAGF,WACE,aACA,8BACA,mBACA,kBAGF,cACE,qBACA,qBAGF,+CAGE,cACA,WAGF,SACE,cACA,WAGF,iBACE,cACA,WAGF,MACE,cAGF,KACE,yBACA,WACA,cACA,iBACA,YACA,kBACA,kBACA,qBACA,qCACA,WAGF,WACE,yBAGF,eACE,eACA,MACA,OACA,WACA,YACA,gCACA,aACA,uBACA,mBACA,UAGF,OACE,sBACA,WACA,aACA,kBACA,cACA,gBACA,WAGF,eACE,kBAGF,OACE,kBACA,MACA,QACA,eAIF,UACE,aACA,kBACA,qBAGF,cACE,qBACA,iBACA,WAIF,kBACE,aACA,kBACA,QACA,yBACA,gBACA,uCACA,UAIF,oBACE,WACA,kBACA,qBACA,cAIF,0BACE,sBAIF,kCACE,cAGF,MACE,oBAGF,MACE,yBACA,eACA,SACA,OACA,WAGF,UACE,aACA,eACA,6BACA,mBACA,WACA,SACA,kBACA,kBAEA,YACE,qBACA,eACA,kBAIJ,aACE,gBACA,SACA,UAGF,aACE,cACA,WAGF,YACE,WACA,cACA,qBAGF,UACE,oBAGF,cACE,aAGF,iBACE,aACA,8BACA,uBACA,mBAGF,kCACI,aAGJ,iCACI,aACA,aACA","file":"style.build.css"}
--------------------------------------------------------------------------------
/src/components/Accounts.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { useNavigate } from 'react-router-dom'
4 | import { SimplePool } from 'nostr-tools/pool'
5 | import hideStringMiddle from '../helpers/hideStringMiddle'
6 | import ImportAccountModal from '../modals/ImportAccountModal'
7 | import DeriveAccountModal from '../modals/DeriveAccountModal'
8 | import MainContext from '../contexts/MainContext'
9 |
10 | const Accounts = () => {
11 | const { accounts, defaultAccount, updateDefaultAccount } = useContext(MainContext)
12 |
13 | //const [accounts, setAccounts] = useState([])
14 | //const [defaultAccount, setDefaultAccount] = useState({ index: '', name: '', type: '' })
15 | const [isDropdownOpen, setIsDropdownOpen] = useState(false)
16 | const [showImportAccountModal, setShowImportAccountModal] = useState(false)
17 | const [showDeriveAccount, setShowDeriveAccount] = useState(false)
18 |
19 | const pool = new SimplePool()
20 | const navigate = useNavigate()
21 |
22 | useEffect(() => {
23 | //browser.storage.onChanged.addListener(function(changes, area) {
24 | // let { newValue, oldValue } = changes.vault
25 | // if (newValue.accountDefault !== oldValue.accountDefault) {
26 | // console.log(1)
27 | // fetchData()
28 | // }
29 | //})
30 | }, [])
31 |
32 | useEffect(() => {
33 | if (accounts.length) {
34 | fetchData()
35 | }
36 | }, [accounts])
37 |
38 | const fetchData = async () => {
39 | const storage = await browser.storage.local.get()
40 |
41 | if (storage.isAuthenticated && !storage.isLocked) {
42 | if (!storage.vault.accountDefault) {
43 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey
44 | }
45 | }
46 | }
47 |
48 | const changeDefaultAccount = async (prvKey) => {
49 | const { vault: vaultData } = await browser.storage.local.get(['vault'])
50 | vaultData.accountDefault = prvKey
51 |
52 | await browser.storage.local.set({
53 | vault: vaultData,
54 | })
55 | updateDefaultAccount()
56 | fetchData()
57 | toggleDropdown()
58 | navigate('/vault')
59 | }
60 |
61 | const toggleDropdown = () => {
62 | setIsDropdownOpen(!isDropdownOpen)
63 | }
64 |
65 | const lockVault = async () => {
66 | await browser.storage.local.set({
67 | isLocked: true,
68 | vault: {
69 | accounts: [],
70 | },
71 | password: '',
72 | })
73 | window.location.reload()
74 | }
75 |
76 | const deriveAccountCallback = () => {
77 | setShowDeriveAccount(false)
78 | //fetchData()
79 | }
80 |
81 | const importAccountCallback = () => {
82 | setShowImportAccountModal(false)
83 | //fetchData()
84 | }
85 |
86 | return (
87 | <>
88 |
176 | >
177 | )
178 | }
179 | export default Accounts
180 |
--------------------------------------------------------------------------------
/src/background.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import {validateEvent, finalizeEvent, getPublicKey} from 'nostr-tools/pure'
3 | import * as nip19 from 'nostr-tools/nip19'
4 | import * as nip04 from 'nostr-tools/nip04'
5 | import * as nip44 from 'nostr-tools/nip44'
6 | import {Mutex} from 'async-mutex'
7 | import {LRUCache} from './utils'
8 |
9 | import {
10 | NO_PERMISSIONS_REQUIRED,
11 | getPermissionStatus,
12 | updatePermission,
13 | showNotification,
14 | getPosition
15 | } from './common'
16 |
17 | let openPrompt = null
18 | let promptMutex = new Mutex()
19 | let releasePromptMutex = () => {}
20 | let secretsCache = new LRUCache(100)
21 | let accountDefault = null
22 | let previousSk = null
23 |
24 | function getSharedSecret(sk, peer) {
25 | // Detect a key change and erase the cache if they changed their key
26 | if (previousSk !== sk) {
27 | secretsCache.clear()
28 | }
29 |
30 | let key = secretsCache.get(peer)
31 |
32 | if (!key) {
33 | key = nip44.v2.utils.getConversationKey(sk, peer)
34 | secretsCache.set(peer, key)
35 | }
36 |
37 | return key
38 | }
39 |
40 | //set the width and height of the prompt window
41 | const width = 340
42 | const height = 360
43 |
44 | // Function to handle when the extension is installed or updated
45 | browser.runtime.onInstalled.addListener(async (_, __, reason) => {
46 | if (reason === 'install') browser.runtime.openOptionsPage()
47 |
48 | await browser.storage.local.set({
49 | "relays": [
50 | "wss://relay.damus.io",
51 | "wss://nos.lol",
52 | "wss://nostr.bitcoiner.social",
53 | "wss://offchain.pub",
54 | ]
55 | })
56 | })
57 |
58 | browser.runtime.onMessage.addListener(async (message, sender) => {
59 | if (message.openSignUp) {
60 | openSignUpWindow()
61 | browser.windows.remove(sender.tab.windowId)
62 | } else {
63 | let {prompt} = message
64 | if (prompt) {
65 | handlePromptMessage(message, sender)
66 | } else {
67 | return handleContentScriptMessage(message)
68 | }
69 | }
70 | })
71 |
72 | browser.runtime.onMessageExternal.addListener(
73 | async ({type, params}, sender) => {
74 | let extensionId = new URL(sender.url).host
75 | return handleContentScriptMessage({type, params, host: extensionId})
76 | }
77 | )
78 |
79 | browser.windows.onRemoved.addListener(_ => {
80 | if (openPrompt) {
81 | // calling this with a simple "no" response will not store anything, so it's fine
82 | // it will just return a failure
83 | handlePromptMessage({accept: false}, null)
84 | }
85 | })
86 |
87 | browser.storage.onChanged.addListener((changes, area) => {
88 | for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
89 | if (key === 'vault') {
90 | accountDefault = newValue.accountDefault
91 | }
92 | }
93 | })
94 |
95 | async function handleContentScriptMessage({type, params, host}) {
96 | if (NO_PERMISSIONS_REQUIRED[type]) {
97 | // authorized, and we won't do anything with private key here, so do a separate handler
98 | switch (type) {
99 | case 'replaceURL': {
100 | let {protocol_handler: ph} = await browser.storage.local.get([
101 | 'protocol_handler'
102 | ])
103 | if (!ph) return false
104 |
105 | let {url} = params
106 | let raw = url.split('nostr:')[1]
107 | let {type, data} = nip19.decode(raw)
108 | let replacements = {
109 | raw,
110 | hrp: type,
111 | hex:
112 | type === 'npub' || type === 'note'
113 | ? data
114 | : type === 'nprofile'
115 | ? data.pubkey
116 | : type === 'nevent'
117 | ? data.id
118 | : null,
119 | p_or_e: {npub: 'p', note: 'e', nprofile: 'p', nevent: 'e'}[type],
120 | u_or_n: {npub: 'u', note: 'n', nprofile: 'u', nevent: 'n'}[type],
121 | relay0: type === 'nprofile' ? data.relays[0] : null,
122 | relay1: type === 'nprofile' ? data.relays[1] : null,
123 | relay2: type === 'nprofile' ? data.relays[2] : null
124 | }
125 | let result = ph
126 | Object.entries(replacements).forEach(([pattern, value]) => {
127 | result = result.replace(new RegExp(`{ *${pattern} *}`, 'g'), value)
128 | })
129 |
130 | return result
131 | }
132 | }
133 |
134 | return
135 | } else {
136 | // acquire mutex here before reading policies
137 | releasePromptMutex = await promptMutex.acquire()
138 |
139 | let allowed = await getPermissionStatus(
140 | host,
141 | type,
142 | type === 'signEvent' ? params.event : undefined
143 | )
144 |
145 | if (allowed === true) {
146 | // authorized, proceed
147 | releasePromptMutex()
148 | showNotification(host, allowed, type, params)
149 | } else if (allowed === false) {
150 | // denied, just refuse immediately
151 | releasePromptMutex()
152 | showNotification(host, allowed, type, params)
153 | return {
154 | error: {message: 'denied'}
155 | }
156 | } else {
157 | // ask for authorization
158 | try {
159 | let id = Math.random().toString().slice(4)
160 | let qs = new URLSearchParams({
161 | host,
162 | id,
163 | params: JSON.stringify(params),
164 | type
165 | })
166 | // center prompt
167 | const {top, left} = await getPosition(width, height)
168 | // prompt will be resolved with true or false
169 | let accept = await new Promise((resolve, reject) => {
170 | openPrompt = {resolve, reject}
171 |
172 | browser.windows.create({
173 | url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
174 | type: 'popup',
175 | width: width,
176 | height: height,
177 | top: top,
178 | left: left
179 | })
180 | })
181 |
182 | // denied, stop here
183 | if (!accept) return {error: {message: 'denied'}}
184 | } catch (err) {
185 | // errored, stop here
186 | releasePromptMutex()
187 | return {
188 | error: {message: error.message, stack: error.stack}
189 | }
190 | }
191 | }
192 | }
193 |
194 | // if we're here this means it was accepted
195 | let { vault } = await browser.storage.local.get(['vault'])
196 | if (!vault || !vault.accountDefault) {
197 | return {error: {message: 'no private key found'} }
198 | }
199 |
200 | accountDefault = vault.accountDefault
201 |
202 | try {
203 | switch (type) {
204 | case 'getPublicKey': {
205 | return getPublicKey(accountDefault)
206 | }
207 | case 'signEvent': {
208 | const event = finalizeEvent(params.event, accountDefault)
209 |
210 | return validateEvent(event)
211 | ? event
212 | : {error: {message: 'invalid event'}}
213 | }
214 | case 'nip04.encrypt': {
215 | let {peer, plaintext} = params
216 | return nip04.encrypt(accountDefault, peer, plaintext)
217 | }
218 | case 'nip04.decrypt': {
219 | let {peer, ciphertext} = params
220 | return nip04.decrypt(accountDefault, peer, ciphertext)
221 | }
222 | case 'nip44.encrypt': {
223 | const {peer, plaintext} = params
224 | const key = getSharedSecret(accountDefault, peer)
225 |
226 | return nip44.v2.encrypt(plaintext, key)
227 | }
228 | case 'nip44.decrypt': {
229 | const {peer, ciphertext} = params
230 | const key = getSharedSecret(accountDefault, peer)
231 |
232 | return nip44.v2.decrypt(ciphertext, key)
233 | }
234 | }
235 | } catch (error) {
236 | return {error: {message: error.message, stack: error.stack}}
237 | }
238 | }
239 |
240 | async function handlePromptMessage({host, type, accept, conditions}, sender) {
241 | // return response
242 | openPrompt?.resolve?.(accept)
243 |
244 | // update policies
245 | if (conditions) {
246 | await updatePermission(host, type, accept, conditions)
247 | }
248 |
249 | // cleanup this
250 | openPrompt = null
251 |
252 | // release mutex here after updating policies
253 | releasePromptMutex()
254 |
255 | // close prompt
256 | if (sender) {
257 | browser.windows.remove(sender.tab.windowId)
258 | }
259 | }
260 |
261 | async function openSignUpWindow() {
262 | const {top, left} = await getPosition(width, height)
263 |
264 | browser.windows.create({
265 | url: `${browser.runtime.getURL('signup.html')}`,
266 | type: 'popup',
267 | width: width,
268 | height: height,
269 | top: top,
270 | left: left
271 | })
272 | }
273 |
--------------------------------------------------------------------------------
/src/pages/VaultPage.jsx:
--------------------------------------------------------------------------------
1 | import browser from 'webextension-polyfill'
2 | import React, { useState, useEffect, useContext } from 'react'
3 | import { useNavigate } from 'react-router-dom'
4 | import getIdenticon from '../helpers/identicon'
5 | import hideStringMiddle from '../helpers/hideStringMiddle'
6 | import copyToClipboard from '../helpers/copyToClipboard'
7 | import EditAccountModal from '../modals/EditAccountModal'
8 | import AccountDetailsModal from '../modals/AccountDetailsModal'
9 | import MainContext from '../contexts/MainContext'
10 | import { encrypt, removePermissions } from '../common'
11 |
12 | const VaultPage = () => {
13 | const { accounts, defaultAccount, updateAccounts, updateDefaultAccount } = useContext(MainContext)
14 |
15 | const navigate = useNavigate()
16 |
17 | const [showEditAccountModal, setEditAccountModal] = useState(false)
18 | const [accountEditing, setAccountEditing] = useState({})
19 | const [accountDetails, setAccountDetails] = useState({})
20 | const [showAccountDetails, setShowAccountDetails] = useState(false)
21 | const [policies, setPermissions] = useState([])
22 | const [accountFormat, setAccountFormat] = useState('bech32')
23 | const [loaded, setLoaded] = useState(false)
24 |
25 | useEffect(() => {
26 | updateAccounts()
27 | loadPermissions()
28 | browser.storage.onChanged.addListener(function(changes, area) {
29 | for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
30 | if (key === 'isAuthenticated') {
31 | window.location.reload()
32 | }
33 | }
34 | })
35 | }, [])
36 |
37 | useEffect(() => {
38 | if (accounts.length) {
39 | fetchData()
40 |
41 | browser.storage.onChanged.addListener(function(changes, area) {
42 | //let { newValue, oldValue } = changes.vault
43 | //if (newValue.accountDefault !== oldValue.accountDefault) {
44 | // //setLoaded(false)
45 | // //fetchData()
46 | //}
47 | })
48 | }
49 | }, [accounts])
50 |
51 | const fetchData = async () => {
52 | const storage = await browser.storage.local.get(['vault', 'isAuthenticated'])
53 |
54 | if (!storage.isAuthenticated) {
55 | window.location.reload()
56 | }
57 |
58 | if (!storage.vault.accountDefault) {
59 | storage.vault.accountDefault = storage.vault.accounts[0].prvKey
60 | }
61 | setLoaded(true)
62 | }
63 |
64 | async function loadPermissions() {
65 | let { policies = {} } = await browser.storage.local.get('policies')
66 | const hostData = {}
67 |
68 | Object.entries(policies).forEach(([host, accepts]) => {
69 | Object.entries(accepts).some(([accept, types]) => {
70 | if (Object.keys(types).length === 0) {
71 | return
72 | }
73 | hostData[host] = hostData[host] || []
74 | Object.entries(types).forEach(([type, { conditions, created_at }]) => {
75 | hostData[host].push({
76 | type,
77 | accept,
78 | conditions,
79 | created_at
80 | })
81 | })
82 | })
83 | })
84 |
85 | setPermissions(hostData)
86 | }
87 |
88 | async function handleRevoke(host, accept, type) {
89 | if (
90 | window.confirm(
91 | `revoke all ${accept === 'true' ? 'accept' : 'deny'
92 | } ${type} policies from ${host}?`
93 | )
94 | ) {
95 | await removePermissions(host, accept, type)
96 | //showMessage('removed policies') add toast
97 | loadPermissions()
98 | }
99 | }
100 |
101 | const editAccountCallback = async () => {
102 | setEditAccountModal(false)
103 | await updateAccounts()
104 | fetchData()
105 | }
106 |
107 | const UserIdenticon = async ( pubkey ) => {
108 | const identicon = await getIdenticon(pubkey)
109 |
110 | return `data:image/svg+xml;base64,${identicon}`
111 | }
112 |
113 | async function convertFormat(e) {
114 | e.preventDefault()
115 | setAccountFormat(accountFormat === 'bech32' ? 'hex' : 'bech32')
116 | }
117 |
118 | const deleteImportedAccount = async (prvKey) => {
119 | if (confirm("Are you sure you want to delete this account? Make sure if you have made a backup before you continue.")) {
120 | const { vault, password } = await browser.storage.local.get(['vault', 'password'])
121 |
122 | const index = vault.importedAccounts.findIndex(item => item.prvKey === prvKey)
123 | if (index !== -1) {
124 | vault.importedAccounts.splice(index, 1)
125 |
126 | vault.accountDefault = undefined
127 |
128 | const encryptedVault = encrypt(vault, password)
129 | await browser.storage.local.set({ encryptedVault, vault })
130 |
131 | await updateAccounts()
132 | fetchData()
133 | }
134 | }
135 | }
136 |
137 | return (
138 |
139 | <>
140 | {loaded ? (
141 | <>
142 |
143 | { defaultAccount.banner === '' ? (
144 |
145 | ) : (
146 |
147 | )}
148 |
149 |
150 |
188 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
238 |
239 |
240 |
241 | {!!Object.values(policies).length && (
242 | Object.entries(policies).map(([host, permissions]) => (
243 |
244 |
{host}
245 | {permissions.map((permission, index) => (
246 |
247 |
248 |
249 |
250 | {permission.type}
251 |
252 | {permission.accept === 'true' ? 'allow' : 'deny'}
253 |
254 | {permission.conditions.kinds
255 | ? `kinds: ${Object.keys(permission.conditions.kinds).join(', ')}`
256 | : 'always'}
257 |
258 |
259 | {new Date(permission.created_at * 1000)
260 | .toISOString()
261 | .split('.')[0]
262 | .split('T')
263 | .join(' ')}
264 |
265 |
266 |
267 | { handleRevoke(host, permission.accept, permission.type) }}
270 | >
271 |
272 |
273 |
274 |
275 |
276 | ))}
277 |
278 |
279 | ))
280 | )}
281 | {Object.values(policies).length === 0 && (
282 |
283 | No permissions have been granted yet
284 |
285 | )}
286 |
287 |
288 |
289 | >
290 | ) : (
291 | <>
292 |
293 | Loading...
294 |
295 | >
296 | )}
297 | >
298 |
299 |
setEditAccountModal(false)}
304 | >
305 |
306 |
setShowAccountDetails(false)}
310 | >
311 |
312 | )
313 | }
314 |
315 | export default VaultPage
316 |
--------------------------------------------------------------------------------
/src/assets/css/style.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | /*
12 | Use a more-intuitive box-sizing model.
13 | */
14 | *, *::before, *::after {
15 | box-sizing: border-box;
16 | }
17 |
18 | html {
19 | line-height: 1.15; /* 1 */
20 | -webkit-text-size-adjust: 100%; /* 2 */
21 | }
22 |
23 | /* Sections
24 | ========================================================================== */
25 |
26 | /**
27 | * Remove the margin in all browsers.
28 | */
29 |
30 | body {
31 | margin: 0;
32 | }
33 |
34 | /**
35 | * Render the `main` element consistently in IE.
36 | */
37 |
38 | main {
39 | display: block;
40 | }
41 |
42 | /**
43 | * Correct the font size and margin on `h1` elements within `section` and
44 | * `article` contexts in Chrome, Firefox, and Safari.
45 | */
46 |
47 | h1 {
48 | font-size: 2em;
49 | margin: 0.67em 0;
50 | }
51 |
52 | /* Grouping content
53 | ========================================================================== */
54 |
55 | /**
56 | * 1. Add the correct box sizing in Firefox.
57 | * 2. Show the overflow in Edge and IE.
58 | */
59 |
60 | hr {
61 | box-sizing: content-box; /* 1 */
62 | height: 0; /* 1 */
63 | overflow: visible; /* 2 */
64 | }
65 |
66 | /**
67 | * 1. Correct the inheritance and scaling of font size in all browsers.
68 | * 2. Correct the odd `em` font sizing in all browsers.
69 | */
70 |
71 | pre {
72 | font-family: monospace, monospace; /* 1 */
73 | font-size: 1em; /* 2 */
74 | }
75 |
76 | /* Text-level semantics
77 | ========================================================================== */
78 |
79 | /**
80 | * Remove the gray background on active links in IE 10.
81 | */
82 |
83 | a {
84 | background-color: transparent;
85 | }
86 |
87 | /**
88 | * 1. Remove the bottom border in Chrome 57-
89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
90 | */
91 |
92 | abbr[title] {
93 | border-bottom: none; /* 1 */
94 | text-decoration: underline; /* 2 */
95 | text-decoration: underline dotted; /* 2 */
96 | }
97 |
98 | /**
99 | * Add the correct font weight in Chrome, Edge, and Safari.
100 | */
101 |
102 | b,
103 | strong {
104 | font-weight: bolder;
105 | }
106 |
107 | /**
108 | * 1. Correct the inheritance and scaling of font size in all browsers.
109 | * 2. Correct the odd `em` font sizing in all browsers.
110 | */
111 |
112 | code,
113 | kbd,
114 | samp {
115 | font-family: monospace, monospace; /* 1 */
116 | font-size: 1em; /* 2 */
117 | }
118 |
119 | /**
120 | * Add the correct font size in all browsers.
121 | */
122 |
123 | small {
124 | font-size: 80%;
125 | }
126 |
127 | /**
128 | * Prevent `sub` and `sup` elements from affecting the line height in
129 | * all browsers.
130 | */
131 |
132 | sub,
133 | sup {
134 | font-size: 75%;
135 | line-height: 0;
136 | position: relative;
137 | vertical-align: baseline;
138 | }
139 |
140 | sub {
141 | bottom: -0.25em;
142 | }
143 |
144 | sup {
145 | top: -0.5em;
146 | }
147 |
148 | /* Embedded content
149 | ========================================================================== */
150 |
151 | /**
152 | * Remove the border on images inside links in IE 10.
153 | */
154 |
155 | img {
156 | border-style: none;
157 | }
158 |
159 | /* Forms
160 | ========================================================================== */
161 |
162 | /**
163 | * 1. Change the font styles in all browsers.
164 | * 2. Remove the margin in Firefox and Safari.
165 | */
166 |
167 | button,
168 | input,
169 | optgroup,
170 | select,
171 | textarea {
172 | font-family: inherit; /* 1 */
173 | font-size: 100%; /* 1 */
174 | line-height: 1.15; /* 1 */
175 | margin: 0; /* 2 */
176 | }
177 |
178 | /**
179 | * Show the overflow in IE.
180 | * 1. Show the overflow in Edge.
181 | */
182 |
183 | button,
184 | input { /* 1 */
185 | overflow: visible;
186 | }
187 |
188 | /**
189 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
190 | * 1. Remove the inheritance of text transform in Firefox.
191 | */
192 |
193 | button,
194 | select { /* 1 */
195 | text-transform: none;
196 | }
197 |
198 | /**
199 | * Correct the inability to style clickable types in iOS and Safari.
200 | */
201 |
202 | button,
203 | [type="button"],
204 | [type="reset"],
205 | [type="submit"] {
206 | -webkit-appearance: button;
207 | }
208 |
209 | /**
210 | * Remove the inner border and padding in Firefox.
211 | */
212 |
213 | button::-moz-focus-inner,
214 | [type="button"]::-moz-focus-inner,
215 | [type="reset"]::-moz-focus-inner,
216 | [type="submit"]::-moz-focus-inner {
217 | border-style: none;
218 | padding: 0;
219 | }
220 |
221 | /**
222 | * Restore the focus styles unset by the previous rule.
223 | */
224 |
225 | button:-moz-focusring,
226 | [type="button"]:-moz-focusring,
227 | [type="reset"]:-moz-focusring,
228 | [type="submit"]:-moz-focusring {
229 | outline: 1px dotted ButtonText;
230 | }
231 |
232 | /**
233 | * Correct the padding in Firefox.
234 | */
235 |
236 | fieldset {
237 | padding: 0.35em 0.75em 0.625em;
238 | }
239 |
240 | /**
241 | * 1. Correct the text wrapping in Edge and IE.
242 | * 2. Correct the color inheritance from `fieldset` elements in IE.
243 | * 3. Remove the padding so developers are not caught out when they zero out
244 | * `fieldset` elements in all browsers.
245 | */
246 |
247 | legend {
248 | box-sizing: border-box; /* 1 */
249 | color: inherit; /* 2 */
250 | display: table; /* 1 */
251 | max-width: 100%; /* 1 */
252 | padding: 0; /* 3 */
253 | white-space: normal; /* 1 */
254 | }
255 |
256 | /**
257 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
258 | */
259 |
260 | progress {
261 | vertical-align: baseline;
262 | }
263 |
264 | /**
265 | * Remove the default vertical scrollbar in IE 10+.
266 | */
267 |
268 | textarea {
269 | overflow: auto;
270 | }
271 |
272 | /**
273 | * 1. Add the correct box sizing in IE 10.
274 | * 2. Remove the padding in IE 10.
275 | */
276 |
277 | [type="checkbox"],
278 | [type="radio"] {
279 | box-sizing: border-box; /* 1 */
280 | padding: 0; /* 2 */
281 | }
282 |
283 | /**
284 | * Correct the cursor style of increment and decrement buttons in Chrome.
285 | */
286 |
287 | [type="number"]::-webkit-inner-spin-button,
288 | [type="number"]::-webkit-outer-spin-button {
289 | height: auto;
290 | }
291 |
292 | /**
293 | * 1. Correct the odd appearance in Chrome and Safari.
294 | * 2. Correct the outline style in Safari.
295 | */
296 |
297 | [type="search"] {
298 | -webkit-appearance: textfield; /* 1 */
299 | outline-offset: -2px; /* 2 */
300 | }
301 |
302 | /**
303 | * Remove the inner padding in Chrome and Safari on macOS.
304 | */
305 |
306 | [type="search"]::-webkit-search-decoration {
307 | -webkit-appearance: none;
308 | }
309 |
310 | /**
311 | * 1. Correct the inability to style clickable types in iOS and Safari.
312 | * 2. Change font properties to `inherit` in Safari.
313 | */
314 |
315 | ::-webkit-file-upload-button {
316 | -webkit-appearance: button; /* 1 */
317 | font: inherit; /* 2 */
318 | }
319 |
320 | /* Interactive
321 | ========================================================================== */
322 |
323 | /*
324 | * Add the correct display in Edge, IE 10+, and Firefox.
325 | */
326 |
327 | details {
328 | display: block;
329 | }
330 |
331 | /*
332 | * Add the correct display in all browsers.
333 | */
334 |
335 | summary {
336 | display: list-item;
337 | }
338 |
339 | /* Misc
340 | ========================================================================== */
341 |
342 | /**
343 | * Add the correct display in IE 10+.
344 | */
345 |
346 | template {
347 | display: none;
348 | }
349 |
350 | /**
351 | * Add the correct display in IE 10.
352 | */
353 |
354 | [hidden] {
355 | display: none;
356 | }
357 |
358 | button {
359 | background-color: #9b59b6; /* Purple color */
360 | color: #fff; /* White text */
361 | padding: 5px 10px; /* Padding */
362 | border: none; /* No border */
363 | border-radius: 5px; /* Rounded corners */
364 | cursor: pointer; /* Cursor style */
365 | text-align: center; /* Center text */
366 | text-decoration: none; /* Remove underline */
367 | display: inline-block; /* Make it inline block */
368 | transition: background-color 0.3s ease; /* Smooth transition */
369 | }
370 |
371 | button:hover {
372 | background-color: #c39bd3; /* Lighter shade of purple on hover */
373 | }
374 |
375 |
376 | a {
377 | color: #7d7d7d;
378 | text-decoration: none;
379 | transition: color 0.3s ease;
380 | }
381 |
382 | a:hover {
383 | color: #cdcdcd;
384 | }
385 |
386 | @import '_fonts.scss';
387 | @import '../../../node_modules/react-toastify/scss/main.scss';
388 |
389 | /* Set default font family and size */
390 | body {
391 | background-color: #000;
392 | color: #fff;
393 | font-family: Arial, sans-serif;
394 | font-size: 16px;
395 | margin: 0 auto;
396 | position: relative;
397 | height: 600px;
398 | width: 357px;
399 | }
400 |
401 | .header {
402 | background-color: #9b59b6;
403 | color: #fff;
404 | padding: 10px;
405 | display: flex;
406 | justify-content: space-between;
407 | align-items: center;
408 | h1 {
409 | margin: 0;
410 | }
411 |
412 | a {
413 | color: #fff;
414 |
415 | &:hover {
416 | color: rgba(#fff, .7);
417 | }
418 | }
419 | }
420 |
421 | .header-nav {
422 | }
423 |
424 | .account {
425 | position: relative;
426 | }
427 |
428 | .account-dropdown {
429 | background: #5d5d5d;
430 | padding: 15px;
431 | position: absolute;
432 | right: 0;
433 | z-index: 1000;
434 | }
435 |
436 | .account-dropdown__item {
437 | &:hover {
438 | background: rgba(#333, .7);
439 | }
440 | &.current {
441 | background: #333;
442 | }
443 | }
444 |
445 | .account-items {
446 | border: 1px solid rgba(#fff, .6);
447 | overflow-y: scroll;
448 | height: 150px;
449 | }
450 |
451 | .account-dropdown__head {
452 | align-items: center;
453 | display: flex;
454 | justify-content: space-between;
455 | margin-bottom: 10px;
456 | }
457 |
458 | .account-dropdown-nav {
459 | list-style: none;
460 | padding-left: 0;
461 | margin-bottom: 0;
462 |
463 | > li {
464 | margin-bottom: 5px;
465 | &:last-child {
466 | margin-bottom: 0;
467 | }
468 | }
469 | }
470 |
471 | .account-profile {
472 | display: flex;
473 | flex-direction: row-reverse;
474 | align-items: center;
475 | }
476 |
477 | .account-profile__body {
478 | flex: 1;
479 | text-align: right;
480 | }
481 |
482 | .account-dropdown__item {
483 | padding: 5px 10px;
484 | }
485 |
486 | .account-profile__img {
487 | border-radius: 50%;
488 | display: block;
489 | height: 35px;
490 | margin: 0 2px 0 5px;
491 | width: 35px;
492 | }
493 |
494 | .profile {
495 | padding: 100px 10px 15px;
496 | position: relative;
497 | }
498 |
499 | .profile__banner {
500 | background: url('../../assets/img/store-banner.jpeg');
501 | background-position: center;
502 | background-size: contain;
503 | height: 100px;
504 | position: absolute;
505 | top: 0;
506 | left: 0;
507 | width: 100%;
508 | }
509 |
510 | .profile__img {
511 | border-radius: 50%,;
512 | border: 3px solid #fff;
513 | margin-bottom: 10px;
514 | width: 100px;
515 | height: 100px;
516 | }
517 |
518 | .profile__body {
519 | line-height: 1.25rem;
520 | margin-top: -53px;
521 | position: relative;
522 | }
523 |
524 | .profile__nav {
525 | list-style: none;
526 | margin: 0;
527 | padding: 0;
528 | position: absolute;
529 | right: 0;
530 | top: 62px;
531 | text-align: center;
532 |
533 | > li {
534 | display: inline-block;
535 | margin-left: 10px;
536 | width: 16px;
537 |
538 | > a {
539 | color: #fff;
540 | display: block;
541 | }
542 | }
543 | }
544 |
545 | .Popup {
546 | }
547 |
548 | .Popup .container {
549 | margin: 0 15px;
550 | }
551 |
552 | .Options .container {
553 | margin: 0 auto;
554 | width: 600px;
555 | }
556 |
557 |
558 | .card {
559 | background: #111;
560 | border-bottom: 1px solid #222;
561 | padding: 15px;
562 | }
563 |
564 | .card:first-child {
565 | border-top: 1px solid #222;
566 | }
567 |
568 | .card-head {
569 | display: flex;
570 | justify-content: space-between;
571 | align-items: center;
572 | margin-bottom: 5px;
573 | }
574 |
575 | .break-string {
576 | word-wrap: break-word;
577 | white-space: pre-wrap;
578 | }
579 |
580 | textarea,
581 | input[type="text"],
582 | input[type="password"] {
583 | display: block;
584 | width: 100%;
585 | }
586 |
587 | textarea {
588 | display: block;
589 | width: 100%;
590 | }
591 |
592 | input[type="file"] {
593 | display: block;
594 | width: 100%;
595 | }
596 |
597 | label {
598 | display: block;
599 | }
600 |
601 | .btn {
602 | background-color: #9b59b6; /* Purple color */
603 | color: #fff; /* White text */
604 | display: block;
605 | padding: 5px 10px; /* Padding */
606 | border: none; /* No border */
607 | border-radius: 5px; /* Rounded corners */
608 | text-align: center; /* Center text */
609 | text-decoration: none; /* Remove underline */
610 | transition: background-color 0.3s ease; /* Smooth transition */
611 | width: 100%;
612 | }
613 |
614 | .btn:hover {
615 | background-color: #c39bd3; /* Lighter shade of purple on hover */
616 | }
617 |
618 | .modal-overlay {
619 | position: fixed;
620 | top: 0;
621 | left: 0;
622 | width: 100%;
623 | height: 100%;
624 | background-color: rgba(0, 0, 0, 0.5);
625 | display: flex;
626 | justify-content: center;
627 | align-items: center;
628 | z-index: 2;
629 | }
630 |
631 | .modal {
632 | background-color: #222;
633 | color: #fff;
634 | padding: 15px;
635 | border-radius: 8px;
636 | margin: 0 15px;
637 | max-width: 340px;
638 | width: 100%;
639 | }
640 |
641 | .modal-content {
642 | position: relative;
643 | }
644 |
645 | .close {
646 | position: absolute;
647 | top: 0;
648 | right: 0;
649 | cursor: pointer;
650 | }
651 |
652 | /* Style the dropdown button */
653 | .dropdown {
654 | display: flex;
655 | position: relative;
656 | display: inline-block;
657 | }
658 |
659 | .dropdown-btn {
660 | display: inline-block;
661 | text-align: right;
662 | width: 25px;
663 | }
664 |
665 | /* Style the dropdown content (hidden by default) */
666 | .dropdown-content {
667 | display: none;
668 | position: absolute;
669 | right: 0;
670 | background-color: #f9f9f9;
671 | min-width: 200px;
672 | box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
673 | z-index: 1;
674 | }
675 |
676 | /* Style the links inside the dropdown */
677 | .dropdown-content a {
678 | color: black;
679 | padding: 10px 15px;
680 | text-decoration: none;
681 | display: block;
682 | }
683 |
684 | /* Change color of dropdown links on hover */
685 | .dropdown-content a:hover {
686 | background-color: #ddd;
687 | }
688 |
689 | /* Show the dropdown menu on hover */
690 | .dropdown:hover .dropdown-content {
691 | display: block;
692 | }
693 |
694 | .tabs {
695 | padding-bottom: 65px;
696 | }
697 |
698 | .foot {
699 | background-color: #5d5d5d;
700 | position: fixed;
701 | bottom: 0;
702 | left: 0;
703 | width: 100%;
704 | }
705 |
706 | .foot-nav {
707 | display: flex;
708 | font-size: 12px;
709 | justify-content: space-around;
710 | align-items: center;
711 | color: #fff;
712 | margin: 0;
713 | padding: 15px 10px;
714 | text-align: center;
715 |
716 | i {
717 | display: inline-block;
718 | font-size: 16px;
719 | margin-bottom: 5px;
720 | }
721 | }
722 |
723 | .foot-nav ul {
724 | list-style: none;
725 | margin: 0;
726 | padding: 0;
727 | }
728 |
729 | .foot-nav li {
730 | display: block;
731 | width: 100%;
732 | }
733 |
734 | .foot-nav a {
735 | color: #fff;
736 | display: block;
737 | text-decoration: none;
738 | }
739 |
740 | .tabs-nav {
741 | padding: 10px 10px 0;
742 | }
743 |
744 | .tabs-content {
745 | padding: 10px;
746 | }
747 |
748 | .permission-item {
749 | display: flex;
750 | justify-content: space-between;
751 | align-items: flex-start;
752 | margin-bottom: 16px;
753 | }
754 |
755 | .permission-item__col:first-child {
756 | flex: 0 0 70%;
757 | }
758 |
759 | .permission-item__col:last-child {
760 | flex: 0 0 30%;
761 | display: flex;
762 | justify-content: flex-end;
763 | }
764 |
--------------------------------------------------------------------------------
/src/assets/css/style.build.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */*,*::before,*::after{box-sizing:border-box}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}button{background-color:#9b59b6;color:#fff;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:background-color .3s ease}button:hover{background-color:#c39bd3}a{color:#7d7d7d;text-decoration:none;transition:color .3s ease}a:hover{color:#cdcdcd}@font-face{font-family:"icomoon";src:url("../fonts/icomoon.eot?rw1199");src:url("../fonts/icomoon.eot?rw1199#iefix") format("embedded-opentype"),url("../fonts/icomoon.ttf?rw1199") format("truetype"),url("../fonts/icomoon.woff?rw1199") format("woff"),url("../fonts/icomoon.svg?rw1199#icomoon") format("svg");font-weight:normal;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:"icomoon" !important;speak:never;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-dots-three-vertical:before{content:""}.icon-pencil:before{content:""}.icon-file-zip:before{content:""}.icon-copy:before{content:""}.icon-folder-plus:before{content:""}.icon-folder-download:before{content:""}.icon-folder-upload:before{content:""}.icon-qrcode:before{content:""}.icon-hour-glass:before{content:""}.icon-spinner3:before{content:""}.icon-key:before{content:""}.icon-lock:before{content:""}.icon-unlocked:before{content:""}.icon-cog:before{content:""}.icon-lab:before{content:""}.icon-bin:before{content:""}.icon-bin2:before{content:""}.icon-switch:before{content:""}.icon-list:before{content:""}.icon-tree:before{content:""}.icon-menu:before{content:""}.icon-download:before{content:""}.icon-upload:before{content:""}.icon-download3:before{content:""}.icon-sphere:before{content:""}.icon-earth:before{content:""}.icon-eye:before{content:""}.icon-eye-blocked:before{content:""}.icon-warning:before{content:""}.icon-notification:before{content:""}.icon-plus:before{content:""}.icon-info:before{content:""}.icon-cancel-circle:before{content:""}.icon-cross:before{content:""}.icon-enter:before{content:""}.icon-exit:before{content:""}.icon-loop2:before{content:""}.icon-arrow-down:before{content:""}.icon-tab:before{content:""}.icon-move-down:before{content:""}.icon-svg:before{content:""}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, 0.7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-offset: 16px;--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-toast-bd-radius: 6px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error);--toastify-color-progress-bgo: 0.2}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0, 0, var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:var(--toastify-toast-top);left:var(--toastify-toast-left)}.Toastify__toast-container--top-center{top:var(--toastify-toast-top);left:50%;transform:translateX(-50%)}.Toastify__toast-container--top-right{top:var(--toastify-toast-top);right:var(--toastify-toast-right)}.Toastify__toast-container--bottom-left{bottom:var(--toastify-toast-bottom);left:var(--toastify-toast-left)}.Toastify__toast-container--bottom-center{bottom:var(--toastify-toast-bottom);left:50%;transform:translateX(-50%)}.Toastify__toast-container--bottom-right{bottom:var(--toastify-toast-bottom);right:var(--toastify-toast-right)}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:env(safe-area-inset-left);margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:env(safe-area-inset-top);transform:translateX(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:env(safe-area-inset-bottom);transform:translateX(0)}.Toastify__toast-container--rtl{right:env(safe-area-inset-right);left:initial}}.Toastify__toast{--y: 0;position:relative;touch-action:none;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:var(--toastify-toast-bd-radius);box-shadow:0px 4px 12px rgba(0,0,0,.1);display:flex;justify-content:space-between;max-height:var(--toastify-toast-max-height);font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0;overflow:hidden}.Toastify__toast--stacked{position:absolute;width:100%;transform:translate3d(0, var(--y), 0) scale(var(--s));transition:transform .3s}.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,.Toastify__toast--stacked[data-collapsed] .Toastify__close-button{transition:opacity .1s}.Toastify__toast--stacked[data-collapsed=false]{overflow:visible}.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0}.Toastify__toast--stacked:after{content:"";position:absolute;left:0;right:0;height:calc(var(--g)*1px);bottom:100%}.Toastify__toast--stacked[data-pos=top]{top:0}.Toastify__toast--stacked[data-pos=bot]{bottom:0}.Toastify__toast--stacked[data-pos=bot].Toastify__toast--stacked:before{transform-origin:top}.Toastify__toast--stacked[data-pos=top].Toastify__toast--stacked:before{transform-origin:bottom}.Toastify__toast--stacked:before{content:"";position:absolute;left:0;right:0;bottom:0;height:100%;transform:scaleY(3);z-index:-1}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;flex:1 1 auto;padding:6px;display:flex;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;flex:1}.Toastify__toast-icon{margin-inline-end:10px;width:20px;flex-shrink:0;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.5s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:rgba(0,0,0,0);outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;align-self:flex-start;z-index:1}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}100%{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:100%;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right;border-bottom-left-radius:initial;border-bottom-right-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp{position:absolute;bottom:0;left:0;width:100%;height:5px;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp[data-hidden=true]{opacity:0}.Toastify__progress-bar--bg{opacity:var(--toastify-color-progress-bgo);width:100%;height:100%}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(3000px, 0, 0)}60%{opacity:1;transform:translate3d(-25px, 0, 0)}75%{transform:translate3d(10px, 0, 0)}90%{transform:translate3d(-5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px, var(--y), 0)}to{opacity:0;transform:translate3d(2000px, var(--y), 0)}}@keyframes Toastify__bounceInLeft{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(-3000px, 0, 0)}60%{opacity:1;transform:translate3d(25px, 0, 0)}75%{transform:translate3d(-10px, 0, 0)}90%{transform:translate3d(5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px, var(--y), 0)}to{opacity:0;transform:translate3d(-2000px, var(--y), 0)}}@keyframes Toastify__bounceInUp{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(0, 3000px, 0)}60%{opacity:1;transform:translate3d(0, -20px, 0)}75%{transform:translate3d(0, 10px, 0)}90%{transform:translate3d(0, -5px, 0)}to{transform:translate3d(0, 0, 0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, -2000px, 0)}}@keyframes Toastify__bounceInDown{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(0, -3000px, 0)}60%{opacity:1;transform:translate3d(0, 25px, 0)}75%{transform:translate3d(0, -10px, 0)}90%{transform:translate3d(0, 5px, 0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, 2000px, 0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{from{opacity:0;transform:scale3d(0.3, 0.3, 0.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{from{opacity:1}50%{opacity:0;transform:translate3d(0, var(--y), 0) scale3d(0.3, 0.3, 0.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{from{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{from{transform:translate3d(0, var(--y), 0) perspective(400px)}30%{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, -20deg);opacity:1}to{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, 90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{from{transform:translate3d(110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInLeft{from{transform:translate3d(-110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInUp{from{transform:translate3d(0, 110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInDown{from{transform:translate3d(0, -110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideOutRight{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(110%, var(--y), 0)}}@keyframes Toastify__slideOutLeft{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(-110%, var(--y), 0)}}@keyframes Toastify__slideOutDown{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, 500px, 0)}}@keyframes Toastify__slideOutUp{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, -500px, 0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown;animation-timing-function:ease-in;animation-duration:.3s}@keyframes Toastify__spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}body{background-color:#000;color:#fff;font-family:Arial,sans-serif;font-size:16px}.header{background-color:#333;color:#fff;padding:10px 15px;display:flex;justify-content:space-between;align-items:center}.header>h1{color:#9b59b6;margin:0}.Popup .container{margin:0 15px}.Options .container{margin:0 auto;width:600px}.card{background:#111;border-bottom:1px solid #222;padding:15px}.card:first-child{border-top:1px solid #222}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px}.break-string{word-wrap:break-word;white-space:pre-wrap}input[type=text],input[type=password]{display:block;width:100%}textarea{display:block;width:100%}input[type=file]{display:block;width:100%}label{display:block}.btn{display:block;width:100%}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:2}.modal{background-color:#222;color:#fff;padding:15px;border-radius:8px;margin:0 15px;max-width:340px;width:100%}.modal-content{position:relative}.close{position:absolute;top:0;right:0;cursor:pointer}.dropdown{display:flex;position:relative;display:inline-block}.dropdown-btn{display:inline-block;text-align:right;width:25px}.dropdown-content{display:none;position:absolute;right:0;background-color:#f9f9f9;min-width:200px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);z-index:1}.dropdown-content a{color:#000;padding:10px 15px;text-decoration:none;display:block}.dropdown-content a:hover{background-color:#ddd}.dropdown:hover .dropdown-content{display:block}
2 |
3 | /*# sourceMappingURL=style.build.css.map */
--------------------------------------------------------------------------------
/dist/assets/css/style.build.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */*,*::before,*::after{box-sizing:border-box}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:rgba(0,0,0,0)}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}button{background-color:#9b59b6;color:#fff;padding:5px 10px;border:none;border-radius:5px;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:background-color .3s ease}button:hover{background-color:#c39bd3}a{color:#7d7d7d;text-decoration:none;transition:color .3s ease}a:hover{color:#cdcdcd}@font-face{font-family:"icomoon";src:url("../onts/icomoon.eot?z8ca6h");src:url("../fonts/icomoon.eot?z8ca6h#iefix") format("embedded-opentype"),url("../fonts/icomoon.ttf?z8ca6h") format("truetype"),url("../fonts/icomoon.woff?z8ca6h") format("woff"),url("../fonts/icomoon.svg?z8ca6h#icomoon") format("svg");font-weight:normal;font-style:normal;font-display:block}[class^=icon-],[class*=" icon-"]{font-family:"icomoon" !important;speak:never;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-wallet:before{content:""}.icon-money:before{content:""}.icon-wallet1:before{content:""}.icon-money1:before{content:""}.icon-cash:before{content:""}.icon-wallet2:before{content:""}.icon-dots-three-vertical:before{content:""}.icon-pencil:before{content:""}.icon-file-zip:before{content:""}.icon-copy:before{content:""}.icon-folder-plus:before{content:""}.icon-folder-download:before{content:""}.icon-folder-upload:before{content:""}.icon-qrcode:before{content:""}.icon-hour-glass:before{content:""}.icon-spinner3:before{content:""}.icon-key:before{content:""}.icon-lock:before{content:""}.icon-unlocked:before{content:""}.icon-cog:before{content:""}.icon-lab:before{content:""}.icon-bin:before{content:""}.icon-bin2:before{content:""}.icon-switch:before{content:""}.icon-list:before{content:""}.icon-tree:before{content:""}.icon-menu:before{content:""}.icon-download:before{content:""}.icon-upload:before{content:""}.icon-download3:before{content:""}.icon-sphere:before{content:""}.icon-earth:before{content:""}.icon-eye:before{content:""}.icon-eye-blocked:before{content:""}.icon-warning:before{content:""}.icon-notification:before{content:""}.icon-plus:before{content:""}.icon-info:before{content:""}.icon-cancel-circle:before{content:""}.icon-cross:before{content:""}.icon-enter:before{content:""}.icon-exit:before{content:""}.icon-loop2:before{content:""}.icon-arrow-down:before{content:""}.icon-tab:before{content:""}.icon-move-down:before{content:""}.icon-svg:before{content:""}.icon-home:before{content:""}.icon-home3:before{content:""}.icon-user:before{content:""}.icon-user-plus:before{content:""}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, 0.7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-offset: 16px;--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-toast-bd-radius: 6px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error);--toastify-color-progress-bgo: 0.2}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0, 0, var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:var(--toastify-toast-top);left:var(--toastify-toast-left)}.Toastify__toast-container--top-center{top:var(--toastify-toast-top);left:50%;transform:translateX(-50%)}.Toastify__toast-container--top-right{top:var(--toastify-toast-top);right:var(--toastify-toast-right)}.Toastify__toast-container--bottom-left{bottom:var(--toastify-toast-bottom);left:var(--toastify-toast-left)}.Toastify__toast-container--bottom-center{bottom:var(--toastify-toast-bottom);left:50%;transform:translateX(-50%)}.Toastify__toast-container--bottom-right{bottom:var(--toastify-toast-bottom);right:var(--toastify-toast-right)}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:env(safe-area-inset-left);margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:env(safe-area-inset-top);transform:translateX(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:env(safe-area-inset-bottom);transform:translateX(0)}.Toastify__toast-container--rtl{right:env(safe-area-inset-right);left:initial}}.Toastify__toast{--y: 0;position:relative;touch-action:none;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:var(--toastify-toast-bd-radius);box-shadow:0px 4px 12px rgba(0,0,0,.1);display:flex;justify-content:space-between;max-height:var(--toastify-toast-max-height);font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0;overflow:hidden}.Toastify__toast--stacked{position:absolute;width:100%;transform:translate3d(0, var(--y), 0) scale(var(--s));transition:transform .3s}.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,.Toastify__toast--stacked[data-collapsed] .Toastify__close-button{transition:opacity .1s}.Toastify__toast--stacked[data-collapsed=false]{overflow:visible}.Toastify__toast--stacked[data-collapsed=true]:not(:last-child)>*{opacity:0}.Toastify__toast--stacked:after{content:"";position:absolute;left:0;right:0;height:calc(var(--g)*1px);bottom:100%}.Toastify__toast--stacked[data-pos=top]{top:0}.Toastify__toast--stacked[data-pos=bot]{bottom:0}.Toastify__toast--stacked[data-pos=bot].Toastify__toast--stacked:before{transform-origin:top}.Toastify__toast--stacked[data-pos=top].Toastify__toast--stacked:before{transform-origin:bottom}.Toastify__toast--stacked:before{content:"";position:absolute;left:0;right:0;bottom:0;height:100%;transform:scaleY(3);z-index:-1}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;flex:1 1 auto;padding:6px;display:flex;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;flex:1}.Toastify__toast-icon{margin-inline-end:10px;width:20px;flex-shrink:0;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.5s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:rgba(0,0,0,0);outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;align-self:flex-start;z-index:1}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}100%{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:100%;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right;border-bottom-left-radius:initial;border-bottom-right-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp{position:absolute;bottom:0;left:0;width:100%;height:5px;border-bottom-left-radius:var(--toastify-toast-bd-radius)}.Toastify__progress-bar--wrp[data-hidden=true]{opacity:0}.Toastify__progress-bar--bg{opacity:var(--toastify-color-progress-bgo);width:100%;height:100%}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(3000px, 0, 0)}60%{opacity:1;transform:translate3d(-25px, 0, 0)}75%{transform:translate3d(10px, 0, 0)}90%{transform:translate3d(-5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px, var(--y), 0)}to{opacity:0;transform:translate3d(2000px, var(--y), 0)}}@keyframes Toastify__bounceInLeft{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(-3000px, 0, 0)}60%{opacity:1;transform:translate3d(25px, 0, 0)}75%{transform:translate3d(-10px, 0, 0)}90%{transform:translate3d(5px, 0, 0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px, var(--y), 0)}to{opacity:0;transform:translate3d(-2000px, var(--y), 0)}}@keyframes Toastify__bounceInUp{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}from{opacity:0;transform:translate3d(0, 3000px, 0)}60%{opacity:1;transform:translate3d(0, -20px, 0)}75%{transform:translate3d(0, 10px, 0)}90%{transform:translate3d(0, -5px, 0)}to{transform:translate3d(0, 0, 0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, -2000px, 0)}}@keyframes Toastify__bounceInDown{from,60%,75%,90%,to{animation-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1)}0%{opacity:0;transform:translate3d(0, -3000px, 0)}60%{opacity:1;transform:translate3d(0, 25px, 0)}75%{transform:translate3d(0, -10px, 0)}90%{transform:translate3d(0, 5px, 0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0, calc(var(--y) - 10px), 0)}40%,45%{opacity:1;transform:translate3d(0, calc(var(--y) + 20px), 0)}to{opacity:0;transform:translate3d(0, 2000px, 0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{from{opacity:0;transform:scale3d(0.3, 0.3, 0.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{from{opacity:1}50%{opacity:0;transform:translate3d(0, var(--y), 0) scale3d(0.3, 0.3, 0.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{from{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{from{transform:translate3d(0, var(--y), 0) perspective(400px)}30%{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, -20deg);opacity:1}to{transform:translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, 90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{from{transform:translate3d(110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInLeft{from{transform:translate3d(-110%, 0, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInUp{from{transform:translate3d(0, 110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideInDown{from{transform:translate3d(0, -110%, 0);visibility:visible}to{transform:translate3d(0, var(--y), 0)}}@keyframes Toastify__slideOutRight{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(110%, var(--y), 0)}}@keyframes Toastify__slideOutLeft{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(-110%, var(--y), 0)}}@keyframes Toastify__slideOutDown{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, 500px, 0)}}@keyframes Toastify__slideOutUp{from{transform:translate3d(0, var(--y), 0)}to{visibility:hidden;transform:translate3d(0, -500px, 0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp;animation-timing-function:ease-in;animation-duration:.3s}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown;animation-timing-function:ease-in;animation-duration:.3s}@keyframes Toastify__spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}body{background-color:#000;color:#fff;font-family:Arial,sans-serif;font-size:16px;margin:0 auto;position:relative;height:600px;width:357px}.header{background-color:#9b59b6;color:#fff;padding:10px;display:flex;justify-content:space-between;align-items:center}.header h1{margin:0}.header a{color:#fff}.header a:hover{color:rgba(255,255,255,.7)}.account{position:relative}.account-dropdown{background:#5d5d5d;padding:15px;position:absolute;right:0;z-index:1000}.account-dropdown__item:hover{background:rgba(51,51,51,.7)}.account-dropdown__item.current{background:#333}.account-items{border:1px solid rgba(255,255,255,.6);overflow-y:scroll;height:150px}.account-dropdown__head{align-items:center;display:flex;justify-content:space-between;margin-bottom:10px}.account-dropdown-nav{list-style:none;padding-left:0;margin-bottom:0}.account-dropdown-nav>li{margin-bottom:5px}.account-dropdown-nav>li:last-child{margin-bottom:0}.account-profile{display:flex;flex-direction:row-reverse;align-items:center}.account-profile__body{flex:1;text-align:right}.account-dropdown__item{padding:5px 10px}.account-profile__img{border-radius:50%;display:block;height:35px;margin:0 2px 0 5px;width:35px}.profile{padding:100px 10px 15px;position:relative}.profile__banner{background:url("../../assets/img/store-banner.jpeg");background-position:center;background-size:contain;height:100px;position:absolute;top:0;left:0;width:100%}.profile__img{border-radius:50%;border:3px solid #fff;margin-bottom:10px;width:100px;height:100px}.profile__body{line-height:1.25rem;margin-top:-53px;position:relative}.profile__nav{list-style:none;margin:0;padding:0;position:absolute;right:0;top:62px;text-align:center}.profile__nav>li{display:inline-block;margin-left:10px;width:16px}.profile__nav>li>a{color:#fff;display:block}.Popup .container{margin:0 15px}.Options .container{margin:0 auto;width:600px}.card{background:#111;border-bottom:1px solid #222;padding:15px}.card:first-child{border-top:1px solid #222}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px}.break-string{word-wrap:break-word;white-space:pre-wrap}textarea,input[type=text],input[type=password]{display:block;width:100%}textarea{display:block;width:100%}input[type=file]{display:block;width:100%}label{display:block}.btn{background-color:#9b59b6;color:#fff;display:block;padding:5px 10px;border:none;border-radius:5px;text-align:center;text-decoration:none;transition:background-color .3s ease;width:100%}.btn:hover{background-color:#c39bd3}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:2}.modal{background-color:#222;color:#fff;padding:15px;border-radius:8px;margin:0 15px;max-width:340px;width:100%}.modal-content{position:relative}.close{position:absolute;top:0;right:0;cursor:pointer}.dropdown{display:flex;position:relative;display:inline-block}.dropdown-btn{display:inline-block;text-align:right;width:25px}.dropdown-content{display:none;position:absolute;right:0;background-color:#f9f9f9;min-width:200px;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);z-index:1}.dropdown-content a{color:#000;padding:10px 15px;text-decoration:none;display:block}.dropdown-content a:hover{background-color:#ddd}.dropdown:hover .dropdown-content{display:block}.tabs{padding-bottom:65px}.foot{background-color:#5d5d5d;position:fixed;bottom:0;left:0;width:100%}.foot-nav{display:flex;font-size:12px;justify-content:space-around;align-items:center;color:#fff;margin:0;padding:15px 10px;text-align:center}.foot-nav i{display:inline-block;font-size:16px;margin-bottom:5px}.foot-nav ul{list-style:none;margin:0;padding:0}.foot-nav li{display:block;width:100%}.foot-nav a{color:#fff;display:block;text-decoration:none}.tabs-nav{padding:10px 10px 0}.tabs-content{padding:10px}.permission-item{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}.permission-item__col:first-child{flex:0 0 70%}.permission-item__col:last-child{flex:0 0 30%;display:flex;justify-content:flex-end}
2 |
3 | /*# sourceMappingURL=style.build.css.map */
--------------------------------------------------------------------------------
/dist/assets/fonts/icomoon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Generated by IcoMoon
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------