mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add authorization with notion
This commit is contained in:
parent
07f771c45f
commit
94db3e0de7
7 changed files with 115 additions and 30 deletions
|
|
@ -2,6 +2,7 @@ import axios from 'axios'
|
|||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { env } from '../env'
|
||||
import { getIntegrationClient } from '../services/integrations'
|
||||
import { getClaimsByToken } from '../utils/auth'
|
||||
import { corsConfig } from '../utils/corsConfig'
|
||||
import { logger } from '../utils/logger'
|
||||
|
|
@ -10,10 +11,9 @@ export function integrationRouter() {
|
|||
const router = express.Router()
|
||||
// request token from pocket
|
||||
router.post(
|
||||
'/pocket/auth',
|
||||
'/:name/auth',
|
||||
cors<express.Request>(corsConfig),
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
logger.info('pocket/request-token')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const token = (req.cookies.auth as string) || req.headers.authorization
|
||||
const claims = await getClaimsByToken(token)
|
||||
|
|
@ -21,37 +21,19 @@ export function integrationRouter() {
|
|||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
const consumerKey = env.pocket.consumerKey
|
||||
const redirectUri = `${env.client.url}/settings/integrations`
|
||||
const integrationClient = getIntegrationClient(req.params.name)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const state = req.body.state as string
|
||||
try {
|
||||
// make a POST request to Pocket to get a request token
|
||||
const response = await axios.post<{ code: string }>(
|
||||
'https://getpocket.com/v3/oauth/request',
|
||||
{
|
||||
consumer_key: consumerKey,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
const { code } = response.data
|
||||
const redirectUri = await integrationClient.auth(state)
|
||||
// redirect the user to Pocket to authorize the request token
|
||||
res.redirect(
|
||||
`https://getpocket.com/auth/authorize?request_token=${code}&redirect_uri=${redirectUri}${encodeURIComponent(
|
||||
`?pocketToken=${code}&state=${state}`
|
||||
)}`
|
||||
)
|
||||
res.redirect(redirectUri)
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
} else {
|
||||
logger.error('pocket/request-token exception:', error)
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
res.redirect(
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@ import { DeepPartial, FindOptionsWhere } from 'typeorm'
|
|||
import { Integration } from '../../entity/integration'
|
||||
import { authTrx } from '../../repository'
|
||||
import { IntegrationClient } from './integration'
|
||||
import { NotionClient } from './notion'
|
||||
import { PocketClient } from './pocket'
|
||||
import { ReadwiseClient } from './readwise'
|
||||
|
||||
const integrations: IntegrationClient[] = [
|
||||
new ReadwiseClient(),
|
||||
new PocketClient(),
|
||||
new NotionClient(),
|
||||
]
|
||||
|
||||
export const getIntegrationClient = (name: string): IntegrationClient => {
|
||||
const service = integrations.find((s) => s.name === name)
|
||||
const service = integrations.find(
|
||||
(s) => s.name.toLowerCase() === name.toLowerCase()
|
||||
)
|
||||
if (!service) {
|
||||
throw new Error(`Integration client not found: ${name}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,7 @@ export interface IntegrationClient {
|
|||
|
||||
accessToken(token: string): Promise<string | null>
|
||||
|
||||
auth(state: string): Promise<string>
|
||||
|
||||
export(token: string, items: LibraryItem[]): Promise<boolean>
|
||||
}
|
||||
|
|
|
|||
54
packages/api/src/services/integrations/notion.ts
Normal file
54
packages/api/src/services/integrations/notion.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import axios from 'axios'
|
||||
import { env } from '../../env'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { IntegrationClient } from './integration'
|
||||
|
||||
export class NotionClient implements IntegrationClient {
|
||||
name = 'notion'
|
||||
apiUrl = 'https://api.notion.com/v1'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
}
|
||||
|
||||
accessToken = async (code: string): Promise<string | null> => {
|
||||
const authUrl = `${this.apiUrl}/oauth/token`
|
||||
try {
|
||||
// encode in base 64
|
||||
const encoded = Buffer.from(
|
||||
`${env.notion.clientId}:${env.notion.clientSecret}`
|
||||
).toString('base64')
|
||||
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
authUrl,
|
||||
{
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Basic ${encoded}`,
|
||||
...this.headers,
|
||||
},
|
||||
timeout: 5000, // 5 seconds
|
||||
}
|
||||
)
|
||||
return response.data.access_token
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async auth(state: string): Promise<string> {
|
||||
return Promise.resolve(env.notion.authUrl)
|
||||
}
|
||||
|
||||
export = () => {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
}
|
||||
|
|
@ -39,4 +39,27 @@ export class PocketClient implements IntegrationClient {
|
|||
export = async (): Promise<boolean> => {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
async auth(state: string) {
|
||||
const consumerKey = env.pocket.consumerKey
|
||||
const redirectUri = `${env.client.url}/settings/integrations`
|
||||
|
||||
// make a POST request to Pocket to get a request token
|
||||
const response = await axios.post<{ code: string }>(
|
||||
`${this.apiUrl}/oauth/request`,
|
||||
{
|
||||
consumer_key: consumerKey,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
{
|
||||
headers: this.headers,
|
||||
timeout: 5000, // 5 seconds
|
||||
}
|
||||
)
|
||||
const { code } = response.data
|
||||
|
||||
return `https://getpocket.com/auth/authorize?request_token=${code}&redirect_uri=${redirectUri}${encodeURIComponent(
|
||||
`?pocketToken=${code}&state=${state}`
|
||||
)}`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,17 +57,23 @@ export class ReadwiseClient implements IntegrationClient {
|
|||
export = async (token: string, items: LibraryItem[]): Promise<boolean> => {
|
||||
let result = true
|
||||
|
||||
const highlights = items.flatMap(this.itemToReadwiseHighlight)
|
||||
const highlights = items.flatMap(this._itemToReadwiseHighlight)
|
||||
|
||||
// If there are no highlights, we will skip the sync
|
||||
if (highlights.length > 0) {
|
||||
result = await this.syncWithReadwise(token, highlights)
|
||||
result = await this._syncWithReadwise(token, highlights)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
itemToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => {
|
||||
auth(state: string): Promise<string> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
private _itemToReadwiseHighlight = (
|
||||
item: LibraryItem
|
||||
): ReadwiseHighlight[] => {
|
||||
const category = item.siteName === 'Twitter' ? 'tweets' : 'articles'
|
||||
return item.highlights
|
||||
?.map((highlight) => {
|
||||
|
|
@ -93,7 +99,7 @@ export class ReadwiseClient implements IntegrationClient {
|
|||
.filter((highlight) => highlight !== undefined) as ReadwiseHighlight[]
|
||||
}
|
||||
|
||||
syncWithReadwise = async (
|
||||
private _syncWithReadwise = async (
|
||||
token: string,
|
||||
highlights: ReadwiseHighlight[]
|
||||
): Promise<boolean> => {
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ export interface BackendEnv {
|
|||
mq: redisConfig
|
||||
cache: redisConfig
|
||||
}
|
||||
notion: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
authUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
const nullableEnvVars = [
|
||||
|
|
@ -165,6 +170,9 @@ const nullableEnvVars = [
|
|||
'MQ_REDIS_CERT',
|
||||
'IMPORTER_METRICS_COLLECTOR_URL',
|
||||
'INTERNAL_API_URL',
|
||||
'NOTION_CLIENT_ID',
|
||||
'NOTION_CLIENT_SECRET',
|
||||
'NOTION_AUTH_URL',
|
||||
] // Allow some vars to be null/empty
|
||||
|
||||
/* If not in GAE and Prod/QA/Demo env (f.e. on localhost/dev env), allow following env vars to be null */
|
||||
|
|
@ -311,6 +319,11 @@ export function getEnv(): BackendEnv {
|
|||
cert: parse('REDIS_CERT')?.replace(/\\n/g, '\n'), // replace \n with new line
|
||||
},
|
||||
}
|
||||
const notion = {
|
||||
clientId: parse('NOTION_CLIENT_ID'),
|
||||
clientSecret: parse('NOTION_CLIENT_SECRET'),
|
||||
authUrl: parse('NOTION_AUTH_URL'),
|
||||
}
|
||||
|
||||
return {
|
||||
pg,
|
||||
|
|
@ -333,6 +346,7 @@ export function getEnv(): BackendEnv {
|
|||
pocket,
|
||||
subscription,
|
||||
redis,
|
||||
notion,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue