Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat/web sso redirects #3928

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 25 additions & 5 deletions packages/api/src/routers/auth/auth_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import axios from 'axios'
import cors from 'cors'
import type { Request, Response } from 'express'
import type { CookieOptions, Request, Response } from 'express'
import express from 'express'
import * as jwt from 'jsonwebtoken'
import url from 'url'
Expand Down Expand Up @@ -47,7 +47,9 @@ import {
validateGoogleUser,
} from './google_auth'
import { createWebAuthToken } from './jwt_helpers'
import { createMobileAccountCreationResponse } from './mobile/account_creation'
import { createAccountCreationResponse } from '../../services/account_creation'
import { libraryItemRepository } from '../../repository/library_item'
import { Claims } from '../../resolvers/types'

export interface SignupRequest {
email: string
Expand All @@ -61,8 +63,10 @@ export interface SignupRequest {

const signToken = promisify(jwt.sign)

const cookieParams = {
const cookieParams: CookieOptions = {
httpOnly: true,
sameSite: 'strict',
secure: true,
maxAge: 365 * 24 * 60 * 60 * 1000,
}

Expand Down Expand Up @@ -122,23 +126,39 @@ export function authRouter() {
hourlyLimiter,
cors<express.Request>(corsConfig),
async (req, res) => {
const WELCOME_TO_OMNIVORE_SLUG = 'welcome-to-omnivore'
const { name, bio, username } = req.body

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const token = req.cookies?.pendingUserAuth as string | undefined

const payload = await createMobileAccountCreationResponse(token, {
const payload = await createAccountCreationResponse(token, {
name,
username,
bio,
})

let hasGettingStarted = false
if (payload.json.authToken) {
const { uid } = jwt.decode(payload.json.authToken) as Claims
hasGettingStarted =
uid != null &&
(await libraryItemRepository.count({
where: { user: { id: uid }, slug: WELCOME_TO_OMNIVORE_SLUG },
})) > 0
}

if (payload.json.authToken) {
res.cookie('auth', payload.json.authToken, cookieParams)
res.clearCookie('pendingUserAuth')
}

res.status(payload.statusCode).json({})
const redirect = hasGettingStarted
? `/${username}/${WELCOME_TO_OMNIVORE_SLUG}`
: '/home'
res.status(payload.statusCode).json({
redirect: redirect,
})
}
)

Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/routers/auth/google_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function handleGoogleWebAuth(
if (authToken) {
const ssoToken = createSsoToken(authToken, `${baseURL()}/home`)
const redirectURL = isVercel
? ssoRedirectURL(ssoToken)
? ssoRedirectURL(ssoToken, `${baseURL()}/home`)
: `${baseURL()}/home`

return {
Expand Down
6 changes: 2 additions & 4 deletions packages/api/src/routers/auth/jwt_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

const signToken = promisify(jwt.sign)

type MobileAuthPayload = {
type AuthPayload = {
authToken: string
authCookieString: string
}
Expand All @@ -27,9 +27,7 @@ export async function createWebAuthToken(
}
}

export async function createMobileAuthPayload(
userId: string
): Promise<MobileAuthPayload> {
export async function createAuthPayload(userId: string): Promise<AuthPayload> {
const authToken = await signToken({ uid: userId }, env.server.jwtSecret)
const authCookieString = cookie.serialize('auth', authToken as string, {
httpOnly: true,
Expand Down
71 changes: 0 additions & 71 deletions packages/api/src/routers/auth/mobile/account_creation.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/api/src/routers/auth/mobile/mobile_auth_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
createMobileSignUpResponse,
createMobileEmailSignUpResponse,
} from './sign_up'
import { createMobileAccountCreationResponse } from './account_creation'
import { createAccountCreationResponse } from '../../../services/account_creation'
import { env } from '../../../env'
import { corsConfig } from '../../../utils/corsConfig'
import cors from 'cors'
Expand Down Expand Up @@ -51,7 +51,7 @@ export function mobileAuthRouter() {

router.post('/create-account', async (req, res) => {
const { pendingUserToken, userProfile } = req.body
const payload = await createMobileAccountCreationResponse(
const payload = await createAccountCreationResponse(
pendingUserToken,
userProfile
)
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/routers/auth/mobile/sign_in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
JsonResponsePayload,
} from '../auth_types'
import { decodeGoogleToken } from '../google_auth'
import { createMobileAuthPayload } from '../jwt_helpers'
import { createAuthPayload } from '../jwt_helpers'

export async function createMobileSignInResponse(
isAndroid: boolean,
Expand Down Expand Up @@ -67,7 +67,7 @@ export async function createMobileEmailSignInResponse(
}
}

const mobileAuthPayload = await createMobileAuthPayload(user.id)
const mobileAuthPayload = await createAuthPayload(user.id)

return {
statusCode: 200,
Expand Down Expand Up @@ -109,7 +109,7 @@ async function createAuthResponsePayload(
}
}

const mobileAuthPayload = await createMobileAuthPayload(userId)
const mobileAuthPayload = await createAuthPayload(userId)

return {
statusCode: 200,
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/utils/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ export const createSsoToken = (
return ssoToken
}

export const ssoRedirectURL = (ssoToken: string): string => {
export const ssoRedirectURL = (
ssoToken: string,
redirect: string | undefined
): string => {
const u = new URL(homePageURL())
u.pathname = 'api/client/auth'
u.searchParams.append('tok', ssoToken)
if (redirect) {
u.searchParams.append('redirect', redirect)
}
return u.toString()
}
17 changes: 17 additions & 0 deletions packages/db/migrations/0175.do.update_new_user_article.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Type: DO
-- Name: update_new_user_article
-- Description: Update the new user article

BEGIN;

UPDATE omnivore.popular_read SET
original_url = 'https://omnivore.app/articles/welcome-to-omnivore',
title = 'Welcome to Omnivore',
published_at = '2024-05-01',
site_name = 'Omnivore',
slug = 'welcome-to-omnivore',
readable_content = '<div data-omnivore-anchor-idx="2" class="page" id="readability-page-1"><div data-omnivore-anchor-idx="3" data-v-c5936a1e=""><!--[--><!--]--><main data-omnivore-anchor-idx="4" data-v-c5936a1e=""><div data-omnivore-anchor-idx="5" data-v-c5936a1e=""><p data-omnivore-anchor-idx="6">Hey, welcome to Omnivore. Omnivore was built to help you get more insights and enjoyment from your daily reading. This quick guide will get you started on that journey.</p><h2 data-omnivore-anchor-idx="7" id="the-reader" tabindex="-1">The Reader </h2><p data-omnivore-anchor-idx="8">There are two main views in Omnivore: the reader and the library. Right now you are in the reader view. The reader strips distractions out of web pages and formats things nicely so you can enjoy what&apos;s important: the content.</p><p data-omnivore-anchor-idx="9">In the reader you can select text and add highlights (try it out!). You can also create notes, and add notes to your highlights. If you are an iOS user you can listen to your articles using text to speech.</p><p data-omnivore-anchor-idx="10"><img data-omnivore-anchor-idx="11" data-omnivore-original-src="https://docs.omnivore.app/assets/welcome-highlights-001.39c5dfba.png" src="https://proxy-prod.omnivore-image-cache.app/0x0,s2RljrYbyaiAXTI8P9_iu32c3XLzi4fIuf7_eqGrPVs4/https://docs.omnivore.app/assets/welcome-highlights-001.39c5dfba.png" alt="Add highlights" style="cursor: zoom-in;"></p><p data-omnivore-anchor-idx="12">When you are done reading an article you can delete it or archive it using the buttons on the lefthand side of the reader (or at the top on mobile). Archived items are saved forever, so you still have the content even if the site goes away.</p><h2 data-omnivore-anchor-idx="13" id="the-library" tabindex="-1">The Library </h2><p data-omnivore-anchor-idx="14">The other important view in Omnivore is the library. This is where all your saved items will appear. You can organize your library by creating labels and saved searches.</p><p data-omnivore-anchor-idx="15"><img data-omnivore-anchor-idx="16" data-omnivore-original-src="https://docs.omnivore.app/assets/welcome-library-001.9e12d823.png" src="https://proxy-prod.omnivore-image-cache.app/0x0,st8usfznrVtl1nii8vI6P6nOI8O4OvSNQKoLSa3nGsGA/https://docs.omnivore.app/assets/welcome-library-001.9e12d823.png" alt="The Library" style="cursor: zoom-in;"></p><p data-omnivore-anchor-idx="17">When you are starting out with Omnivore it is important to learn how to save items.</p><p data-omnivore-anchor-idx="18">The easiest way is the add button at the bottom of the menu on the left side of the library. Clicking this will open the add dialog where you can save links, upload PDFs, subscribe to feeds, or import items from a legacy reader app.</p><p data-omnivore-anchor-idx="19">Besides the Add button, you can also use the mobile apps and browser extensions to save items.</p><p data-omnivore-anchor-idx="20">You can organize things in the library by creating labels and saved searches.</p><h2 data-omnivore-anchor-idx="21" id="subscriptions" tabindex="-1">Subscriptions </h2><p data-omnivore-anchor-idx="22">Many users enjoy reading their subscriptions in Omnivore. In fact, we find the most productive users usually have a few (but not too many) subscriptions. With Omnivore you can create an email address and use it to subscribe to newsletters or you can add rss and atom feeds.</p><h2 data-omnivore-anchor-idx="23" id="native-apps-and-browser-extensions" tabindex="-1">Native Apps and Browser Extensions </h2><p data-omnivore-anchor-idx="24">Omnivore has browser extensions and mobile apps to make saving and reading easier.</p><p data-omnivore-anchor-idx="25">Our native iOS and Android apps sync your reading to the device so it’s available offline. They also make it easier to save items by providing share extensions, and on iOS allow you to listen to your articles with text-to-speech.</p><p data-omnivore-anchor-idx="26">You can install the native iOS app <a data-omnivore-anchor-idx="27" href="https://omnivore.app/install/ios" target="_blank" rel="noreferrer">here</a> and the native Android (prelease) app <a data-omnivore-anchor-idx="28" href="https://omnivore.app/install/android" target="_blank" rel="noreferrer">here</a>.</p><p data-omnivore-anchor-idx="29">Browser extensions can save the URLs and the content of the page you are viewing. They also allow creating notes and adding labels while saving. We have browser extensions available for <a data-omnivore-anchor-idx="30" href="https://omnivore.app/install/firefox" target="_blank" rel="noreferrer">Firefox</a>, <a data-omnivore-anchor-idx="31" href="https://omnivore.app/install/chrome" target="_blank" rel="noreferrer">Chrome</a>, <a data-omnivore-anchor-idx="32" href="https://omnivore.app/install/edge" target="_blank" rel="noreferrer">Edge</a>, and <a data-omnivore-anchor-idx="33" href="https://omnivore.app/install/safari" target="_blank" rel="noreferrer">Safari</a>.</p><h2 data-omnivore-anchor-idx="34" id="get-started" tabindex="-1">Get Started </h2><p data-omnivore-anchor-idx="35">Our goal is to help people gain more insights and enjoyment from their daily reading. If there’s something we could do to improve that, please use the feedback button from the menu.</p><p data-omnivore-anchor-idx="36">Now, archive this article and go enjoy some reading!</p></div></main><!--[--><!--]--><!--[--><!--]--></div></div>',
original_content = ''
where popular_read.key = 'omnivore_get_started';

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Type: UNDO
-- Name: update_new_user_article
-- Description: Update the new user article

BEGIN;

COMMIT;
5 changes: 4 additions & 1 deletion packages/web/components/patterns/ArticleSubtitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element {
return (
<Box>
<StyledText style={textStyle} css={{ wordBreak: 'break-word' }}>
{subtitle} {subtitle && <span style={{ bottom: 1 }}>• </span>}{' '}
{subtitle}{' '}
{subtitle && !shouldHideUrl(props.href) && (
<span style={{ bottom: 1 }}>• </span>
)}{' '}
{!props.hideButton && !shouldHideUrl(props.href) && (
<>
<StyledLink
Expand Down
6 changes: 4 additions & 2 deletions packages/web/components/templates/ConfirmProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ export function ConfirmProfileForm(): JSX.Element {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
}).then((response) => {
}).then(async (response) => {
const body = await response.json()
console.log('response: ', body)
if (response.status === 200) {
window.localStorage.setItem('authVerified', 'true')
window.location.href = '/home'
window.location.href = body['redirect'] ?? '/home'
} else {
setErrorMessage('Error creating account')
}
Expand Down
1 change: 0 additions & 1 deletion packages/web/components/templates/PrimaryLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { useApplyLocalTheme } from '../../lib/hooks/useApplyLocalTheme'
import { updateTheme } from '../../lib/themeUpdater'
import { Priority, useRegisterActions } from 'kbar'
import { ThemeId } from '../tokens/stitches.config'
import { useVerifyAuth } from '../../lib/hooks/useVerifyAuth'

type PrimaryLayoutProps = {
children: ReactNode
Expand Down
11 changes: 8 additions & 3 deletions packages/web/components/templates/SettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DEFAULT_HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
import { logout } from '../../lib/logout'
import { SettingsMenu } from './navMenu/SettingsMenu'
import { SettingsDropdown } from './navMenu/SettingsDropdown'
import { useVerifyAuth } from '../../lib/hooks/useVerifyAuth'
import { useIsUserLoggedOut } from '../../lib/hooks/useIsUserLoggedOut'
import Link from 'next/link'
import { CaretLeft } from 'phosphor-react'

Expand Down Expand Up @@ -53,9 +53,8 @@ const ReturnButton = (): JSX.Element => {
}

export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
useVerifyAuth()

const router = useRouter()
const userLoggedOut = useIsUserLoggedOut()
const [showLogoutConfirmation, setShowLogoutConfirmation] = useState(false)
const [showKeyboardCommandsModal, setShowKeyboardCommandsModal] =
useState(false)
Expand All @@ -67,6 +66,12 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
setShowLogoutConfirmation(true)
}, [setShowLogoutConfirmation])

useEffect(() => {
if (userLoggedOut) {
router.replace('/login')
}
}, [userLoggedOut])

useEffect(() => {
document.addEventListener('logout', showLogout)

Expand Down
18 changes: 18 additions & 0 deletions packages/web/lib/hooks/useIsUserLoggedOut.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import useSWR from 'swr'
import { apiFetcher } from '../networking/networkHelpers'
import { fetchEndpoint } from '../appConfig'

export function useIsUserLoggedOut() {
const response = useSWR(`${fetchEndpoint}/auth/verify`, apiFetcher)
// We are not sure yet
if (!response.data && !response.error) {
return false
}
if (!response.error && 'authStatus' in (response.data as any)) {
const { authStatus } = response.data as any
return authStatus !== 'AUTHENTICATED'
}
return true
}
13 changes: 0 additions & 13 deletions packages/web/lib/hooks/useVerifyAuth.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion packages/web/pages/api/client/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const requestHandler = (req: NextApiRequest, res: NextApiResponse): void => {
}

const tok = req.query.tok
const redirect = req.query.redirect
if (ssoJwtSecret && tok && !Array.isArray(tok)) {
const payload = jwt.verify(tok, ssoJwtSecret) as AuthPayload
res.setHeader(
Expand All @@ -29,7 +30,7 @@ const requestHandler = (req: NextApiRequest, res: NextApiResponse): void => {
})
} else {
res.writeHead(302, {
Location: '/home',
Location: redirect ?? '/home',
})
}

Expand Down
2 changes: 1 addition & 1 deletion packages/web/pages/api/client/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withSentry } from '@sentry/nextjs'
import { serialize } from 'cookie'

const requestHandler = (req: NextApiRequest, res: NextApiResponse): void => {
res.setHeader('Set-Cookie', serialize('auth', '', { maxAge: -1 }))
res.setHeader('Set-Cookie', serialize('auth', '', { expires: new Date(0) }))
res.send('logged out')
}

Expand Down