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

refactor: run import ticks on Next server #1218

Merged
merged 1 commit into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 })
}
}
}
Loading