From 0fc6700f097673f6f477f3bc28cc337832b1495b Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:06:07 -0500 Subject: [PATCH 01/16] Fix redis locks from use server access --- lib/redis-lock.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/redis-lock.ts b/lib/redis-lock.ts index dfe1b25e..8d793c92 100644 --- a/lib/redis-lock.ts +++ b/lib/redis-lock.ts @@ -1,10 +1,11 @@ -'use server' +import 'server-only' + import { kv } from '@vercel/kv' import { v4 as uuidv4 } from 'uuid' const LOCK_TIMEOUT = 30 * 1000 // 30 seconds -export async function aquireLock(key: string): Promise { +async function aquireLock(key: string): Promise { const lockKey = `lock:${key}` const lockValue = uuidv4() const acquired = await kv.set(lockKey, lockValue, { @@ -14,7 +15,7 @@ export async function aquireLock(key: string): Promise { return acquired ? lockValue : null } -export async function releaseLock( +async function releaseLock( lockKey: string, lockValue: string, ): Promise { From 33d3a868ce417279054ed732257e8df9b618bcec Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:06:23 -0500 Subject: [PATCH 02/16] Remove unnecessary use server from marketing page --- src/app/page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index ffe9b4fb..99a9bfb5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,3 @@ -'use server' - import Marketing from './marketing/page' export default async function Page() { From 163005bd61ef3e95891944c4cb76a977ebd27be3 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:27:28 -0500 Subject: [PATCH 03/16] Background jobs don't 'use server' --- src/app/api/cron/process-background-jobs.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/api/cron/process-background-jobs.ts b/src/app/api/cron/process-background-jobs.ts index c865b5e7..e253363c 100644 --- a/src/app/api/cron/process-background-jobs.ts +++ b/src/app/api/cron/process-background-jobs.ts @@ -1,7 +1,6 @@ -'use server' +import 'server-only' import { sql } from '@vercel/postgres' -import Airtable from 'airtable' async function processPendingInviteJobs() { const { rows } = From a6e9fba8f45396072c1f1fa071ffecdc52c54819 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:32:36 -0500 Subject: [PATCH 04/16] Make project ideas route 'server-only' --- src/app/api/project_ideas/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/project_ideas/route.ts b/src/app/api/project_ideas/route.ts index 493b31b1..164e7527 100644 --- a/src/app/api/project_ideas/route.ts +++ b/src/app/api/project_ideas/route.ts @@ -1,4 +1,4 @@ -'use server' +import 'server-only' import { NextRequest, NextResponse } from 'next/server' import { sample } from '../../../../lib/flavor' From b2b589d56f72b42b0d25c4bacdcc912176cc084c Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:33:31 -0500 Subject: [PATCH 05/16] Switch referral autonum to 'server-only' --- src/app/api/referral/[autonum]/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/referral/[autonum]/route.ts b/src/app/api/referral/[autonum]/route.ts index 6f19fb9d..5ddc917c 100644 --- a/src/app/api/referral/[autonum]/route.ts +++ b/src/app/api/referral/[autonum]/route.ts @@ -1,4 +1,4 @@ -'use server' +import 'server-only' import { getPersonByAuto } from '@/app/utils/airtable' import { redirect } from 'next/navigation' From ab144fbb2b6dac9a7334fb43a819d8ca5a54e96b Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:40:11 -0500 Subject: [PATCH 06/16] Check session validity in sendFraudReport --- src/app/harbor/battles/fraud-utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/harbor/battles/fraud-utils.ts b/src/app/harbor/battles/fraud-utils.ts index 81f86cc6..3e559445 100644 --- a/src/app/harbor/battles/fraud-utils.ts +++ b/src/app/harbor/battles/fraud-utils.ts @@ -9,6 +9,9 @@ export async function sendFraudReport( reason: string, ) { const session = await getSession() + if (!session) { + throw new Error('Unauthorized request') + } const res = await fetch( 'https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/flagged_projects', From b3c86d38b279b73922af76363ce20bf2fe188258 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:42:57 -0500 Subject: [PATCH 07/16] Rename battles component to match file name --- src/app/harbor/battles/battles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/harbor/battles/battles.tsx b/src/app/harbor/battles/battles.tsx index 33da9921..10393c36 100644 --- a/src/app/harbor/battles/battles.tsx +++ b/src/app/harbor/battles/battles.tsx @@ -33,7 +33,7 @@ interface Matchup { ts: number } -export default function Matchups({ session }: { session: HsSession }) { +export default function Battles({ session }: { session: HsSession }) { const [matchup, setMatchup] = useState(null) const [loading, setLoading] = useState(true) const [selectedProject, setSelectedProject] = useState(null) From 7203772731a0d3bf9a83e05b05a9162ef6eb263f Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 11:44:57 -0500 Subject: [PATCH 08/16] Remove unused/unaudited gallery code --- src/app/harbor/gallery/gallery.tsx | 77 ------------------------------ src/app/harbor/gallery/utils.ts | 55 --------------------- 2 files changed, 132 deletions(-) delete mode 100644 src/app/harbor/gallery/gallery.tsx delete mode 100644 src/app/harbor/gallery/utils.ts diff --git a/src/app/harbor/gallery/gallery.tsx b/src/app/harbor/gallery/gallery.tsx deleted file mode 100644 index 55ad13cc..00000000 --- a/src/app/harbor/gallery/gallery.tsx +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import { useEffect, useState, useCallback } from 'react' -import InfiniteScroll from 'react-infinite-scroll-component' -import { getShips } from './utils' -import { Ship } from '../shipyard/ship-utils' -import Ships from '../shipyard/ships' - -export type ShipsObject = Record - -export default function Gallery({ ships, setShips }: any) { - const [nextOffset, setNextOffset] = useState(undefined) - const [hasMore, setHasMore] = useState(true) - const [isLoading, setIsLoading] = useState(false) - - const fetchShips = useCallback(async () => { - if (isLoading || !hasMore) return - setIsLoading(true) - - try { - const newShipInfo = await getShips(nextOffset) - setNextOffset(newShipInfo.offset) - setHasMore(!!newShipInfo.offset) - - setShips((prev: any) => { - const newShips = newShipInfo.ships.reduce( - (acc: ShipsObject, ship: Ship) => { - acc[ship.id] = ship - return acc - }, - {}, - ) - return { ...prev, ...newShips } - }) - } catch (error) { - console.error('Error fetching ships:', error) - setHasMore(false) - setIsLoading(false) - } finally { - setIsLoading(false) - } - }, [isLoading, hasMore, nextOffset]) - - useEffect(() => { - if (Object.keys(ships).length === 0) { - fetchShips() - } - }, []) - - if (!ships) return

