From 11abd7d70a03f081d05a16e14548414ae6fcc2d9 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 8 May 2024 19:22:24 +0800 Subject: [PATCH 1/6] Better handling of redirects from the sso and for logged out settings users --- packages/api/src/routers/auth/google_auth.ts | 2 +- packages/api/src/utils/sso.ts | 8 +++++++- packages/web/components/templates/PrimaryLayout.tsx | 1 - .../web/components/templates/SettingsLayout.tsx | 12 +++++++++--- packages/web/lib/hooks/useVerifyAuth.tsx | 13 ------------- packages/web/pages/api/client/auth.ts | 3 ++- 6 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 packages/web/lib/hooks/useVerifyAuth.tsx diff --git a/packages/api/src/routers/auth/google_auth.ts b/packages/api/src/routers/auth/google_auth.ts index 1108b6f235..b723947390 100644 --- a/packages/api/src/routers/auth/google_auth.ts +++ b/packages/api/src/routers/auth/google_auth.ts @@ -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 { diff --git a/packages/api/src/utils/sso.ts b/packages/api/src/utils/sso.ts index 25820d7f54..47cf1704f6 100644 --- a/packages/api/src/utils/sso.ts +++ b/packages/api/src/utils/sso.ts @@ -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() } diff --git a/packages/web/components/templates/PrimaryLayout.tsx b/packages/web/components/templates/PrimaryLayout.tsx index ce661f161e..d05103af8f 100644 --- a/packages/web/components/templates/PrimaryLayout.tsx +++ b/packages/web/components/templates/PrimaryLayout.tsx @@ -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 diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 71f8f44c66..223995764d 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -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' @@ -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) @@ -67,6 +66,13 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element { setShowLogoutConfirmation(true) }, [setShowLogoutConfirmation]) + useEffect(() => { + console.log('AUTH VERIFIED: ', userLoggedOut) + if (userLoggedOut) { + router.replace('/login') + } + }, [userLoggedOut]) + useEffect(() => { document.addEventListener('logout', showLogout) diff --git a/packages/web/lib/hooks/useVerifyAuth.tsx b/packages/web/lib/hooks/useVerifyAuth.tsx deleted file mode 100644 index e641f0768e..0000000000 --- a/packages/web/lib/hooks/useVerifyAuth.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useEffect } from 'react' -import { useRouter } from 'next/router' - -export function useVerifyAuth() { - const router = useRouter() - - useEffect(() => { - if (!window.localStorage.getItem('authVerified')) { - window.location.href = `/login?redirect=${window.location.pathname}` - return - } - }, [router]) -} diff --git a/packages/web/pages/api/client/auth.ts b/packages/web/pages/api/client/auth.ts index ee1d2dc015..2f7dec800c 100644 --- a/packages/web/pages/api/client/auth.ts +++ b/packages/web/pages/api/client/auth.ts @@ -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( @@ -29,7 +30,7 @@ const requestHandler = (req: NextApiRequest, res: NextApiResponse): void => { }) } else { res.writeHead(302, { - Location: '/home', + Location: redirect ?? '/home', }) } From 5e61c6e4fb9b5b0cec908742092d27a309a84f54 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 9 May 2024 10:35:25 +0800 Subject: [PATCH 2/6] Some cleanup for account deletion state --- packages/api/src/routers/auth/auth_router.ts | 10 +-- packages/api/src/routers/auth/jwt_helpers.ts | 6 +- .../routers/auth/mobile/account_creation.ts | 71 ------------------- .../routers/auth/mobile/mobile_auth_router.ts | 4 +- .../api/src/routers/auth/mobile/sign_in.ts | 6 +- .../templates/ConfirmProfileForm.tsx | 2 +- .../components/templates/SettingsLayout.tsx | 1 - packages/web/pages/api/client/logout.ts | 2 +- .../web/pages/settings/delete-my-account.tsx | 11 +++ 9 files changed, 26 insertions(+), 87 deletions(-) delete mode 100644 packages/api/src/routers/auth/mobile/account_creation.ts diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 1bf9828da3..222d8fbe88 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -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' @@ -47,7 +47,7 @@ import { validateGoogleUser, } from './google_auth' import { createWebAuthToken } from './jwt_helpers' -import { createMobileAccountCreationResponse } from './mobile/account_creation' +import { createAccountCreationResponse } from '../../services/account_creation' export interface SignupRequest { email: string @@ -61,8 +61,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, } @@ -127,7 +129,7 @@ export function authRouter() { // 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, diff --git a/packages/api/src/routers/auth/jwt_helpers.ts b/packages/api/src/routers/auth/jwt_helpers.ts index 941982f54a..f8adbb472a 100644 --- a/packages/api/src/routers/auth/jwt_helpers.ts +++ b/packages/api/src/routers/auth/jwt_helpers.ts @@ -11,7 +11,7 @@ import { const signToken = promisify(jwt.sign) -type MobileAuthPayload = { +type AuthPayload = { authToken: string authCookieString: string } @@ -27,9 +27,7 @@ export async function createWebAuthToken( } } -export async function createMobileAuthPayload( - userId: string -): Promise { +export async function createAuthPayload(userId: string): Promise { const authToken = await signToken({ uid: userId }, env.server.jwtSecret) const authCookieString = cookie.serialize('auth', authToken as string, { httpOnly: true, diff --git a/packages/api/src/routers/auth/mobile/account_creation.ts b/packages/api/src/routers/auth/mobile/account_creation.ts deleted file mode 100644 index b19607e1d8..0000000000 --- a/packages/api/src/routers/auth/mobile/account_creation.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { JsonResponsePayload, UserProfile } from '../auth_types' -import { - createMobileAuthPayload, - decodePendingUserToken, -} from './../jwt_helpers' -import { createUser } from '../../../services/create_user' -import { SignupErrorCode } from '../../../generated/graphql' - -export async function createMobileAccountCreationResponse( - pendingUserToken?: string, - userProfile?: UserProfile -): Promise { - try { - if ( - typeof pendingUserToken !== 'string' || - typeof userProfile !== 'object' - ) { - return accountCreationFailedPayload - } - - const decodedToken = decodePendingUserToken(pendingUserToken) - - if (!decodedToken) { - return accountCreationFailedPayload - } - - const { email, provider, sourceUserId } = decodedToken - const [user] = await createUser({ - email, - sourceUserId, - provider, - name: userProfile.name, - username: userProfile.username, - pictureUrl: undefined, - bio: userProfile.bio || undefined, - }) - - const mobileAuthPayload = await createMobileAuthPayload(user.id) - - return { - statusCode: 200, - json: mobileAuthPayload, - } - } catch (error) { - if (isErrorWithCode(error)) { - if (error.errorCode === SignupErrorCode.UserExists) { - return { - statusCode: 400, - json: { errorCodes: ['USER_ALREADY_EXISTS'] }, - } - } - } - return accountCreationFailedPayload - } -} - -const accountCreationFailedPayload = { - statusCode: 400, - json: { errorCodes: ['AUTH_FAILED'] }, -} - -type ErrorWithCode = { - errorCode: string -} - -export function isErrorWithCode(error: unknown): error is ErrorWithCode { - return ( - (error as ErrorWithCode).errorCode !== undefined && - typeof (error as ErrorWithCode).errorCode === 'string' - ) -} diff --git a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts index f27a032fa4..5cc35ec2bc 100644 --- a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts +++ b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts @@ -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' @@ -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 ) diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index ee4c7778ad..fa0d5d0556 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -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, @@ -67,7 +67,7 @@ export async function createMobileEmailSignInResponse( } } - const mobileAuthPayload = await createMobileAuthPayload(user.id) + const mobileAuthPayload = await createAuthPayload(user.id) return { statusCode: 200, @@ -109,7 +109,7 @@ async function createAuthResponsePayload( } } - const mobileAuthPayload = await createMobileAuthPayload(userId) + const mobileAuthPayload = await createAuthPayload(userId) return { statusCode: 200, diff --git a/packages/web/components/templates/ConfirmProfileForm.tsx b/packages/web/components/templates/ConfirmProfileForm.tsx index 4b3c4170c7..9c2b3e0327 100644 --- a/packages/web/components/templates/ConfirmProfileForm.tsx +++ b/packages/web/components/templates/ConfirmProfileForm.tsx @@ -75,7 +75,7 @@ export function ConfirmProfileForm(): JSX.Element { 'Content-Type': 'application/json', }, body: JSON.stringify(data), - }).then((response) => { + }).then(async (response) => { if (response.status === 200) { window.localStorage.setItem('authVerified', 'true') window.location.href = '/home' diff --git a/packages/web/components/templates/SettingsLayout.tsx b/packages/web/components/templates/SettingsLayout.tsx index 223995764d..7b8a666a98 100644 --- a/packages/web/components/templates/SettingsLayout.tsx +++ b/packages/web/components/templates/SettingsLayout.tsx @@ -67,7 +67,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element { }, [setShowLogoutConfirmation]) useEffect(() => { - console.log('AUTH VERIFIED: ', userLoggedOut) if (userLoggedOut) { router.replace('/login') } diff --git a/packages/web/pages/api/client/logout.ts b/packages/web/pages/api/client/logout.ts index 58e2faadb7..16db73bebd 100644 --- a/packages/web/pages/api/client/logout.ts +++ b/packages/web/pages/api/client/logout.ts @@ -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') } diff --git a/packages/web/pages/settings/delete-my-account.tsx b/packages/web/pages/settings/delete-my-account.tsx index de0b35a890..2796c8a667 100644 --- a/packages/web/pages/settings/delete-my-account.tsx +++ b/packages/web/pages/settings/delete-my-account.tsx @@ -14,6 +14,7 @@ import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuer import { Loader } from '../../components/templates/SavingRequest' import { deleteAccountMutation } from '../../lib/networking/mutations/deleteAccountMutation' import Link from 'next/link' +import { webBaseURL } from '../../lib/appConfig' export default function DeleteMyAccount(): JSX.Element { const router = useRouter() @@ -31,6 +32,16 @@ export default function DeleteMyAccount(): JSX.Element { const result = await deleteAccountMutation(viewerId) if (result) { + localStorage.clear() + const logout = await fetch(`${webBaseURL}/api/client/logout`, { + method: 'GET', + credentials: 'include', + mode: 'cors', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }) showSuccessToast('Account deleted') setTimeout(() => { window.location.href = '/login' From 72691da9af76caa953746392b9659b073f77a2bc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 9 May 2024 15:25:15 +0800 Subject: [PATCH 3/6] Handle backend redirects after new users are created --- packages/api/src/routers/auth/auth_router.ts | 20 ++++++++++++++++++- .../templates/ConfirmProfileForm.tsx | 4 +++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 222d8fbe88..e5a082d0e8 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -48,6 +48,8 @@ import { } from './google_auth' import { createWebAuthToken } from './jwt_helpers' import { createAccountCreationResponse } from '../../services/account_creation' +import { libraryItemRepository } from '../../repository/library_item' +import { Claims } from '../../resolvers/types' export interface SignupRequest { email: string @@ -124,6 +126,7 @@ export function authRouter() { hourlyLimiter, cors(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 @@ -135,12 +138,27 @@ export function authRouter() { 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, + }) } ) diff --git a/packages/web/components/templates/ConfirmProfileForm.tsx b/packages/web/components/templates/ConfirmProfileForm.tsx index 9c2b3e0327..48eab5a312 100644 --- a/packages/web/components/templates/ConfirmProfileForm.tsx +++ b/packages/web/components/templates/ConfirmProfileForm.tsx @@ -76,9 +76,11 @@ export function ConfirmProfileForm(): JSX.Element { }, body: JSON.stringify(data), }).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') } From a6227a3c7072e6fb9f9f24f91076768bf269242c Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 9 May 2024 15:25:53 +0800 Subject: [PATCH 4/6] Dont display the dot if there is no URL to display --- packages/web/components/patterns/ArticleSubtitle.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/web/components/patterns/ArticleSubtitle.tsx b/packages/web/components/patterns/ArticleSubtitle.tsx index dbda5b69c0..8c5cb35ffa 100644 --- a/packages/web/components/patterns/ArticleSubtitle.tsx +++ b/packages/web/components/patterns/ArticleSubtitle.tsx @@ -18,7 +18,10 @@ export function ArticleSubtitle(props: ArticleSubtitleProps): JSX.Element { return ( - {subtitle} {subtitle && }{' '} + {subtitle}{' '} + {subtitle && !shouldHideUrl(props.href) && ( + + )}{' '} {!props.hideButton && !shouldHideUrl(props.href) && ( <> Date: Thu, 9 May 2024 15:27:00 +0800 Subject: [PATCH 5/6] Function to check if users are logged out --- packages/web/lib/hooks/useIsUserLoggedOut.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/web/lib/hooks/useIsUserLoggedOut.tsx diff --git a/packages/web/lib/hooks/useIsUserLoggedOut.tsx b/packages/web/lib/hooks/useIsUserLoggedOut.tsx new file mode 100644 index 0000000000..9ec2563487 --- /dev/null +++ b/packages/web/lib/hooks/useIsUserLoggedOut.tsx @@ -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 +} From 7656d5700edb48778ab89f3ac3d0b76475adc634 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 9 May 2024 15:28:17 +0800 Subject: [PATCH 6/6] Update the onboarding article text --- .../0175.do.update_new_user_article.sql | 17 +++++++++++++++++ .../0175.undo.update_new_user_article.sql | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100755 packages/db/migrations/0175.do.update_new_user_article.sql create mode 100755 packages/db/migrations/0175.undo.update_new_user_article.sql diff --git a/packages/db/migrations/0175.do.update_new_user_article.sql b/packages/db/migrations/0175.do.update_new_user_article.sql new file mode 100755 index 0000000000..a75bd55fec --- /dev/null +++ b/packages/db/migrations/0175.do.update_new_user_article.sql @@ -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 = '

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.

The Reader

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's important: the content.

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.

Add highlights

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.

The Library

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.

The Library

When you are starting out with Omnivore it is important to learn how to save items.

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.

Besides the Add button, you can also use the mobile apps and browser extensions to save items.

You can organize things in the library by creating labels and saved searches.

Subscriptions

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.

Native Apps and Browser Extensions

Omnivore has browser extensions and mobile apps to make saving and reading easier.

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.

You can install the native iOS app here and the native Android (prelease) app here.

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 Firefox, Chrome, Edge, and Safari.

Get Started

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.

Now, archive this article and go enjoy some reading!

', + original_content = '' + where popular_read.key = 'omnivore_get_started'; + +COMMIT; diff --git a/packages/db/migrations/0175.undo.update_new_user_article.sql b/packages/db/migrations/0175.undo.update_new_user_article.sql new file mode 100755 index 0000000000..8850a312f0 --- /dev/null +++ b/packages/db/migrations/0175.undo.update_new_user_article.sql @@ -0,0 +1,7 @@ +-- Type: UNDO +-- Name: update_new_user_article +-- Description: Update the new user article + +BEGIN; + +COMMIT;