Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into map-areaid-share
Browse files Browse the repository at this point in the history
  • Loading branch information
clintonlunn committed Nov 18, 2024
2 parents a8a995d + 6894837 commit 591154e
Show file tree
Hide file tree
Showing 19 changed files with 283 additions and 336 deletions.
3 changes: 0 additions & 3 deletions .dockerignore

This file was deleted.

30 changes: 0 additions & 30 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,3 @@ jobs:
- name: Build project
run: yarn build
if: ${{ github.event_name != 'pull_request' }}

test-docker-compose:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
- name: Build the dev container
run: docker-compose build --no-cache --progress plain
env:
SIRV_CLIENT_SECRET_RO: ${{ secrets.SIRV_CLIENT_SECRET_RO }}
SIRV_CLIENT_SECRET_RW: ${{ secrets.SIRV_CLIENT_SECRET_RW }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
AUTH0_CLIENT_SECRET: ${$ secrets.AUTH0_CLIENT_SECRET }}
AUTH0_MGMT_CLIENT_SECRET: ${$ secrets.AUTH0_MGMT_CLIENT_SECRET }}

- name: Start docker compose
run: docker-compose up -d
env:
SIRV_CLIENT_SECRET_RO: ${{ secrets.SIRV_CLIENT_SECRET_RO }}

- name: Wait for the site to be online
run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 180 localhost:3000 -- echo "Site is up"
env:
WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3

- name: Stop containers
if: always()
run: docker-compose down
17 changes: 0 additions & 17 deletions Dockerfile

This file was deleted.

21 changes: 0 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,6 @@ yarn dev

The application is now available at http://localhost:3000


### Alternate build method using Docker

If you just want to run the app locally without installing node, npm, etc., you can do so with Docker.

**Requirements:** [Docker](https://docs.docker.com/get-docker/)

```
docker compose up
```

The application is now available at http://localhost:3000. The project will rebuild automatically when you make changes to files in `./src` dir.

Note: If you install new NPM packages, you will need to rebuild the docker image with

```
docker compose up --build
```

The application is now available at `http://localhost:3000`

## Tips

### API key errors
Expand Down
23 changes: 0 additions & 23 deletions docker-compose.yml

This file was deleted.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@google-cloud/storage": "^6.11.0",
"@google-cloud/storage": "^7.14.0",
"@headlessui/react": "^1.7.15",
"@heroicons/react": "2.0.13",
"@hookform/resolvers": "^3.1.1",
Expand All @@ -38,6 +38,7 @@
"daisyui": "^3.9.4",
"date-fns": "^2.28.0",
"embla-carousel-react": "^8.0.0",
"encoding": "^0.1.13",
"file-saver": "^2.0.5",
"fuse.js": "^6.6.2",
"graphql": "^16.2.0",
Expand Down
19 changes: 14 additions & 5 deletions src/app/api/mobile/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async function postHandler (request: NextRequest): Promise<NextResponse> {
throw new Error('Invalid payload')
}
} catch (error) {
return NextResponse.json({ error: 'Unexpected error', status: 400 })
return NextResponse.json({ error: 'Unexpected error' }, { status: 400 })
}

let response: Auth0.JSONApiResponse<Auth0.TokenSet> | undefined
Expand All @@ -30,12 +30,21 @@ async function postHandler (request: NextRequest): Promise<NextResponse> {
audience: 'https://api.openbeta.io',
realm: 'Username-Password-Authentication'
})

return NextResponse.json({ data: response.data })
return NextResponse.json({ ...response.data }, { status: response.status })
} catch (error) {
console.error('#### Auth0 error ####', error)
return NextResponse.json({ error: 'Unexpected auth error', status: 403 })
return errorHandler(error)
}
}

export const POST = withMobileAuth(postHandler)

/**
* Handle Auth0 errors
*/
export const errorHandler = (error: any): NextResponse => {
console.error('#### Auth0 error ####', error)
if (error instanceof Auth0.AuthApiError) {
return NextResponse.json({ error: error?.error_description ?? '' }, { status: error?.statusCode ?? 401 })
}
return NextResponse.json({ error: 'Unexpected auth error' }, { status: 401 })
}
6 changes: 3 additions & 3 deletions src/app/api/mobile/refreshToken/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import * as Auth0 from 'auth0'
import { auth0Client, isNullOrEmpty } from '@/js/auth/mobile'
import { withMobileAuth } from '@/js/auth/withMobileAuth'
import { errorHandler } from '../login/route'

