mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'main' into OMN-700
This commit is contained in:
commit
2f1ac68a74
10 changed files with 323 additions and 10 deletions
|
|
@ -7,4 +7,4 @@ graphql_gen:
|
|||
extension_gen:
|
||||
pushd ../pkg/extension/ && yarn build-prod && popd
|
||||
cp -r ../pkg/extension/dist/* Sources/SafariExtension/Resources/.
|
||||
cat ../pkg/extension/dist/manifest.json | jq '.background.persistent = false' > Sources/SafariExtension/Resources/manifest.json
|
||||
cat ../pkg/extension/dist/manifest.json | jq '.background.persistent = false' | jq '.permissions = ["activeTab", "storage", "nativeMessaging", "https://*/**", "http://*/**"]' > Sources/SafariExtension/Resources/manifest.json
|
||||
|
|
@ -1 +1 @@
|
|||
"use strict";!function(){const e={};function t(t){const n=t.tagName.toLowerCase();if("iframe"===n){const n=e[t.src];if(!n)return;const o=document.createElement("div");o.className="omnivore-instagram-embed",o.innerHTML=n;const r=t.parentNode;if(!r)return;return void r.replaceChild(o,t)}if("img"===n||"image"===n){if(-1===window.getComputedStyle(t).getPropertyValue("filter").indexOf("blur("))return;return void t.remove()}const o=window.getComputedStyle(t),r=o.getPropertyValue("background-image");if(r&&"none"!==r)return;const i=o.getPropertyValue("filter");if(i&&-1!==i.indexOf("blur("))return void t.remove();if(t.src)return;if(t.innerHTML.length>24)return;const a=/url\("(.+?)"\)/gi,c=a.exec(r);a.lastIndex=0;const l=c&&c[1];if(!l)return;const s=document.createElement("img");s.src=l;const p=t.parentNode;p&&p.replaceChild(s,t)}function n(){const e=document.createElement("div");e.style.position="absolute",e.style.left="-2000px",e.style.zIndex="-2000",e.innerHTML=document.body.innerHTML,document.documentElement.appendChild(e),Array.from(e.getElementsByTagName("*")).forEach(t);const n=`<head>${document.head.innerHTML}</head><body>${e.innerHTML}</body>`;return e.remove(),n}function o(){const e=document.querySelectorAll(".webext-omnivore-backdrop");for(let t=0;t<e.length;t++)e[t].style.setProperty("opacity","0","important");setTimeout((()=>{for(let t=0;t<e.length;t++)e[t].remove()}),500)}browserApi.runtime.onMessage.addListener((({action:t,payload:n},o,r)=>{if(t!==ACTIONS.AddIframeContent)return;const{url:i,content:a}=n;e[i]=a,r({})})),window.prepareContent=async function(){return await async function(){const e=".pdf"===window.location.pathname.slice(-4).toLowerCase(),t=-1!==["application/acrobat","application/pdf","application/x-pdf","applications/vnd.pdf","text/pdf","text/x-pdf"].indexOf(document.contentType);if(!e&&!t)return Promise.resolve(null);const n=document.querySelector("embed");return n&&"application/pdf"!==n.type?Promise.resolve(null):ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS&&n.src?Promise.resolve({type:"url",uploadContentObjUrl:n.src}):new Promise(((e,t)=>{const n=new XMLHttpRequest;n.open("GET","",!0),n.responseType="blob",n.onload=function(n){200===this.status?e({type:"pdf",uploadContentObjUrl:URL.createObjectURL(this.response)}):t(n)},n.send()}))}()||(await async function(){const e=document.scrollingElement||document.body,t=e.scrollTop,n=e.scrollHeight;o();const r=function(){const e=document.createElement("div");return e.className="webext-omnivore-backdrop",e.style.cssText="all: initial !important;\n position: fixed !important;\n top: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n left: 0 !important;\n z-index: 99999 !important;\n background: #fff !important;\n opacity: 0.8 !important;\n transition: opacity 0.3s !important;\n -webkit-backdrop-filter: blur(4px) !important;\n backdrop-filter: blur(4px) !important;\n ",e}();for(document.body.appendChild(r);e.scrollTop<=n-500;){const t=e.scrollTop;if(e.scrollTop+=500,await new Promise((e=>{setTimeout(e,10)})),e.scrollTop===t)break}e.scrollTop=t,await new Promise((e=>{setTimeout(e,10)}))}(),o(),{type:"html",content:n()})}}();
|
||||
"use strict";!function(){const e={};function t(t){const n=t.tagName.toLowerCase();if("iframe"===n){const n=e[t.src];if(!n)return;const o=document.createElement("div");o.className="omnivore-instagram-embed",o.innerHTML=n;const r=t.parentNode;if(!r)return;return void r.replaceChild(o,t)}if("img"===n||"image"===n){if(-1===window.getComputedStyle(t).getPropertyValue("filter").indexOf("blur("))return;return void t.remove()}const o=window.getComputedStyle(t),r=o.getPropertyValue("background-image");if(r&&"none"!==r)return;const i=o.getPropertyValue("filter");if(i&&-1!==i.indexOf("blur("))return void t.remove();if(t.src)return;if(t.innerHTML.length>24)return;const a=/url\("(.+?)"\)/gi,c=a.exec(r);a.lastIndex=0;const l=c&&c[1];if(!l)return;const s=document.createElement("img");s.src=l;const p=t.parentNode;p&&p.replaceChild(s,t)}function n(){const e=document.createElement("div");e.style.position="absolute",e.style.left="-2000px",e.style.zIndex="-2000",e.innerHTML=document.body.innerHTML,document.documentElement.appendChild(e),Array.from(e.getElementsByTagName("*")).forEach(t);const n=`<html><head>${document.head.innerHTML}</head><body>${e.innerHTML}</body></html>`;return e.remove(),n}function o(){const e=document.querySelectorAll(".webext-omnivore-backdrop");for(let t=0;t<e.length;t++)e[t].style.setProperty("opacity","0","important");setTimeout((()=>{for(let t=0;t<e.length;t++)e[t].remove()}),500)}browserApi.runtime.onMessage.addListener((({action:t,payload:n},o,r)=>{if(t!==ACTIONS.AddIframeContent)return;const{url:i,content:a}=n;e[i]=a,r({})})),window.prepareContent=async function(){return await async function(){const e=".pdf"===window.location.pathname.slice(-4).toLowerCase(),t=-1!==["application/acrobat","application/pdf","application/x-pdf","applications/vnd.pdf","text/pdf","text/x-pdf"].indexOf(document.contentType);if(!e&&!t)return Promise.resolve(null);const n=document.querySelector("embed");return n&&"application/pdf"!==n.type?Promise.resolve(null):ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS&&n.src?Promise.resolve({type:"url",uploadContentObjUrl:n.src}):new Promise(((e,t)=>{const n=new XMLHttpRequest;n.open("GET","",!0),n.responseType="blob",n.onload=function(n){200===this.status?e({type:"pdf",uploadContentObjUrl:URL.createObjectURL(this.response)}):t(n)},n.send()}))}()||(await async function(){const e=document.scrollingElement||document.body,t=e.scrollTop,n=e.scrollHeight;o();const r=function(){const e=document.createElement("div");return e.className="webext-omnivore-backdrop",e.style.cssText="all: initial !important;\n position: fixed !important;\n top: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n left: 0 !important;\n z-index: 99999 !important;\n background: #fff !important;\n opacity: 0.8 !important;\n transition: opacity 0.3s !important;\n -webkit-backdrop-filter: blur(4px) !important;\n backdrop-filter: blur(4px) !important;\n ",e}();for(document.body.appendChild(r);e.scrollTop<=n-500;){const t=e.scrollTop;if(e.scrollTop+=500,await new Promise((e=>{setTimeout(e,10)})),e.scrollTop===t)break}e.scrollTop=t,await new Promise((e=>{setTimeout(e,10)}))}(),o(),{type:"html",content:n()})}}();
|
||||
|
|
@ -34,7 +34,10 @@ export const apiKeysResolver = authorized<ApiKeysSuccess, ApiKeysError>(
|
|||
const apiKeys = await getRepository(ApiKey).find({
|
||||
select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'],
|
||||
where: { user: { id: uid } },
|
||||
order: { usedAt: 'DESC', createdAt: 'DESC' },
|
||||
order: {
|
||||
usedAt: { direction: 'DESC', nulls: 'last' },
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface FormInputProps {
|
|||
required?: boolean
|
||||
css?: any
|
||||
options?: string[]
|
||||
min?: any
|
||||
}
|
||||
|
||||
export const FormInput = styled('input', {
|
||||
|
|
@ -96,6 +97,7 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element {
|
|||
},
|
||||
}}
|
||||
name={input.name}
|
||||
min={input.min}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { ApiKey } from '../queries/useGetApiKeysQuery'
|
||||
|
||||
export interface GenerateApiKeyInput {
|
||||
name: string
|
||||
scopes?: string[]
|
||||
expiresAt: Date
|
||||
}
|
||||
|
||||
interface GenerateApiKeyResult {
|
||||
generateApiKey: GenerateApiKey
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type GenerateApiKey = {
|
||||
apiKey: ApiKey
|
||||
}
|
||||
|
||||
export async function generateApiKeyMutation(
|
||||
input: GenerateApiKeyInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation GenerateApiKey($input: GenerateApiKeyInput!) {
|
||||
generateApiKey(input: $input) {
|
||||
... on GenerateApiKeySuccess {
|
||||
apiKey {
|
||||
key
|
||||
}
|
||||
}
|
||||
... on GenerateApiKeyError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
input,
|
||||
})) as GenerateApiKeyResult
|
||||
return data.errorCodes ? undefined : data.generateApiKey.apiKey.key
|
||||
} catch (error) {
|
||||
console.log('generateApiKeyMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { ApiKey } from '../queries/useGetApiKeysQuery'
|
||||
|
||||
interface RevokeApiKeyResult {
|
||||
revokeApiKey: RevokeApiKey
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type RevokeApiKey = {
|
||||
apiKey: ApiKey
|
||||
}
|
||||
|
||||
export async function revokeApiKeyMutation(
|
||||
id: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
revokeApiKey(id: "${id}") {
|
||||
... on RevokeApiKeySuccess {
|
||||
apiKey {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on RevokeApiKeyError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation)) as RevokeApiKeyResult
|
||||
return data.errorCodes ? undefined : data.revokeApiKey.apiKey.id
|
||||
} catch (error) {
|
||||
console.log('revokeApiKeyMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
75
packages/web/lib/networking/queries/useGetApiKeysQuery.tsx
Normal file
75
packages/web/lib/networking/queries/useGetApiKeysQuery.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
export interface ApiKey {
|
||||
id: string
|
||||
name: string
|
||||
key?: string
|
||||
scopes: string[]
|
||||
createdAt: Date
|
||||
expiresAt: Date
|
||||
usedAt?: Date
|
||||
}
|
||||
|
||||
interface ApiKeysQueryResponse {
|
||||
isValidating: boolean
|
||||
apiKeys: ApiKey[]
|
||||
revalidate: () => void
|
||||
}
|
||||
|
||||
interface ApiKeysQueryResponseData {
|
||||
apiKeys: ApiKeysData
|
||||
}
|
||||
|
||||
interface ApiKeysData {
|
||||
apiKeys: unknown
|
||||
}
|
||||
|
||||
export function useGetApiKeysQuery(): ApiKeysQueryResponse {
|
||||
const query = gql`
|
||||
query GetApiKeys {
|
||||
apiKeys {
|
||||
... on ApiKeysSuccess {
|
||||
apiKeys {
|
||||
id
|
||||
name
|
||||
key
|
||||
scopes
|
||||
createdAt
|
||||
expiresAt
|
||||
usedAt
|
||||
}
|
||||
}
|
||||
... on ApiKeysError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher)
|
||||
console.log('api keys data', data)
|
||||
|
||||
try {
|
||||
if (data) {
|
||||
const result = data as ApiKeysQueryResponseData
|
||||
const apiKeys = result.apiKeys.apiKeys as ApiKey[]
|
||||
return {
|
||||
isValidating,
|
||||
apiKeys,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
},
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {
|
||||
isValidating: false,
|
||||
apiKeys: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
revalidate: () => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -149,11 +149,11 @@ export const removeItemFromCache = (
|
|||
try {
|
||||
const mappedCache = cache as Map<string, unknown>
|
||||
mappedCache.forEach((value: any, key) => {
|
||||
if (typeof value == 'object' && 'articles' in value) {
|
||||
const articles = value.articles as LibraryItems
|
||||
const idx = articles.edges.findIndex((edge) => edge.node.id == itemId)
|
||||
if (typeof value == 'object' && 'search' in value) {
|
||||
const search = value.search as LibraryItems
|
||||
const idx = search.edges.findIndex((edge) => edge.node.id == itemId)
|
||||
if (idx > -1) {
|
||||
value.articles.edges.splice(idx, 1)
|
||||
value.search.edges.splice(idx, 1)
|
||||
mutate(key, value, false)
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ export const removeItemFromCache = (
|
|||
|
||||
mappedCache.forEach((value: any, key) => {
|
||||
if (Array.isArray(value)) {
|
||||
const idx = value.findIndex((item) => 'articles' in item)
|
||||
const idx = value.findIndex((item) => 'search' in item)
|
||||
if (idx > -1) {
|
||||
mutate(key, value, false)
|
||||
}
|
||||
|
|
|
|||
147
packages/web/pages/settings/api.tsx
Normal file
147
packages/web/pages/settings/api.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Table } from '../../components/elements/Table'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useGetApiKeysQuery } from '../../lib/networking/queries/useGetApiKeysQuery'
|
||||
import { FormInputProps } from '../../components/elements/FormElements'
|
||||
import { generateApiKeyMutation } from '../../lib/networking/mutations/generateApiKeyMutation'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { FormModal } from '../../components/patterns/FormModal'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
import { revokeApiKeyMutation } from '../../lib/networking/mutations/revokeApiKeyMutation'
|
||||
|
||||
interface ApiKey {
|
||||
name: string
|
||||
scopes: string
|
||||
expiresAt: string
|
||||
usedAt: string
|
||||
}
|
||||
|
||||
export default function Api(): JSX.Element {
|
||||
const { apiKeys, revalidate } = useGetApiKeysQuery()
|
||||
const [onDeleteId, setOnDeleteId] = useState<string>('')
|
||||
const [addModelOpen, setAddModelOpen] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
// const [scopes, setScopes] = useState<string[] | undefined>(undefined)
|
||||
const [expiresAt, setExpiresAt] = useState<Date>(new Date())
|
||||
const [formInputs, setFormInputs] = useState<FormInputProps[]>([])
|
||||
const [apiKeyGenerated, setApiKeyGenerated] = useState('')
|
||||
|
||||
const headers = ['Name', 'Scopes', 'Used at', 'Expires on']
|
||||
const rows = useMemo(() => {
|
||||
const rows = new Map<string, ApiKey>()
|
||||
apiKeys.forEach((apiKey) =>
|
||||
rows.set(apiKey.id, {
|
||||
name: apiKey.name,
|
||||
scopes: apiKey.scopes.join(', ') || 'All',
|
||||
usedAt: apiKey.usedAt
|
||||
? new Date(apiKey.usedAt).toISOString()
|
||||
: 'Never used',
|
||||
expiresAt: new Date(apiKey.expiresAt).toDateString(),
|
||||
})
|
||||
)
|
||||
return rows
|
||||
}, [apiKeys])
|
||||
|
||||
// default expiry date is 1 year from now
|
||||
const defaultExpiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365)
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
async function onDelete(id: string): Promise<void> {
|
||||
const result = await revokeApiKeyMutation(id)
|
||||
if (result) {
|
||||
showSuccessToast('API Key deleted', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to delete', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
async function onCreate(): Promise<void> {
|
||||
const result = await generateApiKeyMutation({ name, expiresAt })
|
||||
if (result) {
|
||||
setApiKeyGenerated(result)
|
||||
showSuccessToast('API key generated', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to add', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
return (
|
||||
<PrimaryLayout pageTestId={'api-keys'}>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
|
||||
{addModelOpen && (
|
||||
<FormModal
|
||||
title={'Generate API Key'}
|
||||
onSubmit={onCreate}
|
||||
onOpenChange={setAddModelOpen}
|
||||
inputs={formInputs}
|
||||
acceptButtonLabel={'Generate'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{apiKeyGenerated && (
|
||||
<ConfirmationModal
|
||||
message={`API key generated. Copy the key and use it in your application.
|
||||
You won’t be able to see it again!
|
||||
Key: ${apiKeyGenerated}`}
|
||||
acceptButtonLabel={'Copy'}
|
||||
onAccept={async () => {
|
||||
await navigator.clipboard.writeText(apiKeyGenerated)
|
||||
setApiKeyGenerated('')
|
||||
}}
|
||||
onOpenChange={() => setApiKeyGenerated('')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{onDeleteId && (
|
||||
<ConfirmationModal
|
||||
message={'API key would be revoked. This action cannot be undone.'}
|
||||
onAccept={async () => {
|
||||
await onDelete(onDeleteId)
|
||||
setOnDeleteId('')
|
||||
}}
|
||||
onOpenChange={() => setOnDeleteId('')}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
heading={'API Keys'}
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
onDelete={setOnDeleteId}
|
||||
onAdd={() => {
|
||||
setFormInputs([
|
||||
{
|
||||
label: 'Name',
|
||||
onChange: setName,
|
||||
name: 'name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Expired on',
|
||||
name: 'expiredAt',
|
||||
required: true,
|
||||
onChange: setExpiresAt,
|
||||
type: 'date',
|
||||
min: new Date().toISOString().split('T')[0], // today
|
||||
value: defaultExpiresAt,
|
||||
},
|
||||
])
|
||||
setName('')
|
||||
setExpiresAt(new Date(defaultExpiresAt))
|
||||
setAddModelOpen(true)
|
||||
}}
|
||||
/>
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"manifest_version": 2,
|
||||
"name": "process.env.EXTENSION_NAME",
|
||||
"short_name": "process.env.EXTENSION_NAME",
|
||||
"version": "0.1.22",
|
||||
"version": "0.1.24",
|
||||
"description": "Save articles to your Omnivore library",
|
||||
"author": "Omnivore Media, Inc",
|
||||
"default_locale": "en",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"256": "/images/extension/icon-256.png"
|
||||
},
|
||||
|
||||
"permissions": ["activeTab", "storage", "nativeMessaging", "https://*/**", "http://*/**"],
|
||||
"permissions": ["activeTab", "storage", "https://*/**", "http://*/**"],
|
||||
|
||||
"background": {
|
||||
"page": "/views/background.html",
|
||||
|
|
|
|||
Loading…
Reference in a new issue