Loading all ships...

- - const shipsArray: Ship[] = Object.values(ships) - - if (shipsArray.length === 0) - return

Loading all ships...

- - return ( - - Loading -

- } - endMessage={ -

- Yay! You have seen all {shipsArray.length} ships. -

- } - scrollableTarget="harbor-tab-scroll-element" - > - -
- ) -} diff --git a/src/app/harbor/gallery/utils.ts b/src/app/harbor/gallery/utils.ts deleted file mode 100644 index de270bb5..00000000 --- a/src/app/harbor/gallery/utils.ts +++ /dev/null @@ -1,55 +0,0 @@ -'use server' - -import { Ship } from '../shipyard/ship-utils' - -interface AirtableShipRow { - id: string - fields: { - title: string - readme_url: string - repo_url: string - screenshot_url: string - hours: number - rating: number - } -} - -export async function getShips(offset: string | undefined): Promise<{ - ships: Ship[] - offset: string | undefined -}> { - const res = await fetch( - `https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/ships?view=Grid%20view${offset ? `&offset=${offset}` : ''}`, - { - headers: { - Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`, - 'User-Agent': 'highseas.hackclub.com (getShips)', - }, - }, - ) - let data - try { - data = await res.json() - } catch (e) { - console.error(e, await res.text()) - throw e - } - - // TODO: Error checking - const ships = data.records.map((r: AirtableShipRow) => { - return { - id: r.id, - title: r.fields.title, - readmeUrl: r.fields.readme_url, - hours: r.fields.hours, - repoUrl: r.fields.repo_url, - screenshotUrl: r.fields.screenshot_url, - rating: r.fields.rating, - } - }) - - return { - ships, - offset: data.offset, - } -} From 4977680576b5123d2340608a9cc64f59bb020549 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 12:22:23 -0500 Subject: [PATCH 09/16] Fix invite-job overusage of 'use server' --- src/app/marketing/components/email-submission-form.tsx | 2 +- src/app/marketing/invite-job.js | 10 +++------- src/app/marketing/send-invite-job.js | 5 +++++ 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 src/app/marketing/send-invite-job.js diff --git a/src/app/marketing/components/email-submission-form.tsx b/src/app/marketing/components/email-submission-form.tsx index a7edb400..2a41ebfa 100644 --- a/src/app/marketing/components/email-submission-form.tsx +++ b/src/app/marketing/components/email-submission-form.tsx @@ -6,7 +6,7 @@ import { Button } from '../../../components/ui/button' import Icon from '@hackclub/icons' import Modal from '../../../components/ui/modal' import { handleEmailSubmission } from '../marketing-utils' -import { sendInviteJob } from '../invite-job' +import { sendInviteJob } from '../send-invite-job' import { usePlausible } from 'next-plausible' export default function EmailSubmissionForm() { diff --git a/src/app/marketing/invite-job.js b/src/app/marketing/invite-job.js index ffa659f9..410459b6 100644 --- a/src/app/marketing/invite-job.js +++ b/src/app/marketing/invite-job.js @@ -1,19 +1,17 @@ -'use server' - import { sql } from '@vercel/postgres' import { headers } from 'next/headers' -async function sendInviteJob({ email, userAgent }) { +export async function sendInviteJob({ email, userAgent }) { const headersList = headers() const ipAddress = headersList.get('x-forwarded-for') - const result = + const _result = await sql`INSERT INTO invite_job (email, ip_address, user_agent) VALUES (${email}, ${ipAddress}, ${userAgent});` // return result; } -async function processPendingInviteJobs() { +export async function processPendingInviteJobs() { const { rows } = await sql`SELECT * FROM invite_job WHERE airtable_invite_record_id IS NULL LIMIT 10` @@ -61,5 +59,3 @@ async function processPendingInviteJobs() { await sql`UPDATE invite_job SET airtable_invite_record_id = ${id} WHERE email = ${email} AND airtable_invite_record_id IS NULL;` } } - -export { sendInviteJob, processPendingInviteJobs } diff --git a/src/app/marketing/send-invite-job.js b/src/app/marketing/send-invite-job.js new file mode 100644 index 00000000..6e4e497c --- /dev/null +++ b/src/app/marketing/send-invite-job.js @@ -0,0 +1,5 @@ +'use server' + +import { sendInviteJob } from './invite-job.js' + +export { sendInviteJob } \ No newline at end of file From 72b2cebe0cc415cb28609c236d8aee02bb8f5a83 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 12:23:02 -0500 Subject: [PATCH 10/16] Add 'server-only' to invite job --- src/app/marketing/invite-job.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/marketing/invite-job.js b/src/app/marketing/invite-job.js index 410459b6..d5173508 100644 --- a/src/app/marketing/invite-job.js +++ b/src/app/marketing/invite-job.js @@ -1,3 +1,5 @@ +import 'server-only' + import { sql } from '@vercel/postgres' import { headers } from 'next/headers' From 9cda06486375569b6fba92ba7848e0e7bd539f02 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:21:28 -0500 Subject: [PATCH 11/16] Switch airtable.ts & waka.ts to 'server-only' --- lib/redis-lock.ts | 5 +- src/app/api/buy/[item]/route.js | 4 +- src/app/harbor/battles/battles.tsx | 35 +--- src/app/harbor/shipyard/new-ship-form.tsx | 2 +- src/app/harbor/shipyard/new-update-form.tsx | 2 +- src/app/harbor/shipyard/ship-utils.ts | 8 +- src/app/harbor/shipyard/shipyard.tsx | 4 +- src/app/harbor/shop/shop-utils.ts | 6 +- src/app/harbor/shop/shop.tsx | 4 +- src/app/harbor/signpost/leaderboard.tsx | 6 +- src/app/harbor/signpost/referral.tsx | 4 +- src/app/harbor/signpost/signpost.tsx | 3 +- src/app/harbor/tabs/tour.ts | 5 +- src/app/marketing/send-invite-job.js | 2 +- src/app/utils/airtable.ts | 40 ++--- src/app/utils/auth.ts | 7 +- .../utils/get-leaderboard-participating.ts | 3 + src/app/utils/get-safe-person.ts | 4 + src/app/utils/get-self-person.ts | 5 + ...t-votes-remaining-for-next-pending-ship.ts | 5 + src/app/utils/get-waka-sessions.ts | 5 + .../utils/report-leaderboard-participating.ts | 3 + src/app/utils/report-tour-step.ts | 5 + src/app/utils/waka.ts | 163 +----------------- 24 files changed, 85 insertions(+), 245 deletions(-) create mode 100644 src/app/utils/get-leaderboard-participating.ts create mode 100644 src/app/utils/get-safe-person.ts create mode 100644 src/app/utils/get-self-person.ts create mode 100644 src/app/utils/get-votes-remaining-for-next-pending-ship.ts create mode 100644 src/app/utils/get-waka-sessions.ts create mode 100644 src/app/utils/report-leaderboard-participating.ts create mode 100644 src/app/utils/report-tour-step.ts diff --git a/lib/redis-lock.ts b/lib/redis-lock.ts index 8d793c92..66c50045 100644 --- a/lib/redis-lock.ts +++ b/lib/redis-lock.ts @@ -15,10 +15,7 @@ async function aquireLock(key: string): Promise { return acquired ? lockValue : null } -async function releaseLock( - lockKey: string, - lockValue: string, -): Promise { +async function releaseLock(lockKey: string, lockValue: string): Promise { const currentLockValue = await kv.get(lockKey) if (currentLockValue === lockValue) { await kv.del(lockKey) diff --git a/src/app/api/buy/[item]/route.js b/src/app/api/buy/[item]/route.js index d8781ca7..ad210e9f 100644 --- a/src/app/api/buy/[item]/route.js +++ b/src/app/api/buy/[item]/route.js @@ -1,12 +1,12 @@ import { getSession } from '@/app/utils/auth' import { redirect } from 'next/navigation' import { NextResponse } from 'next/server' -import { getSelfPerson } from '@/app/utils/airtable' +import { getSelfPerson } from '@/app/utils/get-self-person' import { base } from 'airtable' export async function GET(request, { params }) { const session = await getSession() - const person = await getSelfPerson(session.slackId) + const person = await getSelfPerson() if (!person) { return NextResponse.json( { error: "i don't even know who you are" }, diff --git a/src/app/harbor/battles/battles.tsx b/src/app/harbor/battles/battles.tsx index 10393c36..2915141b 100644 --- a/src/app/harbor/battles/battles.tsx +++ b/src/app/harbor/battles/battles.tsx @@ -7,10 +7,8 @@ import { AnimatePresence, motion } from 'framer-motion' import ReactMarkdown from 'react-markdown' import { LoadingSpinner } from '../../../components/ui/loading_spinner.js' -import { - getVotesRemainingForNextPendingShip, - safePerson, -} from '@/app/utils/airtable' +import { getVotesRemainingForNextPendingShip } from '@/app/utils/get-votes-remaining-for-next-pending-ship' +import { getSafePerson } from '@/app/utils/get-safe-person' import useLocalStorageState from '../../../../lib/useLocalStorageState' import { useToast } from '@/hooks/use-toast' import { HsSession } from '@/app/utils/auth' @@ -81,7 +79,7 @@ export default function Battles({ session }: { session: HsSession }) { }) useEffect(() => { - safePerson().then((sp) => { + getSafePerson().then((sp) => { setCursed(sp.cursed) setBlessed(sp.blessed) }) @@ -174,33 +172,8 @@ export default function Battles({ session }: { session: HsSession }) { }, interval) } - // useEffect(() => { - // if (turnstileRef.current) { - // let widgetId; - - // const genToken = () => { - // widgetId = window.turnstile!.render(turnstileRef.current, { - // sitekey: "0x4AAAAAAAzOAaBz1TUgJG68", // Site key - // theme: "dark", - // callback: (token: string) => { - // console.log(token); - // setTurnstileToken(token); - // }, - // }); - // }; - // genToken(); - - // const genTokenInterval = setInterval(genToken, 4 * 60 * 1_000); // Every 4 minutes - - // return () => { - // window.turnstile!.reset(widgetId); - // clearInterval(genTokenInterval); - // }; - // } - // }, [selectedProject]); - const fetchVoteBalance = async () => { - setVoteBalance(await getVotesRemainingForNextPendingShip(session.slackId)) + setVoteBalance(await getVotesRemainingForNextPendingShip()) } const fetchMatchup = async ( diff --git a/src/app/harbor/shipyard/new-ship-form.tsx b/src/app/harbor/shipyard/new-ship-form.tsx index 5821b529..e7e31e77 100644 --- a/src/app/harbor/shipyard/new-ship-form.tsx +++ b/src/app/harbor/shipyard/new-ship-form.tsx @@ -5,7 +5,7 @@ import { createShip, Ship } from './ship-utils' import { Button } from '@/components/ui/button' import JSConfetti from 'js-confetti' import { useEffect, useRef, useState } from 'react' -import { getWakaSessions } from '@/app/utils/waka' +import { getWakaSessions } from '@/app/utils/get-waka-sessions' import { AnimatePresence, motion } from 'framer-motion' import { useToast } from '@/hooks/use-toast' import Icon from '@hackclub/icons' diff --git a/src/app/harbor/shipyard/new-update-form.tsx b/src/app/harbor/shipyard/new-update-form.tsx index 6e72563c..bf837de3 100644 --- a/src/app/harbor/shipyard/new-update-form.tsx +++ b/src/app/harbor/shipyard/new-update-form.tsx @@ -4,7 +4,7 @@ import type { Ship } from '@/app/utils/data' import { Button } from '@/components/ui/button' import JSConfetti from 'js-confetti' import { useCallback, useEffect, useRef, useState } from 'react' -import { getWakaSessions } from '@/app/utils/waka' +import { getWakaSessions } from '@/app/utils/get-waka-sessions' import Icon from '@hackclub/icons' export default function NewUpdateForm({ diff --git a/src/app/harbor/shipyard/ship-utils.ts b/src/app/harbor/shipyard/ship-utils.ts index 84e0ff2e..83c06b66 100644 --- a/src/app/harbor/shipyard/ship-utils.ts +++ b/src/app/harbor/shipyard/ship-utils.ts @@ -1,9 +1,9 @@ 'use server' -import { getSelfPerson } from '@/app/utils/airtable' +import { getSelfPerson } from '@/app/utils/get-self-person' import { getSession } from '@/app/utils/auth' import { fetchShips, person } from '@/app/utils/data' -import { getWakaSessions } from '@/app/utils/waka' +import { getWakaSessions } from '@/app/utils/get-waka-sessions' import { cookies } from 'next/headers' import type { Ship } from '@/app/utils/data' import Airtable from 'airtable' @@ -45,7 +45,7 @@ export async function createShip(formData: FormData, isTutorial: boolean) { const slackId = session.slackId return await withLock(`ship:${slackId}`, async () => { - const entrantId = await getSelfPerson(slackId).then((p) => p.id) + const entrantId = await getSelfPerson().then((p) => p.id) const isShipUpdate = formData.get('isShipUpdate') @@ -100,7 +100,7 @@ export async function createShipUpdate( const slackId = session.slackId return withLock(`update:${slackId}`, async () => { - const entrantId = await getSelfPerson(slackId).then((p) => p.id) + const entrantId = await getSelfPerson().then((p) => p.id) // This pattern makes sure the ship data is not fraudulent const ships = await fetchShips(slackId) diff --git a/src/app/harbor/shipyard/shipyard.tsx b/src/app/harbor/shipyard/shipyard.tsx index 42e54419..8c7f2e8d 100644 --- a/src/app/harbor/shipyard/shipyard.tsx +++ b/src/app/harbor/shipyard/shipyard.tsx @@ -3,7 +3,7 @@ import Ships from './ships' import useLocalStorageState from '../../../../lib/useLocalStorageState' import { useEffect } from 'react' -import { getVotesRemainingForNextPendingShip } from '@/app/utils/airtable' +import { getVotesRemainingForNextPendingShip } from '@/app/utils/get-votes-remaining-for-next-pending-ship' import Pill from '@/components/ui/pill' import { fetchShips, Ship } from '@/app/utils/data' import { IdeaGenerator } from './idea-generator/impl' @@ -44,7 +44,7 @@ export default function Shipyard({ session }: any) { useEffect(() => { fetchShips(session.slackId).then((ships) => setShips(ships)) - getVotesRemainingForNextPendingShip(session.slackId).then((balance) => + getVotesRemainingForNextPendingShip().then((balance) => setVoteBalance(balance), ) }, []) diff --git a/src/app/harbor/shop/shop-utils.ts b/src/app/harbor/shop/shop-utils.ts index 7b357f14..5d36d374 100644 --- a/src/app/harbor/shop/shop-utils.ts +++ b/src/app/harbor/shop/shop-utils.ts @@ -2,7 +2,7 @@ import Airtable from 'airtable' import { getSession } from '@/app/utils/auth' -import { getSelfPerson } from '@/app/utils/airtable' +import { getSelfPerson } from '@/app/utils/get-self-person' import { NextResponse } from 'next/server' const base = () => { @@ -37,7 +37,7 @@ export async function getPerson() { if (!('slackId' in session)) { return } - const person = await getSelfPerson(session.slackId) + const person = await getSelfPerson() if (!person) { return NextResponse.json( { error: "i don't even know who you are" }, @@ -52,7 +52,7 @@ export async function getShop(): Promise { if (!('slackId' in session)) { return } - const person = await getSelfPerson(session.slackId) + const person = await getSelfPerson() return new Promise((resolve, reject) => { base()('shop_items') diff --git a/src/app/harbor/shop/shop.tsx b/src/app/harbor/shop/shop.tsx index e90227b7..f4feab49 100644 --- a/src/app/harbor/shop/shop.tsx +++ b/src/app/harbor/shop/shop.tsx @@ -9,7 +9,7 @@ import JaggedCardSmall from '@/components/jagged-card-small' import { ShopItemComponent } from './shop-item-component.js' import { ShopkeeperComponent } from './shopkeeper.js' -import { safePerson } from '@/app/utils/airtable' +import { getSafePerson } from '@/app/utils/get-safe-person' import Progress from './progress.tsx' export default function Shop({ session }: { session: HsSession }) { const [filterIndex, setFilterIndex] = useLocalStorageState( @@ -28,7 +28,7 @@ export default function Shop({ session }: { session: HsSession }) { useEffect(() => { setBannerText(sample(shopBanner)) getShop().then((shop) => setShopItems(shop)) - safePerson().then((sp) => setPersonTicketBalance(sp.settledTickets)) + getSafePerson().then((sp) => setPersonTicketBalance(sp.settledTickets)) }, []) const [favouriteItems, setFavouriteItems] = useState( JSON.parse(localStorage.getItem('favouriteItems') ?? '[]'), diff --git a/src/app/harbor/signpost/leaderboard.tsx b/src/app/harbor/signpost/leaderboard.tsx index fe90eb16..4e5be2e2 100644 --- a/src/app/harbor/signpost/leaderboard.tsx +++ b/src/app/harbor/signpost/leaderboard.tsx @@ -1,10 +1,8 @@ import { Button } from '@/components/ui/button' import Icon from '@hackclub/icons' import { useEffect, useState } from 'react' -import { - getLeaderboardParticipating, - reportLeaderboardParticipating, -} from '@/app/utils/airtable' +import { getLeaderboardParticipating } from '@/app/utils/get-leaderboard-participating' +import { reportLeaderboardParticipating } from '@/app/utils/report-leaderboard-participating' export default function LeaderboardOptIn() { const [optedIn, setOptedIn] = useState(false) diff --git a/src/app/harbor/signpost/referral.tsx b/src/app/harbor/signpost/referral.tsx index e8636b89..66ac8077 100644 --- a/src/app/harbor/signpost/referral.tsx +++ b/src/app/harbor/signpost/referral.tsx @@ -10,14 +10,14 @@ import { PopoverContent, } from '@/components/ui/popover' import Icon from '@hackclub/icons' -import { safePerson } from '@/app/utils/airtable' +import { getSafePerson } from '@/app/utils/get-safe-person' export default function Referral() { const [shareLink, setShareLink] = useState(null) const [open, setOpen] = useState(false) useEffect(() => { - safePerson().then((sp) => { + getSafePerson().then((sp) => { if (sp?.referralLink) { setShareLink(sp.referralLink) } diff --git a/src/app/harbor/signpost/signpost.tsx b/src/app/harbor/signpost/signpost.tsx index 75eb5467..52b595b5 100644 --- a/src/app/harbor/signpost/signpost.tsx +++ b/src/app/harbor/signpost/signpost.tsx @@ -6,10 +6,9 @@ import { motion } from 'framer-motion' import Verification from './verification' import Platforms from '@/app/utils/wakatime-setup/platforms' import JaggedCard from '../../../components/jagged-card' -import JaggedCardSmall from '@/components/jagged-card-small' import Cookies from 'js-cookie' import FeedItems from './feed-items' -import { getWakaSessions } from '@/app/utils/waka' +import { getWakaSessions } from '@/app/utils/get-waka-sessions' import Referral from './referral' import { getStickyUrls } from './help' import pluralize from '../../../../lib/pluralize.js' diff --git a/src/app/harbor/tabs/tour.ts b/src/app/harbor/tabs/tour.ts index 21ede11a..9782a686 100644 --- a/src/app/harbor/tabs/tour.ts +++ b/src/app/harbor/tabs/tour.ts @@ -2,7 +2,8 @@ import Shepherd, { type Tour } from 'shepherd.js' import './shepherd.css' import { offset } from '@floating-ui/dom' import Cookies from 'js-cookie' -import { reportTourStep, safePerson } from '../../utils/airtable' +import { reportTourStep } from '@/app/utils/report-tour-step' +import { getSafePerson } from '@/app/utils/get-safe-person' const waitForElement = ( selector: string, @@ -58,7 +59,7 @@ const t = new Shepherd.Tour({ let hasSetUp = false export function tour() { - safePerson().then(({ hasCompletedTutorial }) => { + getSafePerson().then(({ hasCompletedTutorial }) => { console.log('Setting tutorial sessionstorage to', hasCompletedTutorial) sessionStorage.setItem('tutorial', (!hasCompletedTutorial).toString()) }) diff --git a/src/app/marketing/send-invite-job.js b/src/app/marketing/send-invite-job.js index 6e4e497c..d19c383b 100644 --- a/src/app/marketing/send-invite-job.js +++ b/src/app/marketing/send-invite-job.js @@ -2,4 +2,4 @@ import { sendInviteJob } from './invite-job.js' -export { sendInviteJob } \ No newline at end of file +export { sendInviteJob } diff --git a/src/app/utils/airtable.ts b/src/app/utils/airtable.ts index ef1c63ff..ab5ee8a1 100644 --- a/src/app/utils/airtable.ts +++ b/src/app/utils/airtable.ts @@ -1,19 +1,12 @@ -'use server' +import 'server-only' import { getSession } from './auth' import { person, updateShowInLeaderboard } from './data' -export const getSelfPerson = async (slackId: string) => { - const session = await getSession() - if (!session) { - throw new Error('No session when trying to get self person') +export const getPersonBySlackId = async (slackId: string) => { + if (!slackId) { + throw new Error('No slackId provided') } - if (session.slackId !== slackId) { - const err = new Error('Session slackId does not match provided slackId') - console.error(err) - throw err - } - const url = `https://middleman.hackclub.com/airtable/v0/${process.env.BASE_ID}/people` const filterByFormula = encodeURIComponent(`{slack_id} = '${slackId}'`) const response = await fetch(`${url}?filterByFormula=${filterByFormula}`, { @@ -39,6 +32,15 @@ export const getSelfPerson = async (slackId: string) => { return data.records[0] } +export const getSelfPerson = async () => { + const session = await getSession() + if (!session) { + throw new Error('No session when trying to get self person') + } + + return getPersonBySlackId(session?.slackId) +} + export const getSignpostUpdates = async () => { const url = `https://middleman.hackclub.com/airtable/v0/${process.env.BASE_ID}/signpost` const response = await fetch(url, { @@ -138,16 +140,14 @@ export async function getPersonByMagicToken(token: string): Promise<{ return { id, email, slackId } } -export async function getSelfPersonIdentifier(slackId: string) { - const person = await getSelfPerson(slackId) +export async function getSelfPersonIdentifier() { + const person = await getSelfPerson() return person.fields.identifier } export const getPersonTicketBalanceAndTutorialStatutWowThisMethodNameSureIsLongPhew = - async ( - slackId: string, - ): Promise<{ tickets: number; hasCompletedTutorial: boolean }> => { - const person = await getSelfPerson(slackId) + async (): Promise<{ tickets: number; hasCompletedTutorial: boolean }> => { + const person = await getSelfPerson() const tickets = person.fields.settled_tickets as number const hasCompletedTutorial = person.fields.academy_completed === true @@ -155,8 +155,8 @@ export const getPersonTicketBalanceAndTutorialStatutWowThisMethodNameSureIsLongP } // deprecate -export async function getVotesRemainingForNextPendingShip(slackId: string) { - const person = await getSelfPerson(slackId) +export async function getVotesRemainingForNextPendingShip() { + const person = await getSelfPerson() return person['fields']['votes_remaining_for_next_pending_ship'] as number } @@ -175,7 +175,7 @@ export interface SafePerson { } // Good method -export async function safePerson(): Promise { +export async function getSafePerson(): Promise { const record = await person() const id = record.id diff --git a/src/app/utils/auth.ts b/src/app/utils/auth.ts index 63f7cdb8..9051b165 100644 --- a/src/app/utils/auth.ts +++ b/src/app/utils/auth.ts @@ -1,7 +1,8 @@ 'use server' import { cookies, headers } from 'next/headers' -import { getPersonByMagicToken, getSelfPerson } from './airtable' +import { getPersonByMagicToken } from './airtable' +import { getPersonBySlackId } from './airtable' export interface HsSession { /// The Person record ID in the high seas base @@ -82,7 +83,7 @@ export async function impersonate(slackId: string) { } // look for airtable user with this record - const person = await getSelfPerson(slackId) + const person = await getPersonBySlackId(slackId) const id = person.id const email = person.fields.email @@ -124,7 +125,7 @@ export async function createSlackSession(slackOpenidToken: string) { if (!payload) throw new Error('Failed to decode the Slack OpenId JWT') - let person = (await getSelfPerson(payload.sub as string)) as any + let person = (await getPersonBySlackId(payload.sub as string)) as any if (!person) { const body = JSON.stringify({ diff --git a/src/app/utils/get-leaderboard-participating.ts b/src/app/utils/get-leaderboard-participating.ts new file mode 100644 index 00000000..f0a35149 --- /dev/null +++ b/src/app/utils/get-leaderboard-participating.ts @@ -0,0 +1,3 @@ +'use server' +import { getLeaderboardParticipating } from './airtable' +export { getLeaderboardParticipating } diff --git a/src/app/utils/get-safe-person.ts b/src/app/utils/get-safe-person.ts new file mode 100644 index 00000000..cb32c397 --- /dev/null +++ b/src/app/utils/get-safe-person.ts @@ -0,0 +1,4 @@ +'use server' + +import { getSafePerson } from './airtable' +export { getSafePerson } diff --git a/src/app/utils/get-self-person.ts b/src/app/utils/get-self-person.ts new file mode 100644 index 00000000..721b1ee0 --- /dev/null +++ b/src/app/utils/get-self-person.ts @@ -0,0 +1,5 @@ +'use server' + +import { getSelfPerson } from './airtable' + +export { getSelfPerson } diff --git a/src/app/utils/get-votes-remaining-for-next-pending-ship.ts b/src/app/utils/get-votes-remaining-for-next-pending-ship.ts new file mode 100644 index 00000000..3f242590 --- /dev/null +++ b/src/app/utils/get-votes-remaining-for-next-pending-ship.ts @@ -0,0 +1,5 @@ +'use server' + +import { getVotesRemainingForNextPendingShip } from './airtable' + +export { getVotesRemainingForNextPendingShip } diff --git a/src/app/utils/get-waka-sessions.ts b/src/app/utils/get-waka-sessions.ts new file mode 100644 index 00000000..925478b3 --- /dev/null +++ b/src/app/utils/get-waka-sessions.ts @@ -0,0 +1,5 @@ +'use server' + +import { getWakaSessions } from '@/app/utils/waka' + +export { getWakaSessions } diff --git a/src/app/utils/report-leaderboard-participating.ts b/src/app/utils/report-leaderboard-participating.ts new file mode 100644 index 00000000..92f149e0 --- /dev/null +++ b/src/app/utils/report-leaderboard-participating.ts @@ -0,0 +1,3 @@ +'use server' +import { reportLeaderboardParticipating } from './airtable' +export { reportLeaderboardParticipating } diff --git a/src/app/utils/report-tour-step.ts b/src/app/utils/report-tour-step.ts new file mode 100644 index 00000000..d2867247 --- /dev/null +++ b/src/app/utils/report-tour-step.ts @@ -0,0 +1,5 @@ +'use server' + +import { reportTourStep } from './airtable' + +export { reportTourStep } diff --git a/src/app/utils/waka.ts b/src/app/utils/waka.ts index b0867ff3..e9bf5263 100644 --- a/src/app/utils/waka.ts +++ b/src/app/utils/waka.ts @@ -1,8 +1,6 @@ -'use server' +import 'server-only' -import { cookies, headers } from 'next/headers' -import { redirect } from 'next/navigation' -import { getSession, HsSession } from './auth' +import { getSession } from './auth' import { fetchWaka } from './data' const WAKA_API_KEY = process.env.WAKA_API_KEY @@ -17,110 +15,6 @@ export interface WakaInfo { key: string } -// Moved -// export async function waka(): Promise { -// return new Promise(async (resolve, reject) => { -// const p = await person(); -// const { -// wakatime_username, -// wakatime_key, -// slack_id, -// email, -// name, -// preexistingUser, -// } = p.fields; - -// if (wakatime_key && wakatime_username) { -// const info = { -// username: wakatime_username, -// key: wakatime_key, -// }; -// console.log("[waka::waka] From Airtable:", info); -// return resolve(info); -// } - -// const legacyKeyRaw = cookies().get("waka-key")?.value as string | undefined; -// if (preexistingUser && slack_id && legacyKeyRaw) { -// let legacyKey; -// try { -// legacyKey = JSON.parse(legacyKeyRaw); -// } catch { -// const error = new Error( -// `Could not parse legacy cookie: ${legacyKeyRaw}`, -// ); -// console.error(error); -// throw error; -// } - -// const info = { -// username: slack_id, -// key: legacyKey.api_key, -// }; -// console.log("[waka::waka] From legacy:", info); -// return resolve(info); -// } - -// // Create -// const newWakaInfo = await createWaka(email, name ?? null, slack_id ?? null); - -// // Add to person record -// const res = await fetch( -// `https://middleman.hackclub.com/airtable/v0/appTeNFYcUiYfGcR6/people`, -// { -// method: "PATCH", -// headers: { -// Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`, -// "Content-Type": "application/json", -// }, -// body: JSON.stringify({ -// records: [ -// { -// id: p.id, -// fields: { -// wakatime_username: newWakaInfo.username, -// wakatime_key: newWakaInfo.key, -// }, -// }, -// ], -// }), -// }, -// ).then((d) => d.json()); - -// console.log("[waka::waka] From created:", newWakaInfo); -// return resolve(newWakaInfo); -// }); -// } - -// Deprecated -// export async function getWaka(): Promise { -// let key = cookies().get("waka-key"); -// if (!key) { -// const session = await getSession(); - -// if (!session?.email) -// throw new Error("You can't make a wakatime account without an email!"); - -// await createWaka(session.email, session?.name ?? null, session?.slackId); -// console.log("Created a wakatime account from getWaka. Session: ", session); -// key = cookies().get("waka-key"); -// if (!key) return null; -// } - -// return JSON.parse(key.value) as WakaSignupResponse; -// } - -// Deprecated -// async function setWaka(username: string, resp: WakaSignupResponse) { -// cookies().set("waka-key", JSON.stringify(resp), { -// secure: process.env.NODE_ENV !== "development", -// httpOnly: true, -// }); -// cookies().set("waka-username", JSON.stringify(username), { -// secure: process.env.NODE_ENV !== "development", -// httpOnly: true, -// }); -// } - // Good function export async function createWaka( email: string, @@ -129,12 +23,6 @@ export async function createWaka( ): Promise { const password = crypto.randomUUID() - // if (!slackId) { - // const err = new Error("No slack ID found while trying to create WakaTime"); - // console.error(err); - // throw err; - // } - const payload = { location: 'America/New_York', email, @@ -208,50 +96,3 @@ export async function getWakaSessions(interval?: string): Promise<{ return summaryResJson } - -// export async function hasRecvFirstHeartbeat(): Promise { -// try { -// const session = await getSession(); -// if (!session) -// throw new Error( -// "No Slack OAuth session found while trying to get WakaTime sessions.", -// ); - -// const slackId = session.slackId; - -// const hasDataRes: { hasData: boolean } = await fetch( -// `https://waka.hackclub.com/api/special/hasData/?user=${slackId}`, -// { -// headers: { -// Authorization: `Bearer ${WAKA_API_KEY}`, -// }, -// }, -// ).then((res) => res.json()); - -// return hasDataRes.hasData; -// } catch (e) { -// console.error(e); -// return false; -// } -// } - -// export async function getWakaEmail(): Promise { -// const session = await getSession(); -// if (!session) -// throw new Error( -// "No Slack OAuth session found while trying to get WakaTime sessions.", -// ); - -// const slackId = session.slackId; - -// const email: { email: string | null } = await fetch( -// `https://waka.hackclub.com/api/special/email/?user=${slackId}`, -// { -// headers: { -// Authorization: `Bearer ${WAKA_API_KEY}`, -// }, -// }, -// ).then((res) => res.json()); - -// return email.email; -// } From bad7181dba02db059e2880791801f101e75ca273 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:21:37 -0500 Subject: [PATCH 12/16] Remove unused import/export --- src/app/harbor/signpost/help.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/app/harbor/signpost/help.ts b/src/app/harbor/signpost/help.ts index 697d6136..966c43b5 100644 --- a/src/app/harbor/signpost/help.ts +++ b/src/app/harbor/signpost/help.ts @@ -5,13 +5,6 @@ const base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( process.env.BASE_ID!, ) -//TODO: Delete this file it's weird -import { getWakaSessions } from '@/app/utils/waka' - -export async function wakaSessions() { - return await getWakaSessions() -} - export async function getStickyUrls(): string[] { const allStickies = (await base('sticky_holidays').select({}).all()).sort( (a, b) => a.fields.id - b.fields.id, From bdfea9fc7012eb2a97bcb3254cc4288bf8e2a8f7 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:28:08 -0500 Subject: [PATCH 13/16] Delete tutorial-utils.ts --- .../utils/wakatime-setup/tutorial-utils.ts | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/app/utils/wakatime-setup/tutorial-utils.ts diff --git a/src/app/utils/wakatime-setup/tutorial-utils.ts b/src/app/utils/wakatime-setup/tutorial-utils.ts deleted file mode 100644 index 080c9e54..00000000 --- a/src/app/utils/wakatime-setup/tutorial-utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use server' - -import { getSession } from '../../utils/auth' - -// export async function hasHb(username: string, key: string): Promise { -// if (!username) -// throw new Error("Username is undefined while checking waka hasData"); - -// const res = await fetch( -// `https://waka.hackclub.com/api/special/hasData?user=${username}`, -// { headers: { Authorization: `Bearer ${process.env.WAKA_API_KEY}` } }, -// ); -// if (!res.ok) { -// const txt = await res.text(); -// const err = new Error( -// `Error while checking ${username}'s waka hasData: ${txt}`, -// ); -// console.error(err); -// throw err; -// } - -// const resJson = await res.json(); -// return resJson.hasData === true; -// } From 27e660f047210a21feb95c26e8667772fc50e89c Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:28:48 -0500 Subject: [PATCH 14/16] Switch auth.ts from 'use server' -> 'server-only' --- src/app/[tab]/page.tsx | 3 ++- src/app/harbor/shipyard/ships.tsx | 3 ++- src/app/harbor/tabs/tabs.tsx | 3 ++- src/app/utils/auth.ts | 2 +- src/app/utils/create-magic-session.ts | 4 ++++ src/app/utils/get-session.ts | 4 ++++ 6 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 src/app/utils/create-magic-session.ts create mode 100644 src/app/utils/get-session.ts diff --git a/src/app/[tab]/page.tsx b/src/app/[tab]/page.tsx index 69415b58..bdd7c110 100644 --- a/src/app/[tab]/page.tsx +++ b/src/app/[tab]/page.tsx @@ -2,7 +2,8 @@ import { notFound } from 'next/navigation' import Harbor from '../harbor/tabs/tabs' -import { createMagicSession, getSession } from '../utils/auth' +import { createMagicSession } from '../utils/create-magic-session' +import { getSession } from '../utils/get-session' import { Card } from '@/components/ui/card' import { SoundButton } from '../../components/sound-button.js' import { useEffect } from 'react' diff --git a/src/app/harbor/shipyard/ships.tsx b/src/app/harbor/shipyard/ships.tsx index 7bbd1a58..d5bff3b1 100644 --- a/src/app/harbor/shipyard/ships.tsx +++ b/src/app/harbor/shipyard/ships.tsx @@ -11,7 +11,8 @@ import { markdownComponents } from '@/components/markdown' import { Button, buttonVariants } from '@/components/ui/button' import NewShipForm from './new-ship-form' import EditShipForm from './edit-ship-form' -import { getSession, HsSession } from '@/app/utils/auth' +import { type HsSession } from '@/app/utils/auth' +import { getSession } from '@/app/utils/get-session' import Link from 'next/link' import TimeAgo from 'javascript-time-ago' import ShipPillCluster from '@/components/ui/ship-pill-cluster' diff --git a/src/app/harbor/tabs/tabs.tsx b/src/app/harbor/tabs/tabs.tsx index f42b6897..1380b523 100644 --- a/src/app/harbor/tabs/tabs.tsx +++ b/src/app/harbor/tabs/tabs.tsx @@ -10,7 +10,8 @@ import SignPost from '../signpost/signpost' import { tour } from './tour' import useLocalStorageState from '../../../../lib/useLocalStorageState' import { useRouter } from 'next/navigation' -import { getSession, type HsSession } from '@/app/utils/auth' +import { type HsSession } from '@/app/utils/auth' +import { getSession } from '@/app/utils/get-session' import { Popover, PopoverTrigger, diff --git a/src/app/utils/auth.ts b/src/app/utils/auth.ts index 9051b165..a4cfdcf2 100644 --- a/src/app/utils/auth.ts +++ b/src/app/utils/auth.ts @@ -1,4 +1,4 @@ -'use server' +import 'server-only' import { cookies, headers } from 'next/headers' import { getPersonByMagicToken } from './airtable' diff --git a/src/app/utils/create-magic-session.ts b/src/app/utils/create-magic-session.ts new file mode 100644 index 00000000..bc63fffb --- /dev/null +++ b/src/app/utils/create-magic-session.ts @@ -0,0 +1,4 @@ +'use server' + +import { createMagicSession } from './auth' +export { createMagicSession } diff --git a/src/app/utils/get-session.ts b/src/app/utils/get-session.ts new file mode 100644 index 00000000..a4c6564f --- /dev/null +++ b/src/app/utils/get-session.ts @@ -0,0 +1,4 @@ +'use server' + +import { getSession } from './auth' +export { getSession } From 026860021022da132acbe66d9d28c22f5370f233 Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:30:44 -0500 Subject: [PATCH 15/16] Remove unused 'use server' from sign_in.tsx --- src/components/sign_in.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/sign_in.tsx b/src/components/sign_in.tsx index eb776b03..ca189e59 100644 --- a/src/components/sign_in.tsx +++ b/src/components/sign_in.tsx @@ -1,6 +1,3 @@ -'use server' - -import { buttonVariants } from '@/components/ui/button' import { headers } from 'next/headers' export default async function SignIn({ From d57f46f335d09adc6cba9cc19350b9e9f5baec5e Mon Sep 17 00:00:00 2001 From: Max Wofford Date: Thu, 2 Jan 2025 13:32:20 -0500 Subject: [PATCH 16/16] Remove unused 'use server' from nav.tsx --- src/components/nav.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/nav.tsx b/src/components/nav.tsx index ca7a1bcd..9c0c1c10 100644 --- a/src/components/nav.tsx +++ b/src/components/nav.tsx @@ -1,6 +1,4 @@ -'use server' - -import { getSession } from '@/app/utils/auth' +import { getSession } from '@/app/utils/get-session' import SignOut from './sign_out' import SignIn from './sign_in' import Image from 'next/image'