/**
* Mobile refresh token handler
Expand All @@ -27,10 +28,9 @@ async function postHandler (request: NextRequest): Promise<any> {
audience: 'https://api.openbeta.io'
})

return NextResponse.json({ data: response.data })
return NextResponse.json({ ...response.data }, { status: response.status })
} catch (error) {
console.error('#### Auth0 error ####', error)
return NextResponse.json({ error: 'Unexpected auth error', status: 403 })
return errorHandler(error)
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/app/api/user/bulkImportTicks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from 'next/server'
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'
import { withUserAuth, PREDEFINED_HEADERS } from '@/js/auth/withUserAuth'

export interface Tick {
name: string
Expand Down Expand Up @@ -79,8 +79,8 @@ async function getMPTicks (profileUrl: string): Promise<MPTick[]> {
}

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 uuid = req.headers.get(PREDEFINED_HEADERS.user_uuid)
const auth0Userid = req.headers.get(PREDEFINED_HEADERS.auth0_id)
const payload = await req.json()
const profileUrl: string = payload.profileUrl

Expand Down
65 changes: 65 additions & 0 deletions src/app/api/user/get-signed-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from 'next/server'
import { customAlphabet } from 'nanoid'
import { nolookalikesSafe } from 'nanoid-dictionary'
import { extname } from 'path'

import { withUserAuth, PREDEFINED_HEADERS } from '@/js/auth/withUserAuth'
import { getSignedUrlForUpload } from '@/js/media/storageClient'

export interface MediaPreSignedProps {
url: string
fullFilename: string
}

/**
* Endpoint for getting a signed url to upload a media file to remote cloud storage.
* Usage: `/api/user/get-signed-url?filename=image001.jpg`
* See https://cloud.google.com/storage/docs/access-control/signed-urls
*/
const getHanlder = async (req: NextRequest): Promise<any> => {
try {
const fullFilename = prepareFilenameFromRequest(req)
if (fullFilename == null) {
return NextResponse.json({ status: 400 })
}
const url = await getSignedUrlForUpload(fullFilename)

return NextResponse.json({ url, fullFilename: '/' + fullFilename })
} catch (e) {
console.error('Uploading to media server failed', e)
return NextResponse.json({ status: 500 })
}
}

export const GET = withUserAuth(getHanlder)

/**
* Random filename generator
*/
const safeFilename = (original: string): string => {
return safeRandomString() + extname(original)
}

const safeRandomString = customAlphabet(nolookalikesSafe, 10)

/**
* Construct the S3 path for a given media file and an authenticated user. Format: `u/{user_uuid}/{filename}`.
* It's super important **not** to have the leading slash '/'.
*/
export const prepareFilenameFromRequest = (req: NextRequest): string | null => {
const searchParams = req.nextUrl.searchParams
const filename = searchParams.get('filename')
if (filename == null) {
return null
}

const uuid = req.headers.get(PREDEFINED_HEADERS.user_uuid)
if (uuid == null) {
return null
}

/**
* Important: no starting slash / when working with buckets
*/
return `u/${uuid}/${safeFilename(filename)}`
}
6 changes: 3 additions & 3 deletions src/app/api/user/me/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { withUserAuth } from '@/js/auth/withUserAuth'
import { NextRequest, NextResponse } from 'next/server'
import { PREDEFINED_HEADERS, withUserAuth } from '@/js/auth/withUserAuth'
import useUserProfileCmd from '@/js/hooks/useUserProfileCmd'

/**
* Direct `/api/user/me` to `/u/<user_id`
*/
const getHandler = async (req: NextRequest): Promise<any> => {
const uuid = req.headers.get('x-openbeta-user-uuid')
const accessToken = req.headers.get('x-auth0-access-token')
const uuid = req.headers.get(PREDEFINED_HEADERS.user_uuid)
const accessToken = req.headers.get(PREDEFINED_HEADERS.access_token)

if (accessToken == null || uuid == null) {
return NextResponse.json({ status: 500 })
Expand Down
24 changes: 24 additions & 0 deletions src/app/api/user/remove-media/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextRequest, NextResponse } from 'next/server'

import { withUserAuth } from '@/js/auth/withUserAuth'
import { deleteMediaFromBucket } from '@/js/media/storageClient'
import { prepareFilenameFromRequest } from '../get-signed-url/route'

/**
* Endpoint for removing a media object from remote cloud storage
*/
const postHandler = async (req: NextRequest): Promise<any> => {
try {
const filename = prepareFilenameFromRequest(req)
if (filename == null) {
return NextResponse.json({ status: 400 })
}
await deleteMediaFromBucket(filename)
return NextResponse.json({ status: 200 })
} catch (e) {
console.log('Removing file from media server failed', e)
return NextResponse.json({ status: 500 })
}
}

export const POST = withUserAuth(postHandler)
26 changes: 23 additions & 3 deletions src/js/auth/initializeUserInDb.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getClient } from '../graphql/ServerClient'
import { MUTATION_UPDATE_PROFILE } from '../graphql/gql/users'
import { MUTATION_UPDATE_PROFILE, QUERY_GET_USERNAME_BY_UUID } from '../graphql/gql/users'
import { updateUser } from './ManagementClient'

import { Username } from '../types'
export interface UpdateUsernameInput {
userUuid: string
username: string
Expand All @@ -14,9 +14,18 @@ interface InitializeUserInDBParams extends UpdateUsernameInput {
auth0UserId: string
}

const serverClient = getClient()

/**
* Look up in our db (not Auth0) to see whether a user by uuid exists. If it doesn't then insert a new user profile.
*/
export const initializeUserInDB = async (params: InitializeUserInDBParams): Promise<boolean> => {
const { auth0UserId, accessToken, userUuid, username, email, avatar } = params
const res = await getClient().mutate<{ updateUserProfile?: boolean }, UpdateUsernameInput>({
const existed = await doesUserByUuidExist(userUuid)
if (existed != null) {
return false
}
const res = await serverClient.mutate<{ updateUserProfile?: boolean }, UpdateUsernameInput>({
mutation: MUTATION_UPDATE_PROFILE,
variables: {
userUuid,
Expand All @@ -42,3 +51,14 @@ export const initializeUserInDB = async (params: InitializeUserInDBParams): Prom
}
return success
}

const doesUserByUuidExist = async (userUuid: string): Promise<Username | null> => {
const res = await serverClient.query<{ getUsername?: Username }, { userUuid: string }>({
query: QUERY_GET_USERNAME_BY_UUID,
variables: {
userUuid
},
fetchPolicy: 'no-cache'
})
return res.data?.getUsername ?? null
}
4 changes: 2 additions & 2 deletions src/js/auth/withMobileAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ type Next13ApiHandler = (req: NextRequest) => Promise<NextResponse>
export const withMobileAuth = (handler: Next13ApiHandler): Next13ApiHandler => {
return async function (request: NextRequest) {
if (request.method !== 'POST') {
return NextResponse.json({ message: 'Must send POST request', status: 405 })
return NextResponse.json({ message: 'Must send POST request' }, { status: 405 })
}
const authHeader = request.headers.get('Secret')
if (mobileAuthSecret != null && authHeader === mobileAuthSecret) {
return await handler(request)
}
return NextResponse.json({ message: 'Unauthorized', status: 401 })
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 })
}
}
12 changes: 9 additions & 3 deletions src/js/auth/withUserAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ export const withUserAuth = (handler: Next13APIHandler): Next13APIHandler => {
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)
req.headers.set('x-auth0-access-token', session.accessToken)
req.headers.set(PREDEFINED_HEADERS.user_uuid, session.user.metadata.uuid)
req.headers.set(PREDEFINED_HEADERS.auth0_id, session.id)
req.headers.set(PREDEFINED_HEADERS.access_token, session.accessToken)
return await handler(req)
} else {
return NextResponse.json({ status: 401 })
}
}
}

export enum PREDEFINED_HEADERS {
user_uuid = 'x-openbeta-user-uuid',
auth0_id = 'x-auth0-userid', // Example: 'auth0|1237492749372923498234'
access_token = 'x-auth0-access-token' // JWT token
}
Loading

0 comments on commit 591154e

Please sign in to comment.