Skip to content

Commit

Permalink
refactor: run import ticks on Next server (#1218)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent authored Nov 12, 2024
1 parent c7d7181 commit a8bfdfb
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NextApiHandler } from 'next'
import { getServerSession } from 'next-auth'
import csv from 'csvtojson'
import axios, { AxiosInstance } from 'axios'
import { v5 as uuidv5, NIL } from 'uuid'
import { NextRequest, NextResponse } from 'next/server'

import withAuth from '../withAuth'
import { authOptions } from '../auth/[...nextauth]'
import { updateUser } from '@/js/auth/ManagementClient'
import { graphqlClient } from '@/js/graphql/Client'
import { MUTATION_IMPORT_TICKS } from '@/js/graphql/gql/fragments'
import { withUserAuth } from '@/js/auth/withUserAuth'

export interface Tick {
name: string
Expand Down Expand Up @@ -58,11 +58,12 @@ interface MPTick {
'Rating Code': string
}

async function getMPTicks (uid: string): Promise<MPTick[]> {
async function getMPTicks (profileUrl: string): Promise<MPTick[]> {
const mpClient: AxiosInstance = axios.create({
baseURL: 'https://www.mountainproject.com/user'
baseURL: 'https://www.mountainproject.com/user',
timeout: 60000
})
const res = await mpClient.get(`${uid}/tick-export`)
const res = await mpClient.get(`${profileUrl}/tick-export`)
if (res.status === 200) {
const data = await csv({
// output: "csv",
Expand All @@ -77,20 +78,22 @@ async function getMPTicks (uid: string): Promise<MPTick[]> {
return []
}

const handler: NextApiHandler<any> = async (req, res) => {
if (req.method !== 'POST') res.end()
const session = await getServerSession(req, res, authOptions)
if (session == null) res.end()
const postHandler = async (req: NextRequest): Promise<any> => {
const uuid = req.headers.get('x-openbeta-user-uuid')
const auth0Userid = req.headers.get('x-auth0-userid')
const payload = await req.json()
const profileUrl: string = payload.profileUrl

const uuid = session?.user.metadata?.uuid
const uid: string = JSON.parse(req.body)
if (uuid == null || uid == null) res.status(500)
if (uuid == null || profileUrl == null || auth0Userid == null) {
// A bug in our code - shouldn't get here.
return NextResponse.json({ status: 500 })
}

// fetch data from mountain project here
const tickCollection: Tick[] = []
const ret = await getMPTicks(uid)
const ret = await getMPTicks(profileUrl)

ret.forEach((tick) => {
for (const tick of ret) {
const newTick: Tick = {
name: tick.Route,
notes: tick.Notes,
Expand All @@ -103,15 +106,24 @@ const handler: NextApiHandler<any> = async (req, res) => {
source: 'MP'
}
tickCollection.push(newTick)
})
}

if (tickCollection.length > 0) {
// send ticks to OB backend
await graphqlClient.mutate<any, { input: Tick[] }>({
mutation: MUTATION_IMPORT_TICKS,
variables: {
input: tickCollection
}
})
}

// set the user flag to true, so the popup doesn't show anymore and
// update the metadata
// Note: null check is to make TS happy. We wouldn't get here if session is null.
if (session != null) {
await updateUser(session.id, { ticksImported: true })
}
await updateUser(auth0Userid, { ticksImported: true })

res.json({ ticks: tickCollection })
res.end()
return NextResponse.json({ count: tickCollection.length }, { status: 200 })
}
export default withAuth(handler)

export const POST = withUserAuth(postHandler)
73 changes: 21 additions & 52 deletions src/components/users/ImportFromMtnProj.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ import { useState } from 'react'
import { useRouter } from 'next/router'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import { FolderArrowDownIcon } from '@heroicons/react/24/outline'
import { useMutation } from '@apollo/client'
import { signIn, useSession } from 'next-auth/react'
import { toast } from 'react-toastify'
import clx from 'classnames'

import { graphqlClient } from '../../js/graphql/Client'
import { MUTATION_IMPORT_TICKS } from '../../js/graphql/gql/fragments'
import { INPUT_DEFAULT_CSS } from '../ui/form/TextArea'
import Spinner from '../ui/Spinner'
import { LeanAlert } from '../ui/micro/AlertDialogue'
Expand All @@ -33,42 +30,6 @@ export function ImportFromMtnProj ({ username }: Props): JSX.Element {
const [showInput, setShowInput] = useState(false)
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<string[]>([])
const [addTicks] = useMutation(
MUTATION_IMPORT_TICKS, {
client: graphqlClient,
errorPolicy: 'none'
})

async function fetchMPData (url: string, method: 'GET' | 'POST' | 'PUT' = 'GET', body?: string): Promise<any> {
try {
const headers = {
'Content-Type': 'application/json'
}
const config: RequestInit = {
method,
headers
}

if (body !== null && body !== undefined && body !== '') {
config.body = JSON.stringify(body)
}

const response = await fetch(url, config)

if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.statusText)
}

return await response.json()
} catch (error) {
if (error instanceof Error) {
console.error('Fetch error:', error.message)
throw error
}
throw new Error('An unexpected error occurred')
}
}

// this function is for when the component is rendered as a button and sends the user straight to the input form
function straightToInput (): void {
Expand All @@ -81,25 +42,17 @@ export function ImportFromMtnProj ({ username }: Props): JSX.Element {
}

async function getTicks (): Promise<void> {
// get the ticks and add it to the database
setErrors([])
if (pattern.test(mpUID)) {
setLoading(true)

try {
const response = await fetchMPData('/api/user/ticks', 'POST', JSON.stringify(mpUID))

if (response.ticks[0] !== undefined) {
await addTicks({
variables: {
input: response.ticks
}
})
// Add a delay before rerouting to the new page
const ticksCount: number = response.ticks?.length ?? 0
const { count } = await bulkImportProxy(mpUID)

if (count > 0) {
toast.info(
<>
{ticksCount} ticks have been imported! 🎉 <br />
{count} ticks have been imported! 🎉 <br />
Redirecting in a few seconds...`
</>
)
Expand Down Expand Up @@ -149,6 +102,7 @@ export function ImportFromMtnProj ({ username }: Props): JSX.Element {
onChange={(e) => setMPUID(e.target.value)}
className={clx(INPUT_DEFAULT_CSS, 'w-full')}
placeholder='https://www.mountainproject.com/user/123456789/username'
disabled={loading}
/>
</div>
)}
Expand Down Expand Up @@ -183,7 +137,7 @@ export function ImportFromMtnProj ({ username }: Props): JSX.Element {
setErrors([])
}}
>
<button className='Button mauve'>Cancel</button>
<button className='btn btn-outline' disabled={loading}>Cancel</button>
</AlertDialogPrimitive.Cancel>
</div>
</LeanAlert>
Expand All @@ -193,3 +147,18 @@ export function ImportFromMtnProj ({ username }: Props): JSX.Element {
}

export default ImportFromMtnProj

type BulkImportFn = (profileUrl: string) => Promise<{ count: number }>

const bulkImportProxy: BulkImportFn = async (profileUrl: string) => {
const res = await fetch('/api/user/bulkImportTicks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
profileUrl
})
})
return await res.json()
}
23 changes: 23 additions & 0 deletions src/js/auth/withUserAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getServerSession } from 'next-auth'
import { NextRequest, NextResponse } from 'next/server'
import { authOptions } from 'pages/api/auth/[...nextauth]'

type Next13APIHandler = (req: NextRequest) => Promise<any>

/*
* A high-order function to protect Next 13 (and later) API route
* by checking that the user has a valid session.
*/
export const withUserAuth = (handler: Next13APIHandler): Next13APIHandler => {
return async (req: NextRequest) => {
const session = await getServerSession({ req, ...authOptions })
if (session != null) {
// Passing useful session data downstream
req.headers.set('x-openbeta-user-uuid', session.user.metadata.uuid)
req.headers.set('x-auth0-userid', session.id)
return await handler(req)
} else {
return NextResponse.json({ status: 401 })
}
}
}

0 comments on commit a8bfdfb

Please sign in to comment.