* fix: Library Header layout shift * Bump Github Actions versions. * Self-Hosting Changes * Fix Minio Environment Variable * Just make pdfs successful, due to lack of PDFHandler * Fix issue where flag was set wrong * Added an NGINX Example file * Add some documentation for self-hosting via Docker Compose * Make some adjustments to Puppeteer due to failing sites. * adjust timings * Add start of Mail Service * Fix Docker Files * More email service stuff * Add Guide to use Zapier for Email-Importing. * Ensure that if no env is provided it uses the old email settings * Add some instructions for self-hosted email * Add SNS Endpoints for Mail Watcher * Add steps and functionality for using SES and SNS for email * Uncomment a few jobs. * Added option for Firefox for parser. Was having issues with Chromium on Docker. * Add missing space. Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com> * Fix some wording on the Guide * update browser extension to handle self-hosted instances * add slight documentation to options page * Fix MV * Do raw handlers for Medium * Fix images in Medium * Update self-hosting/GUIDE.md Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Update Guide with other variables * Add The Verge to JS-less handlers * Update regex and image-proxy * Update self-hosting/nginx/nginx.conf Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Update regex and image-proxy * Update self-hosting/docker-compose/docker-compose.yml Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> * Fix Minio for Export * Merge to main * Update GUIDE with newer NGINX * Update nginx config to include api/save route * Enable Native PDF View for PDFS * Enable Native PDF View for PDFS * feat:lover packages test * feat:working build * feat:alpine build * docs:api dockerfile docs * Write a PDF.js wrapper to replace pspdfkit * Revert changes for replication, set settings to have default mode * build folder got removed due to gitignore on pdf * Add Box shadow to pdf pages * Add Toggle for Progress in PDFS, enabled native viewer toggle * Update node version to LTS * Update node version to LTS * Fix Linting issues * Fix Linting issues * Make env variable nullable * Add touchend listener for mobile * Make changes to PDF for mobile * fix(android): change serverUrl to selfhosted first * feat:2 stage alpine content fetch * feat:separated start script * fix:changed to node 22 * Add back youtube functionality and add guide * trigger build * Fix cache issue on YouTube * Allow empty AWS_S3_ENDPOINT * Allow empty AWS_S3_ENDPOINT * Add GCHR for all images * Add GCHR For self hosting. * Add GCHR For self hosting. * Test prebuilt. * Test prebuilt * Test prebuilt... * Fix web image * Remove Web Image (For now) * Move docker-compose to images * Move docker-compose files to correct locations * Remove the need for ARGS * Update packages, and Typescript versions * Fix * Fix issues with build on Web * Correct push * Fix Linting issues * Fix Trace import * Add missing types * Fix Tasks * Add information into guide about self-build * Fix issues with PDF Viewer --------- Co-authored-by: keumky2 <keumky2@woowahan.com> Co-authored-by: William Theaker <wtheaker@nvidia.com> Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com> Co-authored-by: David Adams <david@dadams2.com> Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com> Co-authored-by: m1xxos <66390094+m1xxos@users.noreply.github.com> Co-authored-by: Adil <mr.adil777@gmail.com>
219 lines
5.7 KiB
TypeScript
219 lines
5.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
import * as jwt from 'jsonwebtoken'
|
|
import jwksClient from 'jwks-rsa'
|
|
import { StatusType } from '../../entity/user'
|
|
import { env, homePageURL } from '../../env'
|
|
import { LoginErrorCode } from '../../generated/graphql'
|
|
import { userRepository } from '../../repository/user'
|
|
import { analytics } from '../../utils/analytics'
|
|
import { logger } from '../../utils/logger'
|
|
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
|
|
import { DecodeTokenResult } from './auth_types'
|
|
import {
|
|
createPendingUserToken,
|
|
createWebAuthToken,
|
|
suggestedUsername,
|
|
} from './jwt_helpers'
|
|
import { DEFAULT_HOME_PATH } from '../../utils/navigation'
|
|
|
|
const appleBaseURL = 'https://appleid.apple.com'
|
|
const audienceName = 'app.omnivore.app'
|
|
const webAudienceName = 'app.omnivore'
|
|
|
|
async function fetchApplePublicKey(kid: string): Promise<string | null> {
|
|
const client = jwksClient({
|
|
cache: true,
|
|
jwksUri: `${appleBaseURL}/auth/keys`,
|
|
})
|
|
|
|
try {
|
|
const key: jwksClient.SigningKey = await new Promise((resolve, reject) => {
|
|
client.getSigningKey(kid, (error, result) => {
|
|
if (error || result === undefined) {
|
|
return reject(error)
|
|
}
|
|
return resolve(result)
|
|
})
|
|
})
|
|
return key.getPublicKey()
|
|
} catch (e) {
|
|
logger.error('fetchApplePublicKey error', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function decodeAppleToken(
|
|
token: string
|
|
): Promise<DecodeTokenResult> {
|
|
const decodedToken = jwt.decode(token, { complete: true })
|
|
const { kid, alg } = (decodedToken as any).header
|
|
|
|
try {
|
|
const publicKey = await fetchApplePublicKey(kid)
|
|
if (!publicKey) {
|
|
return { errorCode: 500 }
|
|
}
|
|
const jwtClaims: any = jwt.verify(token, publicKey, { algorithms: [alg] })
|
|
const issVerified = (jwtClaims.iss ?? '') === appleBaseURL
|
|
const audience = jwtClaims.aud ?? ''
|
|
const audVerified = audience == webAudienceName || audience === audienceName
|
|
if (issVerified && audVerified && jwtClaims.email) {
|
|
return {
|
|
email: jwtClaims.email,
|
|
sourceUserId: jwtClaims.sub,
|
|
name: jwtClaims.name,
|
|
}
|
|
} else {
|
|
return {
|
|
errorCode: 401,
|
|
}
|
|
}
|
|
} catch (e) {
|
|
logger.error('decodeAppleToken error', e)
|
|
return { errorCode: 500 }
|
|
}
|
|
}
|
|
|
|
type AppleWebAuthResponse = {
|
|
redirectURL: string
|
|
authToken?: string
|
|
pendingUserToken?: string
|
|
}
|
|
|
|
type AppleUserData = {
|
|
name?: AppleUserName
|
|
email?: string
|
|
}
|
|
|
|
type AppleUserName = {
|
|
firstName?: string
|
|
lastName?: string
|
|
}
|
|
|
|
export async function handleAppleWebAuth(
|
|
idToken: string,
|
|
appleUserData?: AppleUserData,
|
|
isLocal = false,
|
|
isVercel = false
|
|
): Promise<AppleWebAuthResponse> {
|
|
const baseURL = () => {
|
|
if (isLocal) {
|
|
return 'http://localhost:3000'
|
|
}
|
|
|
|
if (isVercel) {
|
|
return homePageURL()
|
|
}
|
|
|
|
return env.client.url
|
|
}
|
|
const decodedTokenResult = await decodeAppleToken(idToken)
|
|
const authFailedRedirect = `${baseURL()}/login?errorCodes=${
|
|
LoginErrorCode.AuthFailed
|
|
}`
|
|
|
|
if (!decodedTokenResult.email || decodedTokenResult.errorCode) {
|
|
return Promise.resolve({
|
|
redirectURL: authFailedRedirect,
|
|
})
|
|
}
|
|
|
|
try {
|
|
const user = await userRepository.findOneBy({
|
|
sourceUserId: decodedTokenResult.sourceUserId,
|
|
source: 'APPLE',
|
|
status: StatusType.Active,
|
|
})
|
|
const userId = user?.id
|
|
|
|
if (!userId) {
|
|
// create a temp token so the user can create a new profile
|
|
const payload = await createTempAppleUserPayload({
|
|
authFailedRedirect,
|
|
appleUserData,
|
|
baseURL: baseURL(),
|
|
sourceUserId: decodedTokenResult.sourceUserId,
|
|
email: decodedTokenResult.email,
|
|
})
|
|
|
|
return payload
|
|
}
|
|
|
|
const authToken = await createWebAuthToken(userId)
|
|
if (authToken) {
|
|
const ssoToken = createSsoToken(
|
|
authToken,
|
|
`${baseURL()}${DEFAULT_HOME_PATH}`
|
|
)
|
|
const redirectURL = isVercel
|
|
? ssoRedirectURL(ssoToken)
|
|
: `${baseURL()}${DEFAULT_HOME_PATH}`
|
|
|
|
analytics.capture({
|
|
distinctId: user.id,
|
|
event: 'login',
|
|
properties: {
|
|
method: 'apple',
|
|
email: user.email,
|
|
username: user.profile.username,
|
|
env: env.server.apiEnv,
|
|
},
|
|
})
|
|
|
|
return {
|
|
authToken,
|
|
redirectURL,
|
|
}
|
|
} else {
|
|
return { redirectURL: authFailedRedirect }
|
|
}
|
|
} catch (e) {
|
|
logger.info('handleAppleWebAuth error', e)
|
|
return { redirectURL: authFailedRedirect }
|
|
}
|
|
}
|
|
|
|
type CreateTempAppleUserPayloadInputs = {
|
|
appleUserData?: AppleUserData
|
|
authFailedRedirect: string
|
|
baseURL: string
|
|
sourceUserId?: string
|
|
email?: string
|
|
}
|
|
|
|
async function createTempAppleUserPayload(
|
|
inputs: CreateTempAppleUserPayloadInputs
|
|
): Promise<AppleWebAuthResponse> {
|
|
if (!inputs.email || !inputs.sourceUserId) {
|
|
throw new Error('missing email or sourceUserId')
|
|
}
|
|
|
|
const firstName = inputs.appleUserData?.name?.firstName ?? ''
|
|
const lastName = inputs.appleUserData?.name?.lastName ?? ''
|
|
const name = `${firstName} ${lastName}`
|
|
const username = suggestedUsername(name)
|
|
|
|
try {
|
|
const pendingUserToken = await createPendingUserToken({
|
|
email: inputs.email,
|
|
sourceUserId: inputs.sourceUserId,
|
|
provider: 'APPLE',
|
|
name,
|
|
username,
|
|
})
|
|
|
|
if (!pendingUserToken) {
|
|
throw new Error('Failed to create pending user token')
|
|
}
|
|
|
|
return {
|
|
redirectURL: `${inputs.baseURL}/confirm-profile?username=${username}&name=${name}`,
|
|
pendingUserToken,
|
|
}
|
|
} catch {
|
|
return { redirectURL: inputs.authFailedRedirect }
|
|
}
|
|
}
|